Making a game in JavaScript Part 6 : Movement and actions

“Let’s make that ninja run!”

The player character is not yet visible on the canvas, but we are going to change that very shortly.
We want to set the player idle, and then also the additional player running animation. When we have the player sitting at idle, and able to run in both left and right directions, we will then step up to add the player jump and attack mechanics.

To do this we will need to simultaneously develop the input management, also updating our player.js script to include the appropriate responses for the player.

First let us create the input controller. This is a simple, easily extended object that allows for mapping the keys as you wish, also allowing for the mapping to be later made customisable in-game or via a settings menu.

Open and save an ‘inputcontroller.js’ in the GameJS/js/ folder, and add the following code:

export class InputController
{
constructor()
{
this.eKeyState = {
eNone : 0,
ePressed : 1,
eDown : 2,
eReleased : 3
}
}

initialise(canvas)
{

}

cycleKeyPress(keyDown)
{

}

cycleKeyRelease(keyUp)
{

}

getKeyState(keymapping)
{

}
}

export default InputController;

The constructor here is setting a ‘keyState’ variable, which determines whether a key is in a state of pressed (this frame), down (still down from last frame), released (was down last frame, is up this frame), and none (no key state, the key is neither pressed, down, or just now released.)

This is important for determining and storing the state of any key we want to track for user input.

In the initialise function, add the following code:

initialise(canvas)
{
// Store the canvas object
this.canvas = canvas;

// Map a keyboard key to an action name
this.mappings = {‘w’:’Up’, ‘s’:’Down’, ‘a’:’Left’, ‘d’:’Right’, ‘Space’:’Attack’, ‘Ctrl’:’Throw’};

// Store the keystate for our actions – currently all keystates will be ‘none’.
this.keys = {
‘Up’: this.eKeyState.eNone, ‘Down’: this.eKeyState.eNone, ‘Left’: this.eKeyState.eNone, ‘Right’:this.eKeyState.eNone, ‘Attack’:this.eKeyState.eNone, ‘Throw’:this.eKeyState.eNone
};

// Add an event listener to the canvas, when a key down event occurs pass the key
// that is pressed to the ‘cycleKeyPress’ function to update the keystate
this.canvas.addEventListener(‘keydown’, function(k_down) {
this.cycleKeyPress(k_down.key);
}.bind(this), false );

// Add an event listener to the canvas, when a key released event occurs pass the key
// that is released to the ‘cycleKeyRelease’ function to update the key state
this.canvas.addEventListener(‘keyup’, function(k_up) {
this.cycleKeyRelease(k_up.key);
}.bind(this), false );

}

Update the cycleKeyPress function to include the following code:

cycleKeyPress(keyDown)
{
// Previously released keys are no longer in a ‘released’ state and are set to ‘none’
for(var keyState in this.keys)
{
if(this.keys[keyState] === this.eKeyState.eReleased)
{
this.keys[keyState] = this.eKeyState.eNone;
}
}

// If the key was previously set to ‘pressed’, update it to ‘down’
// Else set the key to ‘pressed’
if(this.keys[this.mappings[keyDown]] === this.eKeyState.ePressed)
{
this.keys[this.mappings[keyDown]] = this.eKeyState.eDown;
}
else
{
this.keys[this.mappings[keyDown]] = this.eKeyState.ePressed;
}
}

Update the cycleKeyRelease function to include the following code:

cycleKeyRelease(keyUp)
{
// Previously released keys are no longer in a ‘released’ state and are set to ‘none’
for(var keyState in this.keys)
{
if(this.keys[keyState] === this.eKeyState.eReleased)
{
this.keys[keyState] = this.eKeyState.eNone;
}
}

// Set the key state for ‘keyUp’ to ‘released’
this.keys[this.mappings[keyUp]] = this.eKeyState.eReleased;

}

And update the ‘getKeyState’ function:

// Return Keystate for an action, ‘Up’ ‘Down’ ‘Left’ ‘Right’ ‘Attack’ ‘Throw’
// Keystate = 0 for ‘None’ 1 for ‘Pressed’ 2 for ‘Down’ and 3 for ‘Released’
getKeyState(keymapping)
{
if(this.keys[keymapping] === this.eKeyState.eNone)
{
return 0;
}

if(this.keys[keymapping] === this.eKeyState.ePressed)
{
return 1;
}

if(this.keys[keymapping] === this.eKeyState.eDown)
{
return 2;
}

if(this.keys[keymapping] === this.eKeyState.eReleased)
{
return 3;
}
}

And that is our input controller! There is nothing more required for the input controller object but we will need to include that in the playable context object. Open ‘playablecontext.js’ and at the top of the existing script, with the ‘import’ lines of code, add another import for the input controller:

import { InputController } from ‘./inputcontroller.js’;

In the constructor for the PlayableContext object, add the line :

this.inputController = new InputController();

This can be the last line of the constructor function.

In the ‘initialise’ function of the PlayableContext, add the following line to initialise the input controller:

// Set the input controller
this.inputController.initialise(this.canvas);

That is the input controller! It will update the key states independently, as after it is initialised the event listener functions are added to the canvas. These updates to the key states can be checked during the update frames and if the key state is evaluated to be ‘Pressed’ or ‘Down’ or ‘Released’ for a particular game key, we can trigger the required action for the player character.

Next, we want to create the player character code, and we will start with a simplified version of the player character and for now just focus on two actions, ‘idle’ and ‘run’. We want to see that this is working correctly before we go and start adding all the additional action triggers and animations.

Open the ‘player.js’ script and add the following code:

import { AnimationCollection } from ‘./animationCollection.js’
import { InputController } from ‘./userinput.js’
import { Sprite } from ‘./Sprite.js’;

export class Player extends AnimationCollection
{
constructor()
{
super();
this.renderFrame = new Sprite();
this.bFacingRight = true;
this.playerPositionX = 400;
this.playerPositionY = 50;
this.xVelocity = 0.0;
}

initialise()
{
this.renderFrame = this.getAnimationFrame(‘idle’);
this.bFacingRight = true;
this.playerPositionX = 400;
this.playerPositionY = 50;
}

update(inputController)
{

}

getRenderFrame()
{
return this.renderFrame;
}

}

export default Player;

This ‘Player’ class object extends from the previously created ‘AnimationCollection’. When we ‘extend’ a class object using this instance as an example, we have created a new class object called ‘Player’ which is based on the previous object AnimationCollection and has all of the variables and functions of the AnimationCollection. The AnimationCollection is the ‘super’ or ‘base’ class, and the Player class can have additional or overriding functions while still using everything from it’s base class.

In the Player constructor, we call the ‘super()’ function first, which ensures that the AnimationCollection constructor is called. Then we initialise a sprite for the selected render frame from the given animations in the animation collection, we set the player character to be facing to the right (bFacingRight = true) and we set it’s x and y position on the canvas, and velocity.

The ‘getRenderFrame’ function does exactly that, it returns the current render frame, which is determined by the input and currently playing animation, selecting the current frame in the animation cycle.

We need to add the code for the ‘update’ function to contain the following lines:

update(inputController)
{
this.xVelocity = 0.0;

// Get input
if(inputController.getKeyState(‘Right’) === 1 || inputController.getKeyState(‘Right’) === 2)
{
this.xVelocity = 0.3;
}
else if(inputController.getKeyState(‘Left’) === 1 || inputController.getKeyState(‘Left’) === 2)
{
this.xVelocity = -0.3;
}
else
{
this.xVelocity = 0.0;
}

if(this.xVelocity > 0.2)
{
this.bFacingRight = true;
var run = this.animations[‘run’].update();
this.renderFrame = this.getAnimationFrame(‘run’);
}
else if(this.xVelocity < -0.2)
{
this.bFacingRight = false;
var run = this.animations[‘run’].update();
this.renderFrame = this.getAnimationFrame(‘run’);
}
else
{
// Update player animation
var idle = this.animations[‘idle’].update();
this.renderFrame = this.getAnimationFrame(‘idle’);
}
}

With the player class containing all of the requirements to have the player’s idle animation update to change direction to face the directional input and ‘run’ / play the run animation, and then return to idle when the user input is not triggering the velocity, we will want to test this is displaying and responding suitably before we start getting to far ahead with the other game mechanics.

Open the PlayableContext code in playablecontext.js, and add the following line to the top of the file with the other imports:

import { Player } from ‘./player.js’

And in the constructor add the following line before/above the line of code that is initialising the input controller:

this.player = new Player();

In the PlayableContext ‘initialise()’ function, add these lines after the lines of code for the canvas handle:

// Set the player animations
this.player.initialise();
this.setPlayerAnimations();

You will notice above, after we initialise the ‘this.player’ object within the PlayableContext, that we have placed a call to the function ‘this.setPlayerAnimations()’ which is referring to ‘setPlayerAnimations()’ function within this object (the playable context).

This function has not yet been written, so let’s add that now after the constructor here in the playablecontext.js script.

setPlayerAnimations()
{
// Set the image source file for each animation frame
for(var i = 0; i < 10; i++)
{
this.player.animations[‘idle’].animationFrameSrcs[i] = this.files.ninja_idleFrames[i];
this.player.animations[‘run’].animationFrameSrcs[i] = this.files.ninja_runFrames[i];
this.player.animations[‘jump’].animationFrameSrcs[i] = this.files.ninja_jumpFrames[i];
this.player.animations[‘dead’].animationFrameSrcs[i] = this.files.ninja_deadFrames[i];
this.player.animations[‘attack’].animationFrameSrcs[i] = this.files.ninja_swordAttackFrames[i];
this.player.animations[‘throw’].animationFrameSrcs[i] = this.files.ninja_throwAttackFrames[i];

this.player.animations[‘jumpAttack’].animationFrameSrcs[i] = this.files.ninja_jumpingSwordAttackFrames[i];
this.player.animations[‘jumpThrow’].animationFrameSrcs[i] = this.files.ninja_jumpingThrowAttackFrames[i];
this.player.animations[‘slide’].animationFrameSrcs[i] = this.files.ninja_slideFrames[i];
this.player.animations[‘glide’].animationFrameSrcs[i] = this.files.ninja_glideFrames[i];
this.player.animations[‘climb’].animationFrameSrcs[i] = this.files.ninja_climbFrames[i];
}

// Load the sprite image from the previous set source file locations
this.player.animations[‘idle’].loadSpritesFromSrc();
this.player.animations[‘run’].loadSpritesFromSrc();
this.player.animations[‘jump’].loadSpritesFromSrc();
this.player.animations[‘dead’].loadSpritesFromSrc();
this.player.animations[‘attack’].loadSpritesFromSrc();
this.player.animations[‘throw’].loadSpritesFromSrc();
this.player.animations[‘jumpAttack’].loadSpritesFromSrc();
this.player.animations[‘jumpThrow’].loadSpritesFromSrc();
this.player.animations[‘slide’].loadSpritesFromSrc();
this.player.animations[‘glide’].loadSpritesFromSrc();
this.player.animations[‘climb’].loadSpritesFromSrc();

// The ‘Run’ animation is slightly wider that the idle animation, and as such changing the width
// skews the height, we need to specifically update the dimensions of all of the frames
// for the run animation
for(var i = 0; i < 10; i++)
{
this.player.animations[‘run’].animationFrames[i].frameWidth = 93;
this.player.animations[‘run’].animationFrames[i].frameHeight = 112;
this.player.animations[‘run’].animationFrames[i].displayWidth = 93;
this.player.animations[‘run’].animationFrames[i].displayHeight = 112;
this.player.animations[‘run’].animationFrames[i].originalSourceWidth = 363; this.player.animations[‘run’].animationFrames[i].originalSourceHeight = 458;
this.player.animations[‘run’].animationFrames[i].sourceWidth = 363;
this.player.animations[‘run’].animationFrames[i].sourceHeight = 458;
this.player.animations[‘run’].animationFrames[i].ticksPerFrame = 1;
}

}

We now need to update the player object within the PlayableContext. In the ‘update()’ function, add the following code to call player update:

this.player.update(this.inputController);

And then render the player frame in the render() function of the PlayableContext:

render()
{
this.context = this.canvas.getContext(“2d”);
this.context.clearRect(0,0,1280,680);
this.playerFrame = this.player.getRenderFrame();

if(this.player.bFacingRight === true)
{
this.context.drawImage(
this.playerFrame.img,
this.playerFrame.sourceX,
this.playerFrame.sourceY,
this.playerFrame.sourceWidth,
this.playerFrame.sourceHeight,
this.playerFrame.positionX,
this.playerFrame.positionY,
this.playerFrame.displayWidth,
this.playerFrame.displayHeight);
}
else
{
this.context.save();
this.context.scale(-1,1);
this.context.drawImage(
this.playerFrame.img,
this.playerFrame.sourceX,
this.playerFrame.sourceY,
this.playerFrame.sourceWidth,
this.playerFrame.sourceHeight,
-this.playerFrame.positionX – this.playerFrame.displayWidth,
this.playerFrame.positionY,
this.playerFrame.displayWidth,
this.playerFrame.displayHeight
);

this.context.restore();
}
}
}

Now we have the code for the animationCollection, the animations, the animation frames or ‘sprites’, the file manager, the input controller, and the player. We have the playable context that brings it all together, and the app script which controls what the playable context is doing. We also have the index.html webpage that is running the app script. If we have put it all together right, when re run the python http server and host the index.html, we should be able to:

* See the player idle animation, it should be running smooth
* When pressing and holding ‘d’ on the keyboard, the player should start running to the right
* When pressing and holding ‘a’ on the keyboard, the player should start running to the left
* When the keys are released the player character should return to the idle animation, remaining the same direction it was facing when running.

Please note: There may be some typos in the existing provided code as I am editing the post spacing’s and wording as I go. I will follow the posts again from the start and ensure the code provided is in line with the coded project I am working from and make a note to update this and provide and note any corrections in time for the next post.

Update: 23/08/2023
I have gone through the code in my posts and found a couple of parts missing, which I have since gone back over and amended.
To save you time re-reading start to finish and locating the changes, and as I have not highlighted or recorded each individual change (and don’t want to miss highlighting an update that could cause more confusion 🙂 ) I have instead zipped the project files for the tutorial going this far into the project.

You can download a copy of the project files here:
(Broken Link – TBC) Making a JavaScript Game Part 1 – 6 Code + Image Files

If you would prefer, you can go through the posts and try to match your code to the updated tutorial code. The practice can be useful for those new to coding.

Some importantly useful tips for bug-finding when testing your code:

+ Ensure that you have at least the python http server running to host your JavaScript code or it will not run!

+ If you have the server running and your code is up to date but not displaying a ninja that can idle, or run left and right, then press the ‘F12’ key or go the the browser menu, down to ‘More tools’ and open the ‘Developer Tools’.
In the ‘Console’ tab of the Developer Tools you may find any error message that is being provided by the browser, which can help you locate and repair any errors in the script files.

There are a few ways to host the files, I have included one here that is overly simple and excellent for the purpose of code-and-test. It requires Python to be installed, and with it a simple command from the folder directory in the Windows PowerShell or Linux terminal you can start a http server for that folder, and then access the hosted folder as if it is a website by going to the local host address in your browser.

If you don’t already have Python 3 installed, you can download and install it from

https://python.org/ in the Downloads section.

At the time of writing this Python 3.11.4 is the latest version and should work nicely with the mentioned steps.

In Windows or Linux, there will often be an option to right-click within an open folder in the File explorer and select ‘Open location in terminal’ or ‘Open in Terminal’. Alternatively, you can open PowerShell in Windows or the Linux Terminal in Linux and navigate to your directory folder ‘GameJS’.

With the terminal or PowerShell window open and Python installed, you can start a simple http server with the following command:

python -m http.server

With the http server now running in the PowerShell or terminal window, you can access your site by opening your browser and typing the address:

http://localhost:8000

This will open the website up that is hosted on ‘localhost’ (your PC) on port 8000 (the default for the simple python http server).

Brent McEwan