Making a game in JavaScript Part 5 : Images and Animations

In this part, we cover the animation frame referred to as a ‘sprite’, the animation which is the collection of the sprites required to animate the required action, and the animationCollection, which is all the various actions that have been animated for this character.

We want to load the images we require for our game, and first image on the screen to start testing with is the player.

First, we need to have downloaded the NinjaBoy and/or NinjaGirl animation folders and saved them in our GameJS/images/ folder.

Next, open ‘filemanagement.js’ and we will create the ‘FileManager’ class. This class will handle the image loading and animation frame creation.

In the filemanagement.js we will put the following code for the FileManager.

export class FileManager
{
constructor()
{
//Set the default image folder path
this.imagePath = ‘../images/’;

// Set the Ninja Girl folder path
this.ninjaGirlPath = this.imagePath + ‘Characters/NinjaGirl/’;

// Set the Ninja Boy folder path
this.ninjaBoyPath = this.imagePath + ‘Characters/NinjaBoy/’;

// Set the number of frames per animation
this.animationFrameCount = 10;

// Set the sub folder for each animation
this.swordAttackFolder = ‘Attack/’;
this.throwAttackFolder = ‘Throw/’;
this.jumpSwordAttackFolder = ‘JumpAttack/’;
this.jumpThrowAttackFolder = ‘JumpThrow/’;
this.jumpFolder = ‘Jump/’;
this.runFolder = ‘Run/’;
this.idleFolder = ‘Idle/’;
this.deadFolder = ‘Dead/’;
this.slideFolder = ‘Slide/’;
this.glideFolder = ‘Glide/’;
this.climbFolder = ‘Climb/’;

// Set the gender to decide if using the NinjaBoy or NinjaGirl animations
this.ninjaGender = 0; // 0 = Boy, 1 = Girl

// Ninja Image Frames
this.ninja_idleFrames = [];
this.ninja_runFrames = [];
this.ninja_jumpFrames = [];
this.ninja_swordAttackFrames = [];
this.ninja_throwAttackFrames = [];
this.ninja_jumpingSwordAttackFrames = [];
this.ninja_jumpingThrowAttackFrames = [];
this.ninja_glideFrames = [];
this.ninja_slideFrames = [];
this.ninja_climbFrames = [];
this.ninja_deadFrames = [];

// Kunai Image Frame
this.kunaiFrames = [];

// Kunai Image Source
this.kunaiSrc = ‘Kunai.png’;

}

initialiseNinjaFrames()
{

var imgSourcePrefix = this.imgPath;

if(this.ninjaGender === 0)
{
imgSourcePrefix += this.ninjaBoyPath;
}
else
{
imgSourcePrefix += this.ninjaGirlPath;
}

// idle, run, jump, death, sword, throw, jumpSword, jumpThrow, slide, glide, Climb

for(var i = 0; i < 10; i++)
{
this.ninja_idleFrames[i] = (imgSourcePrefix + this.idleFolder + ‘Idle__00’ + i.toString() + ‘.png’);
this.ninja_runFrames[i] = (imgSourcePrefix + this.runFolder + ‘Run__00’ + i.toString() + ‘.png’);
this.ninja_jumpFrames[i] = (imgSourcePrefix + this.jumpFolder + ‘Jump__00’ + i.toString() + ‘.png’);
this.ninja_deadFrames[i] = (imgSourcePrefix + this.deadFolder + ‘Dead__00’ + i.toString() + ‘.png’);
this.ninja_swordAttackFrames[i] = (imgSourcePrefix + this.swordAttackFolder + ‘Attack__00’ + i.toString() + ‘.png’);
this.ninja_throwAttackFrames[i] = (imgSourcePrefix + this.throwAttackFolder + ‘Throw__00’ + i.toString() + ‘.png’);
this.ninja_jumpingSwordAttackFrames[i] = (imgSourcePrefix + this.jumpSwordAttackFolder + ‘Jump_Attack__00’ + i.toString() + ‘.png’);
this.ninja_jumpingThrowAttackFrames[i] = (imgSourcePrefix + this.jumpThrowAttackFolder + ‘Jump_Throw__00’ + i.toString() + ‘.png’);
this.ninja_slideFrames[i] = (imgSourcePrefix + this.slideFolder + ‘Slide__00’ + i.toString() + ‘.png’);
this.ninja_glideFrames[i] = (imgSourcePrefix + this.glideFolder + ‘Glide_00’ + i.toString() + ‘.png’);
this.ninja_climbFrames[i] = (imgSourcePrefix + this.climbFolder + ‘Climb_00’ + i.toString() + ‘.png’);

this.kunaiFrames[I] = (imgSourcePrefix + this.kunai);

}

}

}

export default FileManager;

The code above works like this:

The constructor first sets the base image folder path. Then stores the ninja character image path. Then stores the name of the folder for each animation. This way, the animation images can be accessed by adding ‘baseImagePath + ninjaCharacter + animationFolder’ which would equate to something like this: ‘../images/Characters/NinjaGirl/Idle/’.

The frame file names are stored like this: Idle_000.png, Idle_001.png, etc. So by using an incremental loop we can get each image file path by looping through an incrementing counter ‘I’ = 0 and increasing I for every loop, and loading the image:

‘../images/Characters/NinjaGirl/Idle/’ + ‘Idle_00’ + i.toString() + ‘.png’;

Or
‘‘baseImagePath + ninjaCharacter + animationFolder’ + ‘Idle_00’ + i.toString() + ‘.png’;”
‘‘baseImagePath + ninjaCharacter + animationFolder’ + ‘Run_00’ + i.toString() + ‘.png’;”
‘‘baseImagePath + ninjaCharacter + animationFolder’ + ‘Jump_00’ + i.toString() + ‘.png’;”

etc.

Now, if we create an object of the type ‘FileManager’, it will be created with the constructor initialised variables, and we can then use the object to call the function “initialiseNinjaFrames();”

This will not load the files into an image format yet, but what we have done here – is gathered the file paths of every animation frame that we need, so that we can then go and load each animation frame image for each animation, and we can load each animation for the player ninja character either as NinjaBoy or NinjaGirl.

To now load the images from these file paths, the correct structure needs to be in place so that we can load a collection of animations. The animation collection will handle loading of the animations in the collection, and each animation will handle loading each frame for that animation.

The way we do this is we create an object that stores the frame and all the frame related information. Then an object for the animation which stores all the animation frames and the data for that animation. And then we have an animation collection object that stores these animations.

Since ‘Image’ is already a type in JavaScript, we will call our animation frame object ‘Sprite’. We will then have a ‘Sprite’ class, an Animation class, and an AnimationCollection class.

If we open ‘sprite.js’, we should have already added the sprite object code from the previous tutorial step, which looks like this:

export class Sprite
{
constructor()
{

}

initialise()
{

}

update()
{

}

render()
{

}

}

export default Sprite;

We need to add the required data to this sprite class to get it to be able to load the animationFrame image, and to store the required properties of the animation frame like width, height, position etc.

To load an image in JavaScript, we could use the following code as an example:


var myImage = new Image();
myImage.src = ‘../folder/imagefilename.ext’;

This code would create an object of type Image and store it in the variable ‘myImage’.

With myImage now referring to an Image object, we could set the ‘source file’ of that image object, or myImage.src, to be the folder path and file name for an image file.

This will store the image details, but now if we wanted to draw the image we would use something like the following code:

context.drawImage(myImage, sourceX, sourceY, sourceWidth, sourceHeight,
positionX, positionY, frameWidth, frameHeight);

This drawImage function takes the following properties:
* The image type variable which should have an image source file
* The x Position in the source file to capture from, default would be 0
* The y Position in the source file to capture from, default would be 0
* The width of the frame you would like to capture from the source image
* The height of the frame you would like to capture from the source image

As well as the following Output properties:
* The Position X that you would like to start drawing the image on the canvas
* The Position Y that you would like to start drawing the image on the canvas
* The Width that you would like to stretch the captured source file to display to on the canvas
* The Height that you would like to stretch the captured source file to display to on the canvas

With these properties required for the draw image function, as well as the draw ‘context’ which we will get to later, we will need to lay out somewhere to store these properties for each image.

In the Sprite constructor function, please add the following code:

this.img = new Image();
this.src = ”;
this.sourceX = 0;
this.sourceY = 0;
this.sourceWidth = 232;
this.sourceHeight = 439;
this.positionX = 0;
this.positionY = 0;
this.displayWidth = 0;
this.displayHeight = 0;


// Sometimes the frame width will need to change for the specific animation, so we
// will store the original here also in the event we need to return to the original size
this.originalWidth = 232;
this.originalHeight = 439;

The above variables are required for the drawing of the images, but we also need to add some more properties for the timing of the animation frame. The following needs to be added to the constructor function below the image properties:


this.tickCount = 0;
this.frameIndex = 0;
this.ticksPerFrame = 3;

The tick count is so we can count each time the update function is called. The update function will return true each time it is called and the tick count is incremented. If the tick count gets to be as high as the number set for ticks per frame, the update function will instead return false. This way, we can see if the update function returns false then this animation frame is done updating, and when we receive this returned ‘false’ value we can step to the next frame in the animation.

We will also update the initialise function to receive the property values that we want to set for these properties and update the required values for the sprite object.

initialise(src, width, height, posX, posY)
{
this.src = src;
this.img = new Image();
this.img.src = this.src;
this.sourceX = 0;
this.sourceY = 0;
this.sourceWidth = 232;
this.sourceHeight = 439;
this.positionX = posX;
this.positionY = posY;
this.displayWidth = width;
this.displayHeight = height;


// Sometimes the frame width will need to change for the specific animation, so we
// will store the original here also in the event we need to return to the original size
this.originalWidth = 232;
this.originalHeight = 439;

}

For the update function, we want to add the code that determines if the animation frame is to be held on screen or if the time is up for this animation frame. The following code will help us determine that for each sprite, added in the update function:

update()
{
this.tickCount += 1;
if(this.tickCount > this.ticksPerFrame)
{
this.tickCount = 0;
return false;
}

return true;

}


In the render() function for the sprite we will add the following code.


render(context)
{
context.drawImage(this.img, this.sourceX, this.sourceY, this.sourceWidth, this.sourceHeight, this.positionX, this.positionY, this.displayWidth, this.displayHeight);

}

Now we have the code to create, initialise, update, and render our sprite object. This sprite object is a single animation frame image. We can create many of these and store them in an animation object.

First we need to include the sprite file in our animation.js script. Open ‘animation.js’ and at the very top of the script add this line of code:

import { Sprite } from ‘./sprite.js’;

Then, in the constructor, add the following:

this.frameIndex = 0; // The current frame that we are displaying starting at 0.
this.frameCount = 10; // The total number of animation frames.
this.animationFrameSrcs = []; // The list of folder paths and file names for each image
this.animationFrames = []; // The list of sprite objects containing each image

We also want to add the following functions for the animation object:

setFrameCount(count)
{
this.frameCount = count;
}

loadSpritesFromSrc()
{
var maxCount = 10;
for(var i = 0; i < maxCount; i++)
{
this.animationFrames[i] = new Sprite();
this.animationFrames[i].initialise(this.animationFrameSrcs[i], 58, 107, 600, 530);
}
}


And the update function will need to be updated with the following lines of code:

update()
{
if(this.animationFrames.length === 0)
{
return false;
}

if(this.animationFrames[this.frameIndex].update() === false)
{
this.frameIndex += 1;
}
else
{
return false;
}

if(this.frameIndex >= this.frameCount)
{
this.frameIndex = 0;
return false;
}

return true;

}

The update function is performing the following steps:
1. If the animation does not have more than zero frames, return false
2. If the animation frame is no longer updating, go to the next frameIndex (+=1)
ELSE return false to say ‘the animation is not yet done’, or in other words ‘Query: Does this animation frame require more updating True / False? Answer: False’
3. If the frameIndex is greater than or equal to the frame count then the frame index is set back to 0 and return false (no more updating required).
4. Otherwise, return ‘true’ – continue updating as is.

We also want to add the following function to get the current animation frame from the animation.

getCurrentFrame()
{
if(this.frameIndex < this.frameCount)
{
return this.animationFrames[this.frameIndex];
}
}

And the render function code:

render(context)
{
if(this.frameIndex < this.frameCount)
{
this.animationFrames[this.frameIndex].render(context);
}
}

We can save and close the animation.js script, and now open animationcollection.js.

In animationcollection.js, we will ensure it has the following:


import { Animation } from ‘./animation.js’;

export class AnimationCollection
{
constructor()
{
this.animations = {
‘idle’: new Animation(),
‘run’ : new Animation(),
‘jump’ : new Animation(),
‘slide’: new Animation(),
‘glide’ : new Animation(),
‘climb’ : new Animation(),
‘dead’ : new Animation(),
‘attack’ : new Animation(),
‘throw’ : new Animation(),
‘jumpAttack’ : new Animation(),
‘jumpThrow’ : new Animation()
};
}

update(animationType)
{
return this.animations[animationType].update();
}

getAnimationFrame(animationType)
{
return this.animations[animationType].getCurrentFrame();
}

}

export default AnimationCollection;

Now we have the code for the Sprite (the animation frame), the animation object, and the collection of animations. Next, we can look at creating a Player object and having it available to select the required animation, which we will later update the animations to match the player input.

For now, there will not be anything additional rendering on the canvas, but in the next step we will be able to load the web page and see the animations changed based on the user input.

Brent McEwan