7 x 1.5 hours input and learning sessions:
3 x 3.0 hours concept & work sessions
Reference documents for this session:
Session plan:
Task until next session:
Exercise 1
folder in our base cloud share for this course.General outline:
This time we will be more iterative and mix input and experimentation in
short cycles, along the 4 parts of creating our simple jumping block game
prototype.
Use the scaffold folder in course's base cloud folder and add a canvas to the HTML with a width of 800px and a height of 400px.
In the main.js
then create the following:
gameState
object that can store the players x and y position, as well as a reference to the canvas and the drawing contextdrawPlayer
function that receives the players position and the drawing context and then draws a filled rectangle as the player (suggested width and height: 40 pixels)update
function that clears the canvas (e.g. using clearRect()
), adds 10 pixels to the players x position and then draws the player calling the drawPlayer
function.init
function that receives a state
parameter and uses it to initialise the game state, draws the player for the first time and sets up an interval of 1 second, that calls the update function with the game state as an argumentIn the end you will need to call the init
function, once the document is set up. Basically you could just add a call to init()
somewhere in the main.js file, or in a separate script in the html that is loaded at the end. A good practice though is to check whether the document has been fully loaded and only then call the init function. To do that just put the following code at the end of your main.js (actually you could put it anywhere, but the end of the file seems to be a good place):
document.onreadystatechange = () => {
if (document.readyState === 'complete') {
init(gameState)
}
}
Under the following link you will find a reference solution for this part:
https://tantemalkah.at/artful-coding/2022st/session-content/session-4/part1/
Building on the last part we now want to add some controls to be able to move the player to the left and right. What you need to do for that is:
controls
object to your gameState
that stores whether the left, right or up keys are currently pressedhandleKeys
function that receives an event
and a state
parameter and then checks whether any of the three buttons is currently pressed and sets the values in gameState.controls
accordinglywindow.onkeydown = (event) => { handleKeys(event, state) }
window.onkeyup = (event) => { handleKeys(event, state) }
1000/50
(which is 20 ms) to get a game/rendering speed of 50 fps.Under the following link you will find a reference solution for this part:
https://tantemalkah.at/artful-coding/2022st/session-content/session-4/part2/
Building on the last part, we now also want to be able to jump, when the up key is pressed.
Here we have to add a y velocity to the player, because we need some form of gravity that pulls the player down, once they start jumping up. Also the player should only be allowed to jump, if they are standing on the ground. If (in the last part) you already set the controls also for the up button, now you only need to add some code to the update
function that calculates the y velocity and the y position on the player, before the player gets redrawn.
For a very simple gravity model we could just set the y velocity e.g. to -20 when the player starts jumping. The y position then is calculated similarly to the x position just by adding the current y velocity to the y position. In order for the player to not indefinitely jump upwards, we have to reduce the upwards velocity. Or in other terms, we have to add some gravitational pull. So whenever the player is not standing on the ground, we can just add 1 to the y velocity. As soon as the player reaches the ground again, we can set the y velocity to 0 again.
Under the following link you will find a reference solution for this part:
https://tantemalkah.at/artful-coding/2022st/session-content/session-4/part3/
Wow, we can already run around and jump. This is quite nice. But for the final prototype we should also add game boundaries, so the player cannot run out of the screen. Also some visual ground would be nice. And to top all of that, one obstacle would be nice too.
So what you'll have to do is:
drawGround
function, that just draws a line (or a long and thin rectangle) below where the player is standing, and call this function in your update loopdrawObstacle
function that draws a rectangle at a certain position in a different colour than the player. This too has to be called in the update loop. Additionally we have to add some code that checks whether the player is colliding with the obstacle and in that case not let them move further to the left/right, if the obstacle would be in the way.Under the following link you will find a reference solution for this part:
https://tantemalkah.at/artful-coding/2022st/session-content/session-4/part4/
Exercise 2
folder in our base cloud share for this course. To do so, create a folder with your student id and put your code in itGeneral outline:
While our code so far (also in Artful Coding 1) could always be run by opening
the files directly from your hard drive with a browser, usually at some point
you might want to put your code onto a webserver. Additionally, for more complex
projects and in modern web development in general, we might want to use modules.
Modules are a neat way to structure our code into different files, and to import
only those parts, we need at a certain point in our application. The MDN Web Docs
has a guide on JavaScript modules
that explains why modules can be very helpful, and how they work.
The important thing is, that most browsers will - for security reasons - not load
modules when you open your HTML file directly from your harddrive.
We would have to actually put the code on a web server to make it work. This would
be of course tedious to do, everytime we change and test our code. That is what
development servers are good for.
There are many different options out there. And there are also a lot of guides out
there how to do that. For example the Getting Started with Phaser 3 Guide
talks about this right away in the intro section. If you already have a setup with
any developmen/live server, feel free to reuse this. Or try out those things mentioned
in the Phaser 3 Guide.
But if you are already using VSCode as your editor, the probably best and least
effort solution is to install the Live Server
extension by Ritwick Dey.
Next we will download the current stable Phaser 3 release in its minified version.
For more details on the Phaser 3 setup, follow the Getting Started guide linked
above. As a shortcut, you can go to https://phaser.io/download/stable and download
the linked phaser.min.js file. Put this into the assets folder of a fresh
project (based e.g. on our scaffold).
Then just add a <script src="assets/phaser.min.js"></script>
right before you load
your own main.js file. In the main.js we can work with Phaser 3. For a start
we'll follow the Getting Started with Phaser 3 guide and create the Hello World
from there.
To start we have to create a game config by adding the following:
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
physics: {
default: 'arcade',
arcade: {
gravity: {
y: 200
}
}
},
scene: {
preload: preload,
create: create
}
};
Then we have to create a new instance of a game with this config:
var game = new Phaser.Game(config);
Once we did this, we can use the preload
and create
functions
(configured as defaults for preloading and creating the first game
scene in the config.scene
object):
function preload () {
}
function create () {
}
The preload
function is used to load all game assets that will be
used in a scene, for example images, sounds, etc. Everything that
should be available right away when we later render it in the game.
For this Hello World three images will be loaded from the Phase Labs
website. So let's put the following into our preload
function.
this.load.setBaseURL('http://labs.phaser.io');
this.load.image('sky', 'assets/skies/space3.png');
this.load.image('logo', 'assets/sprites/phaser3-logo.png');
this.load.image('red', 'assets/particles/red.png');
This does not change anything yet, it just makes sure that we can
use those images by using the strings 'sky'
, 'logo'
and 'red'
later on in our create
function.
To actually create some things happening on the screen we now put
the following code into the create
function:
this.add.image(400, 300, 'sky');
var particles = this.add.particles('red');
var emitter = particles.createEmitter({
speed: 100,
scale: { start: 1, end: 0 },
blendMode: 'ADD'
});
var logo = this.physics.add.image(400, 100, 'logo');
logo.setVelocity(100, 200);
logo.setBounce(1, 1);
logo.setCollideWorldBounds(true);
emitter.startFollow(logo);
You should now see a Phaser 3 logo moving around the sky,
emitting pinkish red particles.
Now that we can use the code from above as our first Phaser 3 boilerplate.
Just remove everything again from the preload
and the create
functions,
so that they are empty. The we will also add an update
function similar
to the other two. And we also have to add it to our scene config, which
then should read:
scene: {
preload: preload,
create: create,
update: update,
}
One more little thing you might want to do is to explicitly place the
canvas in your page, so that you can also center it.
In our course scaffold folder we could just add the canvas between the
header and footer in the index.html like this:
<div class="centered">
<canvas id="game-canvas"></canvas>
</div>
Then in the main.js we have to add a canvas option to our game config,
telling phaser which HTML element to use as the cavas, as well as explicitly
set the render type to the canvas engine:
type: Phaser.CANVAS,
canvas: document.getElementById('game-canvas'),
Now we are ready to implement our Jumping Block prototype in Phaser.
First, let's add some of the game objects in the create
function.
As our ground, obstacle and player all are just rectangles of a
different color, we can easily add them to the scene like this:
function create () {
// add the ground
this.add.rectangle(0,300, 800,5, 0xCCCCCC).setOrigin(0,0)
// add the player
this.add.rectangle(100,200, 40,40, 0xff00ff).setOrigin(0, 0)
// add the obstacle
this.add.rectangle(200,230, 30,70, 0x00aa00).setOrigin(0,0)
}
Now we already see the first static image of the scene. But it does
not do anything. Now we have to add all the movement and collission
magic to make our scene dynamic. While this was quite tedious in our
Canvas Jumping Block example last session, Phaser with its physics
model can do a lot of the tedious work for us.
Before we do that, one important thing to keep in mind: we are adding
the Game objects here by using this
. While we just defined a simple
function, outside of any object, the function will be called from
the Scene object, as we configured it in our game config. This is
why this
in context of the preload
, create
, and update
function
always refers to the current scene. This way we can also store our
own objects (e.g. a reference to our player) on the scene, e.g.
by setting this.player
.
Now, lets continue, by creating a group of static physics objects,
which we'll call ground. And then add our actual ground and the
obstacle to it.
function create () {
// create a static physics object group to fill with our non-movable objects
const ground = this.physics.add.staticGroup()
// add the ground
const ground_rect = this.add.rectangle(0,300, 800,5, 0xCCCCCC).setOrigin(0,0)
ground.add(ground_rect)
// add the player
this.add.rectangle(100,200, 40,40, 0xff00ff).setOrigin(0, 0)
// add the obstacle
const obstacle1 = this.add.rectangle(200,230, 30,70, 0x00aa00).setOrigin(0,0)
ground.add(obstacle1)
}
So the only thing we did is to use this.phyics.add.staticGroup()
to
create a statics physics group, store it in a ground
variable, and
then use its ground.add()
function to add the rectangles we have
already drawn for the ground and the obstacle.
Not that here we used const ground
, as we will not need the ground
elsewhere later. But in case we would want to use it also in our
update
function we should rather store it on the Scene object,
e.g. as this.ground
. You'll see in a minute why this is important
when we create the player's dynamic physics object and the movement
controls.
For now nothing really changed. We still see the same thing as before.
Only now Phaser knows that that the ground and obstacle rectangles
are not just two simple drawings on the canvas, but actual physical
objects that will not move. So we can later do stuff with it.
But first, let's create an actual physics object for our player.
Instead of the standalone this.add.rectangle
line for our player
we now write it like this:
// add the player
const player_rect = this.add.rectangle(100,200, 40,40, 0xff00ff).setOrigin(0, 0)
this.player = this.physics.add.existing(player_rect)
So we call this.physics.add.existing()
to create a new dynamic physics
object, as we did use this.physics.add.staticGroup()
to create an
empty group for static physics object. A difference her ist, that
we use the group to add objects afterwards. With the add.existing
we can take a GameObject we already create (our player rectangle)
and make a dynamic physics object out of it. That is why we store
it as this.player
, so we can later access it in our update
function.
Take look. Now we will only see our player shortly, because as soon
as we load the page and the game starts, the player starts falling.
Well, because it is a dynamic physics object, and we have a physics
set with a gravity. And gravity usually pulls down objects towards
the bottom.
But, you may wonder, what do we have our ground for? This is a good
question, because this is exactly what we created our ground for,
as a static physics object group. So first of all the ground does
not fall down, because it is static and therefore not affected by
gravity. Secondly, we now want to use the ground to stop the player
falling. We can do that by telling Phaser that those two things,
our player, and our static objects group called ground, can actually
collide.
We only need to add a collider
to the physics of our scene:
// add a collider between ground and player
this.physics.add.collider(ground, this.player)
Awesome, so the player starts falling, but stops on the ground.
And this is also the reason why, in our little prototype, ground
was created as local variable in the create
function, and not
stored on the scene object itself. Because all we set up, is
done in the create. Phaser then keeps track of everything. But
imagine you add some features to the game, so that another
obstacle could suddenly show up, after the player jumped over
the first obstacle. We would have to check for that in our
update
function. Then it would be handy to have a quick reference
to the ground again.
Now, it would be nice if we could actually move the player. So
let's add some controls. Remember how tedious it was in the last
session when we had to do that manually?. The proces is somewhat
similar in Phaser, only it is soooo much less tedious.
In our create
function we just need to create an object that
keeps track of the keys we want to use:
// add a cursors object that keeps track of our cursor key status
this.cursors = this.input.keyboard.createCursorKeys()
That is all. No tedious writing of event handlers. Phaser handles
if for us. Now in our update
function we can check whether the
left, right or up arrows are pressed and instruct the player to
move.
Let's start with left/right movement:
function update () {
if (this.cursors.left.isDown) {
this.player.body.setVelocityX(-200)
} else if (this.cursors.right.isDown) {
this.player.body.setVelocityX(200)
} else {
this.player.body.setVelocityX(0)
}
}
Whenever the left button is pressed down, we set a negativ x velocity
to the player, so the player object will automatically start to move
to the left. In case left is not pressed, but right, we set a positive
x velocity, so the player moves right. And then we also need to check
if non of those two buttons is pressed, in which case the player should
stop again, so we set the velocity to 0.
Wonderful, now we can run left and right, and we can already bump into
our obstacle, which will stop as. Because we added it to the ground
objects group, which is configured to collide with the player. Remember,
how tedious the collission handling was in the last session?
But there is a problem. We can run out of the frame on the left. And we
can't even return. Well, because the ground really only goes to the
left side of the frame (x-position: 0). If you go beyond that, you'll
just fall, because there is no ground, but there is gravity all around.
Try it out, if you go left, and quickly go right again after you are
outside the frame, you will see the player falling down below the ground.
So we have to add another collission check. We coul create another
obstacle, that is just outside the visible area, and add it to the ground.
But there is an even easier way to do that. Because dynamic physics objects
have a function setCollideWorldBounds()
, which we can call to set whether
the object collides with the visible bounds of our scene. We can do
that in the create
function just after creating the player, by adding:
this.player.body.setCollideWorldBounds(true)
Note that this function is defined on the player's body, that was added
to the rectangle after we created with add.existing
. You might come
accross many examples where this is done directly on the player or
other physics object. We'll see examples in the next session. This
has to do with how the physics object was created. For now it is just
important to keep in mind that physics objects always have a Body
(which is its own class in the Phaser physics system), and this is
used to handle all collissions, movements, etc.: physics stuff, you
need a body for.
Ok, so, now we are only missing the capability to jump, in order to
also cross the obstacle. There is not a lot left to do now. We only
have to check whether the up key is pressed (AND if the player is
standing on the ground), and then set a negativ y velocity:
if (this.cursors.space.isDown && this.player.body.touching.down) {
this.player.body.setVelocityY(-200)
}
Wow, that's it. Our game is done. And we can even land on the
obstacle and jump again from there.
Now you can also play around with the values for gravity and
velocities, to find your preferred jump height and velocity.
And why did we check for the player touching the ground?
Well, try it out and fly to get high.
If you are puzzled by what other things all of those game objects
could to, take a look at https://phaser.io/learn
There you'll find, among other things, the
Making your first Game
guide as well as the API Docs. While
the API Docs might be quite intimidating at first, they can be helpful
when you already know a few things but just can't remember how the
parameters are used, or how it is actually called. Try to go there,
and click on the Game Objects
section, just to get a peek at what different game objects there are.
There you already see that you could use a lot of different things
instead of our boring rectangles. Or just try to search for a term,
e.g. "body" and you will quickly get to the page for the
Phaser.Physics.Arcade.Body,
on which you can see what other properties and methods your player body has.
Under the following link you will find a reference solution for this session:
https://tantemalkah.at/artful-coding/2022st/session-content/session-5/
Exercise 3
folder in our base cloud share for this course.This session is used to reflect on what we have done so far, and elaborate
on particular topics the participants want to know more about, or are struggling
with.
It is also used as a common working session to finish and experiment with
the recent exercises.
In these two final sessions we will recreate the mini game featured on the
Artful Coding 2 website.
The idea of the mini game is that the player can run around and jump to collect
the randomly distributed letters, which are contained in the the title
"Artful Coding 2". To make it a bit more interesting, the there are some
obstacles (rocks on the ground) the player has to jump over. Also the ground ends after some time walking
to the left and right. If the player falls down, they will respawn centered
above the ground again. Every letter that is collected will the be highlighted
in the pages main heading, which is initially set to 10% opacity.
To achieve this we we will use:
Previously we created the Jumping Block in Phaser simply in
one single main.js file and without any sophisticated help by our
IDE. But for this session we'll create a more modern, and especially more
scalable setup. Because at some point, having everything in one
main.js file, without structuring things into different classes and
objects, will become immensely unwieldy. It will also hamper your
development progress.
Let's start by creating a new scaffold folder for Phaser games. We could
simply call it scaffold-phaser
. In it we need:
assets
folder containing:
phaser.min.js
, downloaded from https://phaser.io/download/stablesrc
folder containing (for now):
lib
types
index.html
file, containing our game canvas - more on that laterstyle.css
that gets included by the index.htmljsconfig.json
file containing the following configuration:
{
"compilerOptions": {
"module": "es6",
"target": "es6"
}
}
The content of the index.html could look something like the following:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
<title></title>
</head>
<body>
<header class="centered">
header-content
</header>
<section class="centered">
<canvas id="game-canvas">
Your browser does not seem to support the HTML 5 Canvas element, sorry.
</canvas>
</section>
<footer class="centered">
footer-content
</footer>
<script src="assets/phaser.min.js"></script>
<script type="module" src="src/main.js"></script>
</body>
</html>
Important: note the subtle difference in loading the main.js file here
vs. how we did it so far. Here we use a type="module"
attribute in the script
tag, to tell the browser, that this is not just a single Javascript file
to load and execute, but a module that can use import
statments to load stuff
from other modules and export
to provide stuff to other modules.
The style.css in this scaffold then does not contain anything other then
a rule for the centered
class to center our header, footer and game canvas:
.centered {
margin: auto;
width: fit-content;
}
Now, for that to work, we also need the actual src/main.js
file, in which
we create a new Phaser game with a basic configuration:
import Phaser from './lib/phaser.js'
// import Scene from './Scene.js'
export default new Phaser.Game({
type: Phaser.CANVAS,
width: 1000,
height: 280,
canvas: document.getElementById('game-canvas'),
// scene: [Scene],
physics: {
default: 'arcade',
arcade: {
gravity: {
y: 600,
},
//debug: true,
}
}
})
The only thing still missing for this to be functional is the src/lib/phaser.js
file.
You see in the code above, that we are importing the Phaser
class from it. But
how does that work? The only thing we have to out into the src/lib/phaser.js
file is:
export default window.Phaser
This is, because our setup now facilitates ES6 modules. But Phaser itself was just
included as an unmodularised script tag in the index.html, just before our main.js
was included as a module. So Phaser
already exists on the window
object, but we
want to be able to import it consistently everywhere in our modules. That is what
the src/lib/phaser.js
does: it takes window.Phaser
and exports it as a default.
So we can use import Phaser from './lib/phaser.js'
in our main.js (and other modules),
to use the Phaser class.
Once this is done, the (empty) game should load, and the browser console should
tell you that Phaser (and which version of it) is loaded.
Now the only convenience thing still missing to guide us when writing code, are
the type definitions for Phaser.
These are files provided by Phaser, which help your IDE (e.g. VS Code), to
provide you with more context info when coding. They can be found in the
types
folder of the official Phaser repository.
To use those type definitions with the IntelliSense
feature in VS Code, we need to get the phaser.d.ts
file from there and put it into the
src/types
folder.
Once you have that set up, you can type Phaser.
in your code (e.g. in the main.js)
and VS Code will provide you with a lot of things you could do with the Phaser object.
You can find a reference scaffold-phaser folder, also packed into a .zip or .tgz file,
on the website in session-content/session-6.
Now, before you change any code, create a copy from your scaffold folder, so you
can later reuse it.
In the previous Jumping Block prototype in Phaser our game config contained the
following:
const config = {
// ...
scene: {
preload: preload,
create: create,
update: update,
}
}
This implicitly set up the game with a single scene, linking the preload
,
create
and update
functions of that scene to the three similarly named
functions defined in the main.js.
Now we still will only need one scene, but we want to go big about that!
Also, this is already a good preparation if we want to add additional scenes.
E.g. we might want to add an intro scene for the game, or a scene for displaying
an outro, credits, or a success / game over screen.
This is why we'll modify our game config and remove the three comments that
have been set in the src/main.js of the scaffold folder. It now should look
like this:
import Phaser from './lib/phaser.js'
import Scene from './Scene.js'
export default new Phaser.Game({
type: Phaser.CANVAS,
width: 1000,
height: 280,
canvas: document.getElementById('game-canvas'),
scene: [Scene],
physics: {
default: 'arcade',
arcade: {
gravity: {
y: 600,
},
debug: true,
}
}
})
So ourscene
configuration is now just a list with one scene class: [Scene]
. And
this Scene
is loaded from the Scene.js file, so we'll have to create it. Note,
that I also removed the comment in the physics config for the debug: true
. Having
set debug to true will help a lot throughout development to see how physics objects
behave and interact. But don't forget to turn it back to false (or comment this line)
in your finished game.
But now, let's create the src/Scene.js
file from which Scene
gets imported:
import Phaser from './lib/phaser.js'
export default class Scene extends Phaser.Scene {
constructor () {
super('scene')
}
init () {
}
preload () {
}
create () {
}
update () {
}
}
Here we create a new class Scene
which is derived from the Phaser.Scene
class.
So it will do everything as a regular Phaser Scene. That is why in the constructor
we also call super('scene')
to do all the initialisations Phaser has in place for
a standard Scene.
Then we add four methods to our Scene class: init, preload, create, and update.
Check out the Phaser API docs for Phaser.Scene.
It tells us that we
can also define the optional methods
init(),
preload(),
and create().
We can also see that the Phaser.Scene has only one method: update().
And it also tells us:
This method should be overridden by your own Scenes.
This method is called once per game step while the scene is running.
While we already kind of know what these methods do from our previous Jumping Block example,
it is always good to have the docs ready, in case we want to be sure. Also it tells us,
that init
will be called before preload
and create
, and can be used to set up everything
that can be set up before we load our assets and create our game objects.
We will use the init later to set up the letters of the title, which will be highlighted
whenever the player collects the letters in the game.
For now we'll just load some assets and create a ground. To load the assets put the
following into the preload
function:
preload () {
this.load.spritesheet(
'player', 'assets/character_spritesheet.png',
{ frameWidth: 125, frameHeight: 125 },
);
this.load.image('rock1', 'assets/rock_50x50.png')
this.load.image('rock2', 'assets/rock_75x50.png')
this.load.image('rock3', 'assets/rock_50x75.png')
this.load.image('rock4', 'assets/rock_75x75.png')
}
To be able to load those files you have to create them first. Or use the ones in
the assets folder of the website.
Now we can use them to create the ground and some obstacles. This is done in the
create()
method, but we'll also add an addRocks()
method, which handles all the
obstacle creation:
create () {
this.ground = this.physics.add.staticGroup()
let rect = this.add.rectangle(-500,260, 1500,10, 0xffffff).setOrigin(0, 0)
this.ground.add(rect)
this.addRocks()
}
addRocks () {
this.ground.create(-700,240, 'rock2')
this.ground.create(-100,240, 'rock1')
this.ground.create(20,230, 'rock4')
this.ground.create(700,240, 'rock1')
this.ground.create(1200,230, 'rock3')
}
While this is functionally enough and you should see the two most central
obstacles in your game canvas, we should also add the declaration for the
ground to the class definition. Add the following to the top of the
Scene class:
/** @type {Phaser.Physics.Arcade.StaticGroup} */
ground
This tells the class, that it contains a property ground
. And the comment
tells VS Code, that the type of whatever ground
will be initialised with
(later in the the create method) is a
Phaser.Physics.Arcade.StaticGroup.
With that, whenever we use e.g. this.ground.create() or this.ground.add() in our methods,
VS Code will provide us useful context information with what the expected parameters are
and what the method does.
That's it for part 1. We have a scene, containing a ground and some obstacles.
Under the following link you will find a reference solution for this part:
https://tantemalkah.at/artful-coding/2022st/session-content/session-6/part-1/
For this second part let's add a player to the scene.
We'll add a class member definition for the player and the cursor keys, which
will be used to control the player. So just after the definition of the ground
add the following:
/** @type {Phaser.Physics.Arcade.Sprite} */
player
/** @type {Phaser.Types.Input.Keyboard.CursorKeys} */
cursors
Now in the create
function, after we have created the ground, we create a
player sprite and add a collider between ground and player:
this.player = this.physics.add.sprite(400,200, 'player')
this.player.body.setSize(35)
this.physics.add.collider(this.ground, this.player)
Now let's initialise our cursors and add an event handler for when the
up key or space is pressed:
this.cursors = this.input.keyboard.createCursorKeys()
this.cursors.up.onDown = (event) => { this.jump(event) }
this.cursors.space.onDown = (event) => { this.jump(event) }
Of course we now also have to add a jump
function to our class:
jump(event) {
if (this.player.body.touching.down) {
this.player.setVelocityY(-400)
}
}
To add some left and right movement we will use our tried and tested
approach of checking the keys in the update function. But we'll add three
helper functions to our class first: walkLeft
, walkRight
and walkStop
:
walkLeft(event) {
this.player.setVelocityX(-200)
}
walkRight(event) {
this.player.setVelocityX(200)
}
walkStop(event) {
if (this.cursors.left.isDown || this.cursors.right.isDown) return
this.player.setVelocityX(0)
}
Now the update function can be quite succinct (and later we cann add controls
for the player animation in the walk functions):
update () {
if (this.cursors.left.isDown) {
this.walkLeft()
} else if (this.cursors.right.isDown) {
this.walkRight()
} else {
this.walkStop()
}
}
Now we can run left and right and jump. We can also run and jump out of the
screen. But in this case the player should be able to walk further than that.
Only we would like to follow them. For that we can use the Scene.cameras
which Phaser provides for every scene. We only have to tell the main camera
to follow the player object. The only thing we need to do for that is to
add the following to lines in our create
function:
this.cameras.main.startFollow(this.player)
this.cameras.main.setDeadzone(200, 100)
Now we can move and the camera will follow the player, if they leave the
center by a defined amount (200, 100). Check the docs of the
setDeadzone
function and play around with the values if you want.
Another cool thing we can do with cameras: Zoom! While this is not needed for
the game, it might help us while developing it. E.g. to place obastacles and
later all the letters, it might be nice to see a bigger part of the scene.
So let's add a zoom function which we can use to zoom out and in, by pressing
the Shift key - but only if debug is enabled. In the create
we'll just add
one more event handler, similar to jump:
if (this.game.config.physics.arcade.debug) {
this.cursors.shift.onDown = (event) => { this.zoom(event) }
}
Of course we also need to add the zoom
function now to our Scene class:
zoom(event) {
if (this.cameras.main.zoomX === 1) {
this.cameras.main.setZoom(0.3, 0.3)
} else {
this.cameras.main.setZoom(1, 1)
}
}
What the function does is to check whether the main cameras zoom is set to 1,
and in that case set a new zoom level to 0.3 in x and y direction. Otherwise
(if it is not 1, it will be 0.3), it will be set back to 1 in x and y direction.
Now we can use the Shift key to see the whole scene. Play around with the values
in the setZoom
function, to see how it works. And check the documentation for the
setZoom function,
if you want to know more how that works.
That's it for part 2. Now we can run and jump around the scene with our player.
Under the following link you will find a reference solution for this part:
https://tantemalkah.at/artful-coding/2022st/session-content/session-6/part-2/
For this part we will use our spritesheet to create walking animations, so
that the character will not move around like a very stiff stick figure, but
a more animated one.
For this we have to first create some animations by telling the framework
which frames from the spritesheet to use. So let's put the following code
somewhere into our create()
function, e.g. at the beginning:
this.anims.create({
key: 'walk-right',
frames: this.anims.generateFrameNumbers('player', {start: 1, end: 4}),
frameRate: 7,
repeat: -1,
})
this.anims.create({
key: 'walk-left',
frames: this.anims.generateFrameNumbers('player', {start: 9, end: 6}),
frameRate: 7,
repeat: -1,
})
To get into the details on what is happening here check out the docs for the
create() function of the AnimationManager
and the Animation config object.
Basically we are just setting up two animation cycles that we label walk-right
and walk-left
, so we can easily apply it later to some GameObject - in this
case we will want to apply it to the player. The frameRate
is used to set
the speed of the animation, the repeat
parameter can be used to define how
often the animation should repeat (-1 is used here to repeat infinitely), and
the frames
just define the single images/frames in the animation. This is
an array of AnimationFrame
objects, which we could set up manually. But there are different ways to easily
create the animation frames. We just use the AnimationManager's
generateFrameNumbers()
function, to generate this for us, based on the player
sprite we loaded
in our preload()
function.
So, once this animation is set up, we can use it. And we want to start the
animation whenever the player starts to move, and stop it when the player
stops. As we have been clever before, we already have some functions to
walkLeft
, walkRight
, and walkStop
. So we'll just add some lines there to
start and stop the animation. The adapted code then should look like this:
walkLeft(event) {
this.player.setVelocityX(-200)
this.player.anims.play('walk-left', true)
}
walkRight(event) {
this.player.setVelocityX(200)
this.player.anims.play('walk-right', true)
}
walkStop(event) {
if (this.cursors.left.isDown || this.cursors.right.isDown) return
this.player.setVelocityX(0)
this.player.anims.stop()
this.player.setFrame(0)
}
So we just added one line with a call to the AnimationManager's play()
function
in the functions where we start walking. And a similar line with the corresponding
stop()
function in walkStop(). Additionally we use the setFrame()
function to
reset the animation state to the standing-still image for the player.
But try to play around with it. What happens, when you remove the stop or setFrame
functions again?
And of course this is just a simple, first version of walking animation. But it
already looks so much more appealing than the stiffly unanimated stick figure
from before.
So we are almost done for this part. The only thing we'll still tackle is the case
when the player falls down. So far, you'll just keep falling down, and the game
is basically over without telling you.
In our case we want the player to just fall a little bit, and then respawn above
the platform again, so they can keep collecting the letters, which we'll add in
the next and final part.
The only thing we really need to do for this, is add a little condition in the
update()
function that just checks if the player is below a certain point
(or in corrdinates: above a certain y value). In that case we'll just reset
the player's y postion. And we'll also reset the x position, so that they
are on the center of the platform again:
// when player jumps off the ground, they respawn above again
if (this.player.y > 1000) {
this.player.setX(400)
this.player.setY(0)
this.player.setVelocityY(0)
}
You'll notice that we also set the players y velocity back to 0. Try it
out without it. And also play around with the y values. It seems a bit
smoother this way, but maybe you find even better settings.
That was it for part 3.
Under the following link you will find a reference solution for this part:
https://tantemalkah.at/artful-coding/2022st/session-content/session-6/part-3/
Now for the final part of this little game prototype we want to add some
letters to the scene, which can be collected to light up corresponding
letters in the HTML page.
For that, first of all, we need some content on the HTML page. So let's put
two headings into our <header>
, and give it an id of title
and subtitle
:
<h1 id="title">Artful Coding</h1>
<h2 id="subtitle">Web-based games development <span id="letter-2">2</span></h2>
You will notice, that I put the 2
inside a <span>
tag. This is for two
reasons: first, this should be styled differently than the rest of the heading.
Second, we need some container for the letter, that we can address with an
ID. Because as soon as the player will collect the 2
in the game, we want
to light up this letter too.
Now, the same would actually also be needed for every letter in the "Artful Coding"
title. And I could do that just here, manually, in the HTML. To put a <span>
with
some unique ID around every letter. But this would be quite tedious, and there is
a better, procedural way to do it: with a loop in our init()
function. We'll
get to that in a minute.
First let's also add some styling to our styles.css, so that the 2 (and the spanned
letters we'll add shortly) are displayed with an opacity of 10%, a centered title, and
a white font on black background:
body {
background-color: black;
color: white;
}
#title, #subtitle {
text-align: center;
}
#title span,
#subtitle span {
opacity: .1;
}
That already looks a little more like on the artful coding web page. Now let's create
the <span>
s for the single letters in the title:
init () {
const elTitle = document.getElementById('title')
let titleHTML = ''
for (const l of 'Artful Coding') {
if (l === ' ') {
titleHTML += ' '
} else {
titleHTML += `<span id="letter-${l}">${l}</span>`
}
}
elTitle.innerHTML = titleHTML
}
Here we are just addressing our HTML element as usual. Then we create a string
titleHTML
, which starts out empty. In a loop through every letter of "Artful Coding"
(this includes the space), we'll add something to the titleHTML
string. If it is
the space itself, just a space should be added. But if it is a letter, we'll add
a whole span tag, with an ID of "letter-${l}"
and the letter itself inside it.
If you are unfamiliar with this syntax to create strings with dynamic content,
check out the MDN docs on template literals.
It would be similar to add a string like this: '<span id="' + l + '">' + l + '</span>'
.
But template literals are a really neat way to contruct strings with content from
your variables.
So once we contructed the whole string, we just set it as the .innerHTML
of our
element. Now also the letters in the title should be displayed with 10% opacity.
Next, we have to add letters to our scene. We'll do that in our create()
function
like everything else, and we'll need to use the Text
GameObject from Phaser. But as our letters will have some unique properties, and
we have a modular setup already, we'll create our own GameObject.
For that we create a separate file Letter.js, next to our Scene.js and the
main.js, and put the following in it:
import Phaser from './lib/phaser.js'
export default class Letter extends Phaser.GameObjects.Text {
/**
*
* @param {Phaser.Scene} scene
* @param {number} x
* @param {number} y
* @param {string} text
*/
constructor (scene, x, y, text) {
const r = Phaser.Math.RND.integerInRange(128, 255)
const g = Phaser.Math.RND.integerInRange(128, 255)
const b = Phaser.Math.RND.integerInRange(128, 255)
/** @type Phaser.Types.GameObjects.Text.TextStyle */
const style = {
fontFamily: 'monospace',
fontSize: '48px',
color: `rgb(${r}, ${g}, ${b})`,
}
super(scene, x, y, text, style)
}
}
What you see here, could also be called a wrapper around the standard
Phaser.GameObject.Text. The first part of the magic lies in the definition:
export default class Letter extends Phaser.GameObjects.Text
. This is similar to
how we set up our Scene. So we are creating a class Letter
, which
extends Phaser.GameObjects.Text
. This means, that Letter has all the porperties
and methods Text already has - only that we can then add or change specific things.
And then we export default
this class, so that it can be imported in our Scene
(and potentially every other Scene).
The comment block after the definition then is not necessary functionally, but
it tells VS Code (and other tools) how to interpret the parameters of our
contructor. This is helpfull wenn we use the Letter somewhere else, because
VS Code can provide some context info as help.
The second part of the magic then is our constructor (scene, x, y, text)
, which
will be used to construct a new Text object with a specific style.
In it we use Phasers random number generate to create three random RGB colour
values. Then we create a style
object, where we instruct the Text to
use a monospace font of 48px, with our random RGB value. Then we call
the constructor of our parent class with super(scene, x, y, text, style)
.
That is similar if we would create a new Text
object.
Now this Letter class can be used to create Text objects, without the
need to provide a style
configuration for every letter, and with every letter
getting a random colour.
So let's use this letter in our Scene. First we need to import it in the top
of our Scene.js file:
import Letter from './Letter.js'
Then we will also add a property letters
to our class definition (just after
where we also defined ground
, player
and cursors
). Similar to ground
it
will be a static physics object group:
/** @type {Phaser.Physics.Arcade.StaticGroup} */
letters
In our create()
function, just after we create the player
, we'll add some code
to initialise the letters
group, and tell it that it will contain objects created
from the class Letter
. We'll also call an addLetters
function, which should handle
our letter creation code.
this.letters = this.physics.add.staticGroup({classType: Letter})
this.addLetters()
Of course we have to add the addLetters
method now to our class (similar to addRocks
).
In a first version let's just try to place a single letter, to see how it works:
addLetters() {
const l = new Letter(this, 400, 50, 'A')
this.add.existing(l)
this.letters.add(l)
}
You should see a single letter "A" now, a bit above the player. Now, why did we have
to add it two times? Try playing around with this code and comment out on of the
two add functions and see what it does.
The this.add.existing()
function is responsible to add a GameObject to this scene,
so that it will be displayed. Because only creating the Letter does not do a lot. The
letter afterwards will exist in the whole games Phaser universe, but is not in any
way related to the scene. The this.letters.add()
then is used only to add the letter
also to the static physics group. This does not matter very much at the current point.
You will only see (in debug mode), that a blue border will show up around the letter.
But we need this later, because we want to check collissions / overlaps between
letters and the player.
Good, now, we have one letter in the scene. We now could continue to use these three
lines over and over again, to place all other letters. If it would be important to us,
that every letter is placed at a very specific, carefully designed position, then that
would be a good option.
Also, to get a feel for it, try out to add some other letters manually, before you
continue.
But in the end we just want to have the letters (somewhat) randomly distributed
accross the scene. So, again, we'll use a procedural way to do it, and modify the
code of the addLetters()
function as follows (I've included lots of comments to
walk you through this process):
addLetters() {
// first we just want to randomise the order of the letters how they
// should appear in the game
let sortedLetters = []
for (const l of 'ArtfulCoding2') { sortedLetters.push(l) }
let randomLetters = []
// for every item in our sortedLetters array we will now take out a
// random one and add it to the randomLetters array, as long as there
// is something left to take out
while (sortedLetters.length > 0) {
const index = Phaser.Math.RND.integerInRange(0, sortedLetters.length-1)
randomLetters.push(sortedLetters.splice(index, 1))
}
// now we create a slightly random first x position on the very left
// of our scene
let x = Phaser.Math.RND.integerInRange(-1000, -900)
// and for every letter we will now create an actual Letter object
// at another (slightly) randomised postion from the left of the scene
// to the right
for (const letter of randomLetters) {
// for every letter just add something between 150 and 200 pixels
x += Phaser.Math.RND.integerInRange(150, 200)
// the y should always be between 0 and 50 (a bit above the player)
const y = Phaser.Math.RND.integerInRange(0, 50)
// now use this x and y to create a new Letter object for the letter
const l = new Letter(this, x,y, letter)
// add it to the scene explicityl, so that it shows up
this.add.existing(l)
// add it to our static physics group, so we can later handle
// collisions / overlaps with the player
this.letters.add(l)
}
}
Now you should have a scene filled with all the letters from the title,
plus the 2. And every time you reload the scene, the letters will be arranged
differently and have different colours.
Now, still in debug mode and having built in our nice zoom function, is a good
moment to test the zoom. Press Shift to see the whole scene and the distribution
of letters. If you don't like it, play around with the values in the addLetters
function.
The only thing still missing now is some function that gets called whenever the
player collides - or in this case overlaps - with the letters. This function then
should handle the removal of the letter from the scene, and the highlighting of
the corresponding letter in the heading.
While we already use a collider between the player and the ground, in this case
we don't want the player to actually collide with the letters. Because the letters
should not block the players jump. Instead of using a collider and a collision handler,
we'll add an overlap
handler, just after we called the this.addLetters()
method
in our create()
method:
this.physics.add.overlap(
this.player,
this.letters,
this.collectLetter,
undefined,
this,
)
This defines the player
and the letters
group as two things Phaser should check
for overlaps. And as soon as an overlap happens the this.collectLetter
method
should be calle (which we still have to create). The undefined
as the fourth
parameter tells Phaser not to start a processCallback function, because we
already have a collideCallback function. And the fifth parameter is the
callbackContext, which should be our scene, so this
. Check the docs for the Phaser Arcade
Factory.overlap
function, if you want to know more how that works.
Now, whenever there is an overlap (or a collission, but without blocking the players
path), the collectLetter
method of our scene will be called. So let's add this:
collectLetter(player, letter) {
// remove the letter from the scene and disable the its physics body
this.letters.killAndHide(letter)
this.physics.world.disableBody(letter.body)
// now apply the letters colour to the corresponding letter in the
// header, and set its opacity to 1
const element = document.getElementById('letter-'+letter.text)
element.style.color = letter.style.color
element.style.opacity = 1
}
The collectLetter
method will always get two parameters, when it is called by Phasers
event handling system: the two objects that are colliding/overlapping. And as we
defined it with the player first and the letters second, when we added the overlap
,
here we'll have the same order. In this case we just don't do anything with the
player, because we only remove the letter object from the scene, and then highlight
the correspoding letter in the heading.
So, that's it. The game is done. You can now happily collect letters.
Or refine it and add some more obstacles, create your own assets and animations,
use different texts, add sounds, or whatever else comes to your mind.
And keep in mind that you can use the zoom option with Shift, when you work on the
scene. But also keep in mind to turn debug: false
in the game config, once you are
done and want to release the game.
Under the following link you will find a reference solution for this final part:
https://tantemalkah.at/artful-coding/2022st/session-content/session-6/part-4/
Happy hacking and happy letter collecting!
Exercise 4
folder in our base cloud share for this course.Disclaimer: this session is tailored towards 2 participants, so you would need
more time and probably less iterations in case of a bigger class
10min:
We start with a short recap on what we did so far (in Artful Coding 1 & 2). The
aim of this session now is to switch away from learing and refining our coding
skills to ideation and game creation.
5min:
To start with, everyone familiarizes themselves with out collab board on tryeraser.com
and doodles around in the designated "Doodle Area".
5min:
The we start a one minute exercise: "Draw a (perfect, of course) dog!"
Afterwards we share our drawings and tell a little bit about our dog's character.
(This is mainly to loosen up and to shift our minds away from trying to be perfect)
30min (3x(5+5)):
Now a first ideation sequence starts, where we'll generate three different game
ideas based on the contraints we get from a game jam idea generator:
https://steven-the-gamer.itch.io/game-jam-idea-generator
With one set of contraints everyone gets 5 minutes, to create one game idea.
Then we'll share those ideas, before we create new game contraints with the
generator. This repeats until everyone created 3 different game ideas.
10min:
Here we'll take a first little break, which participants can also use to think
about whether they want to take up one of the game ideas to develop further,
or if they already an idea of their own which they would like to implement
within the remainder of this course.
20min (5+15):
Intro to this sequence, where everyone gets 15 minutes to refine one of the
game ideas. Then the 15 min counter starts in which the participants extend one
of the game ideas by describing:
20min:
Sharing of ideas and discussion
This session is intended as a workshop session, where participants continue on refining
their game concepts from the last session. The focus lies on developing the concept into
a concrete description of a minimal version of the game and to outline a first approach
to implementation.
This page is part of the course website at https://tantemalkah.at/artful-coding/2022st
All contents, where not otherwise noted, are licensed by Andrea Ida Malkah Klaura under a CC-BY-SA 4.0 license.