Follow the instructions here to get the base for the code: Repo.
Open your browser and use the local webserver of choice from the Repo instructions to load the web page.
Open up the developer console on your browser. We will be checking this console for errors while we develop the game.
Your game in the browser should look similar to this (with the developer console open):
Asset Manager
Lets get an asset manager implemented real quick.
Create a new file: engine/assets.js
.
class Assets {
constructor() {
this.success = 0
this.error = 0
this.paths = []
this.cache = {}
}
add(paths) {
this.paths = this.paths.concat(paths)
}
get(path) {
return this.cache[path]
}
loaded() {
return this.paths.length === (this.success + this.error)
}
download(callback) {
if (!callback) callback = () => {}
this.paths.forEach(path => {
const image = new Image()
image.addEventListener('load', () => {
this.success += 1
if (this.loaded()) callback()
})
image.addEventListener('error', () => {
this.error += 1
if (this.loaded()) callback()
})
image.src = path
this.cache[path] = image
})
}
}
Add assets.js to index.html
:
<!-- Game Engine: Dependencies -->
<script src="assets/tilesets.js"></script>
<script src="engine/assets.js"></script> <!-- Add This Line -->
Add our assets to our asset manager in index.html
:
<!-- Run The Game -->
<script>
const assets = new Assets() // Add These Lines
assets.add([
'assets/player.png',
'assets/sandwater.png',
'assets/skeleton.png',
'assets/plasmaball.png',
])
const game = new Game({
stage: document.getElementById('stage'),
music: document.getElementById('audio-music'),
cast: document.getElementById('audio-shoot'),
death: document.getElementById('audio-death'),
hit: document.getElementById('audio-hit'),
level: localStorage.getItem('current-level') || 'square1'
})
</script>
Entities
Now for one of the core pieces of an ECS (Entity, Component, System) engine.
Create a new file: engine/entity.js
class Entity {
constructor() {
this.id = (+new Date()).toString(16) + (Math.random() * 100000000 | 0).toString(16)
this._components = {}
}
set(component) {
this._components[component.name] = component
return this
}
com(name) {
return this._components[name]
}
delete(name) {
delete this._components[name]
return this
}
has(names = []) {
const found = []
names.forEach(name => {
if (this._components.hasOwnProperty(name)) found.push(name)
})
return found.length === names.length
}
missing(names = []) {
return !this.has(names)
}
is(name) {
if (this.missing(['type'])) return false
return this.com('type').value === name
}
}
Add entity.js to index.html
<!-- Game Engine: ECS -->
<script src="engine/game.js"></script>
<script src="engine/entity.js"></script> <!-- Add This Line -->
Add a place to track entities in engine/game.js
this.showHitboxes = false
this.entities = [] // Add This Line
const element = config.stage
Game System Loop and Renderer
Before we draw characters or maps on the screen, we need a way to draw to the screen in a systematic way.
Remove this section from the engine/game.js
constructor:
// Temporary Hello World
this.stage.fillStyle = '#ecf0f1'
this.stage.textAlign = 'center'
this.stage.font = '24px sans-serif'
this.stage.fillText('Stage Set And Ready To Work!', this.width / 2, this.height / 2)
this.stage.fillStyle = '#95a5a6'
this.stage.font = '16px sans-serif'
this.stage.fillText('If everything loaded correctly, we can get started.', this.width / 2, this.height / 2 + 32)
Add these new methods to engine/game.js
:
loop() {
this.entities.forEach((entity, i) => {
// todo: movement, animation, etc systems
})
this.render()
window.requestAnimationFrame(() => this.loop())
}
render() {
const updates = []
this.entities.forEach(entity => {
if (entity.has(['active', 'renderer']) updates.push(entity.com('renderer'))
})
if (!updates.length) return
this.stage.clearRect(0, 0, this.width, this.height)
updates.forEach(update => update.run(this.stage))
}
Add loop to the engine/game.js
constructor:
this.stage = element.getContext('2d')
assets.download(() => this.loop()) // Add This Line
}
Refresh your game in the browser and make sure no errors show up in the console.
First Components
Components are the C in ECS and we’ve already referenced active
and renderer
in previous code. Lets add those:
Create a new file: engine/components.js
.
class Components {
static active() {
return {
name: 'active'
}
}
static renderer(callback) {
return {
name: 'renderer',
run: callback
}
}
}
Add components to index.html
:
<!-- Game Engine: ECS -->
<script src="engine/entity.js"></script>
<script src="engine/components.js"></script> <!-- Add This Line -->
Refresh in the browser and check for errors.
Character
Lets get our player character on the screen. The core of this is adding components to an entity. And to make this easy, we make an assemblage (something that returns the entity with all the components attached).
Create a new file engine/characters.js
.
class Characters {
static entity(x = 0, y = 0, imgPath) {
const entity = new Entity()
entity.set(Components.active())
entity.set(Components.position(x, y))
entity.set(Components.size(64, 64))
entity.set(Components.type('character'))
// todo: add velocity
// todo: add health
entity.set(Components.renderer(context => {
const pos = entity.com('position')
const size = entity.com('size')
const image = assets.get(imgPath)
const frame = {x: 0, y: 64 * 2}
context.drawImage(image, frame.x, frame.y, size.w, size.h, pos.x, pos.y, size.w, size.h)
}))
// todo: add hitbox
// todo: add collision
// todo: add death handler
return entity
}
static Player(x = 0, y = 0) {
const entity = Characters.entity(x, y, 'assets/player.png')
entity.set(Components.type('player'))
// todo: Add walk down animation
return entity
}
}
We need to add these new components in engine/components.js
:
static type(value) {
return {
name: 'type',
value: value
}
}
static position(x = 0, y = 0) {
return {
name: 'position',
x: x,
y: y
}
}
static size(w = 0, h = 0) {
return {
name: 'size',
w: w,
h: h
}
}
Add the new file to index.html
:
<!-- Game Engine: Component Assemblies -->
<script src="engine/characters.js"></script> <!-- Add This Line -->
Next, we add this character to engine/game.js
:
this.stage = element.getContext('2d')
this.player = Characters.Player(32, 32) // Add These Line
this.entities.push(this.player)
assets.download(() => this.loop())
Refresh your game in the browser and see your character on screen (or fix any errors).
Movement
Let’s get our character responding to keyboard input.
First, our character entity needs a velocity component in engine/components.js
:
static velocity(vx = 0, vy = 0) {
return {
name: 'velocity',
vx: vx,
vy: vy
}
}
Replace our todo with velocity in our character assembly code in engine/characters.js
:
entity.set(Components.size(64, 64))
entity.set(Components.type('character'))
entity.set(Components.velocity(0, 0)) // Add This Line
Create a new file to bind input keys: engine/input.js
:
class Input {
constructor(entity, game) {
this.game = game
this.entity = entity
this.bindMovement()
}
bind(codes = []) {
const key = {
codes: codes,
isDown: false,
isUp: true,
press: undefined,
release: undefined
}
window.addEventListener('keydown', (e) => {
if (e.defaultPrevented) return
if (!key.codes.includes(e.code)) return
if (key.isUp && key.press) key.press()
key.isDown = true
key.isUp = false
e.preventDefault()
}, false)
window.addEventListener('keyup', (e) => {
if (e.defaultPrevented) return
if (!key.codes.includes(e.code)) return
if (key.isDown && key.release) key.release()
key.isDown = false
key.isUp = true
e.preventDefault()
}, false)
return key
}
bindMovement() {
if (this.entity.missing(['velocity', 'active'])) return
const up = this.bind(['ArrowUp', 'KeyW']),
down = this.bind(['ArrowDown', 'KeyS']),
left = this.bind(['ArrowLeft', 'KeyA']),
right = this.bind(['ArrowRight', 'KeyD'])
up.press = () => {
this.entity.com('velocity').vx = 0
this.entity.com('velocity').vy = -3
// todo: set walk up animation
}
up.release = () => {
if (down.isDown) return
this.entity.com('velocity').vy = 0
}
down.press = () => {
this.entity.com('velocity').vx = 0
this.entity.com('velocity').vy = 3
// todo: set walk down animation
}
down.release = () => {
if (up.isDown) return
this.entity.com('velocity').vy = 0
}
left.press = () => {
this.entity.com('velocity').vx = -3;
this.entity.com('velocity').vy = 0;
// todo: set walk left animation
}
left.release = () => {
if (right.isDown) return
this.entity.com('velocity').vx = 0;
}
right.press = () => {
this.entity.com('velocity').vx = 3;
this.entity.com('velocity').vy = 0;
// todo: set walk right animation
}
right.release = () => {
if (left.isDown) return
this.entity.com('velocity').vx = 0;
}
}
}
Add it to index.html
:
<!-- Input Controls -->
<script src="engine/input.js"></script> <!-- Add This Line -->
Give our player entity to input in engine/game.js
:
this.player = Characters.Player(32, 32)
this.entities.push(this.player)
this.input = new Input(this.player, this) // Add This Line
assets.download(() => this.loop())
And, add our first game system in engine/game.js
inside the loop()
method:
this.entities.forEach((entity, i) => {
// todo: movement, animation, damage etc
if (entity.has(['active', 'position', 'velocity'])) this.move(entity) // Add This Line
})
Create our movement system in engine/game.js
:
move(entity) {
if (!entity.com('velocity').vx && !entity.com('velocity').vy) return // not moving
// todo: collision detection with interactions
entity.com('position').x += entity.com('velocity').vx
entity.com('position').y += entity.com('velocity').vy
}
Finally, refresh your game in the browser and move the character around the stage.
Maps & Levels
Lets add maps and levels to our game.
First we need a way to interpret assets/tilesets.js
and turn it into a x, y
coordinates. Create a the file engine/utils.js
:
const decoder = {a: 0, b: 32, c: 64, d: 96, e: 128, f: 160}
function decode(letters) {
const x = letters.charAt(0), y = letters.charAt(1)
return {x: decoder[x], y: decoder[y]}
}
Create a new file engine/maps.js
:
class Maps {
static tile(x = 0, y = 0, frame) {
const entity = new Entity()
entity.set(Components.position(x, y))
entity.set(Components.size(32,32))
entity.set(Components.type('tile'))
entity.set(Components.active())
entity.set(Components.renderer((context) => {
const pos = entity.com('position')
const size = entity.com('size')
const image = assets.get(frame.src)
context.drawImage(image, frame.x, frame.y, size.w, size.h, pos.x, pos.y, size.w, size.h)
}))
return entity
}
static parse(data) {
const entities = []
const get = (col, row) => {
return data.tiles[row * data.cols + col]
}
for (let col = 0; col < data.cols; col++) {
for (let row = 0; row < data.rows; row++) {
const letters = get(col, row)
const pos = decode(letters)
entities.push(Maps.tile(col * data.size, row * data.size, {
src: data.src, x: pos.x, y: pos.y
}))
}
}
return entities
}
static SquareIsland() {
const entities = Maps.parse({
src: 'assets/sandwater.png',
cols: 16,
rows: 16,
size: 32,
tiles: tilesets.square1
})
// todo: boundaries
return entities
}
static WhyIsland() {
const entities = Maps.parse({
src: 'assets/sandwater.png',
cols: 16,
rows: 16,
size: 32,
tiles: tilesets.why1
})
// todo: boundaries
return entities
}
}
And that goes into a level at the new file engine/levels.js
:
class Levels {
constructor(player) {
this.player = player
this.items = {
'square1': Levels.SquareIsland1,
'why1': Levels.WhyIsland1
}
}
select(name) {
const level = this.items[name]
if (!level) throw `unknown level ${name}`
return level(this.player)
}
static SquareIsland1(player) {
const entities = Maps.SquareIsland()
// todo: add enemies to the level
player.com('position').x = 32
player.com('position').y = 32
// todo: reset player animations
entities.push(player)
return entities
}
static WhyIsland1(player) {
const entities = Maps.WhyIsland()
// todo: add enemies to the level
player.com('position').x = 13 * 32
player.com('position').y = 32
// todo: reset player animations
entities.push(player)
return entities
}
}
Add our new files to index.html
:
<!-- Game Engine: Dependencies -->
<script src="assets/tilesets.js"></script>
<script src="engine/assets.js"></script>
<script src="engine/utils.js"></script> <!-- Add This Line -->
<!-- Game Engine: Component Assemblies -->
<script src="engine/characters.js"></script>
<script src="engine/maps.js"></script> <!-- Add This Line -->
<script src="engine/levels.js"></script> <!-- Add This Line -->
In engine/game.js
change constructor()
with the following code:
this.player = Characters.Player(32, 32)
this.input = new Input(this.player, this)
this.selectLevel = config.level // Add These Two Lines
this.levels = new Levels(this.player)
assets.download(() => this.loop())
And finally, add this new method to allow our dev controls to change the levels:
changeLevel() {
if (!this.selectLevel) return
this.entities = this.levels.select(this.selectLevel)
this.selectLevel = undefined
}
Refresh the browser and click on the Dev Controls
Load Level buttons to switch between the two layouts.
Hit Box and Hit Box Outline
Right now the character can move “off” screen since there is nothing to collide with. Before we make our simple collision system we need a hitbox mechanism.
Add hitbox method to engine/components.js
:
static hitbox(callback) {
return {
name: 'hitbox',
run: callback
}
}
We want to be able to see the hitboxes to confirm that they work correctly, so add a drawHitboxes
method to engine/game.js
:
drawHitboxes() {
this.stage.strokeStyle = '#f44336'
this.entities.forEach(entity => {
if (entity.missing(['hitbox', 'active'])) return
const hitbox = entity.com('hitbox').run()
this.stage.strokeRect(hitbox.x, hitbox.y, hitbox.w, hitbox.h)
})
}
And at the bottom of our loop()
method add the call to drawHitboxes in engine/game.js
:
loop() {
this.changeLevel()
// ...
if (this.showHitboxes) this.drawHitboxes()
window.requestAnimationFrame(() => this.loop())
}
Lets give our player character a hitbox in engine/characters.js
:
class Characters {
static entity(x = 0, y = 0, imgPath) {
// ...
entity.set(Components.hitbox(() => {
return {
x: entity.com('position').x + 15,
y: entity.com('position').y + 15,
w: entity.com('size').w - 30,
h: entity.com('size').h - 15
}
}))
// todo: add collision
// todo: add death handler
return entity
}
// ...
}
If we refresh the browser and hit the “Toggle Hit Box Outlines” in Dev Controls, we should see an outline around our character:
Collision System
Load the “SquareIsland1” Level from Dev Controls and make sure that your hitbox outline is on:
First thing, add a collision method to engine/components.js
:
static collision(callback) {
return {
name: 'collision',
run: callback
}
}
Then, add a collision component to our character in engine/characters.js
:
class Characters {
static entity(x = 0, y = 0, imgPath) {
// ...
entity.set(Components.collision(target => {
if (target.is('wall')) return true // collided
if (target.is('enemy')) return true // collided
return false
}))
// todo: add death handler
return entity
}
// ...
}
We need a way to detect when two rectangles collide in our game so add this function to engine/utils.js
:
// a rect is {x, y, w, h}
function collides(rect1, rect2) {
return rect1.x < rect2.x + rect2.w &&
rect1.x + rect1.w > rect2.x &&
rect1.y < rect2.y + rect2.h &&
rect1.y + rect1.h > rect2.y
}
Our game needs to know how to handle collisions so lets add that to move()
in engine/game.js
:
move(entity) {
if (!entity.com('velocity').vx && !entity.com('velocity').vy) return // not moving
const hitbox = entity.com('hitbox').run()
const future = {
x: hitbox.x,
y: hitbox.y,
w: hitbox.w,
h: hitbox.h
}
// get where the entity would be if we applied the current velocity values
future.x += entity.com('velocity').vx
future.y += entity.com('velocity').vy
for (let i = 0; i < this.entities.length; i++) {
const target = this.entities[i]
if (target.id === entity.id) continue
if (target.missing(['hitbox', 'active'])) continue
if (!collides(future, target.com('hitbox').run())) continue
const stop = entity.com('collision').run(target) // interactions
if (stop) return // collision told us not to move the entity
}
entity.com('position').x += entity.com('velocity').vx
entity.com('position').y += entity.com('velocity').vy
}
And add “collision” and “hitbox” component checks to loop()
:
loop() {
this.changeLevel()
this.entities.forEach((entity, i) => {
// todo: movement, animation, etc
if (entity.has(['active', 'velocity', 'position', 'collision', 'hitbox'])) this.move(entity)
})
// ...
}
To test our hitbox and collision add the following temporary code to our SquareIsland1 level in engine/levels.js
:
static SquareIsland1(game) {
const entities = Maps.SquareIsland()
// todo: add enemies to the level
// temporary wall
const wall = new Entity()
wall.set(Components.active())
wall.set(Components.type('wall'))
wall.set(Components.hitbox(() => {
return {
x: game.width / 2 - 32,
y: game.height / 2 - 32,
w: 64,
h: 64
}
}))
entities.push(wall)
game.player.com('position').x = 32
game.player.com('position').y = 32
// todo: reset player animations
entities.push(game.player)
return entities
}
Refresh the browser and you should see a red square in the middle of the map that your character cannot move through.
Delete the temporary code we added from engine/levels.js
:
static SquareIsland1(game) {
const entities = Maps.SquareIsland()
// todo: add enemies to the level
game.player.com('position').x = 32
game.player.com('position').y = 32
// todo: reset player animations
entities.push(game.player)
return entities
}
Walls as Boundaries
To keep our characters on land, we will use walls as boundaries.
Add a wall method to engine/maps.js
:
static wall(x = 0, y = 0, w = 0, h = 0) {
const entity = new Entity()
entity.set(Components.position(x, y))
entity.set(Components.size(w, h))
entity.set(Components.active())
entity.set(Components.type('wall'))
entity.set(Components.hitbox(() => {
return {
x: entity.com('position').x,
y: entity.com('position').y,
w: entity.com('size').w,
h: entity.com('size').h
}
}))
return entity
}
I’ve precalculated the values needed to surround “SquareIsland1” in walls so add these values to engine/maps.js
:
static SquareIsland() {
const entities = Maps.parse({
src: 'assets/sandwater.png',
cols: 16,
rows: 16,
size: 32,
tiles: tilesets.square1
})
// walls
entities.push(
Maps.wall(0, 0, 512, 5), // top
Maps.wall(0, 0, 15, 512), // left
Maps.wall(0, 490, 512, 15), // bottom
Maps.wall(497, 0, 15, 512), // right
)
return entities
}
Refresh the game in the browser and you should see hit boxes surrounding “SquareIsland1”:
And here are the values needed to do “WhyIsland1”:
static WhyIsland() {
const entities = Maps.parse({
src: 'assets/sandwater.png',
cols: 16,
rows: 16,
size: 32,
tiles: tilesets.why1
})
// walls
entities.push(
Maps.wall(0, 0, 512, 5), // top
Maps.wall(0, 0, 15, 512), // left
Maps.wall(0, 497, 512, 15), // bottom
Maps.wall(497, 0, 15, 512), // right
// Lower Left water
Maps.wall(0, 268, 110, 224),
// upper middle water
Maps.wall(118, 0, 278, 160),
Maps.wall(160, 160, 128, 29),
// lower right water
Maps.wall(403, 332, 96, 148),
)
return entities
}
If you are curious about how the maps are created from their sprite sheet, check out this companion post.
Animations
Animations should make this more interesting.
Add these methods to engine/components.js
:
static animation(row = 1, frameRange = [0, 0], speed = 70, size = 64, next) {
if (typeof frameRange === 'number') frameRange = [0, frameRange]
const component = {
name: 'animation',
tick: 0,
frame: 0,
frames: [],
speed: speed,
next: next
}
const animationRow = (row - 1) * size
for (let i = frameRange[0]; i < frameRange[1]; i++) {
component.frames.push({x: i * size, y: animationRow})
}
return component
}
static direction(value) {
return {
name: 'direction',
value: value
}
}
static repeat() {
return {
name: 'repeat'
}
}
Add this construction method to engine/characters.js
:
static walk(dir) {
switch (dir) {
case 'up':
return Components.animation(9, [1, 8], 70, 64, Components.repeat())
case 'down':
return Components.animation(11, [1, 8], 70, 64, Components.repeat())
case 'left':
return Components.animation(10, [1, 8], 70, 64, Components.repeat())
case 'right':
return Components.animation(12, [1, 8], 70, 64, Components.repeat())
}
throw `Characters.walk unknown direction: ${dir}`
}
Update the character to have an animation at Player()
in engine/characters.js
:
static Player(x = 0, y = 0) {
const entity = Characters.entity(x, y, 'assets/player.png')
entity.set(Components.type('player'))
entity.set(Characters.walk('down')) // Add This Line
return entity
}
Update our renderer at entity()
in engine/characters.js
:
static entity(...) {
// ...
entity.set(Components.renderer(context => {
const pos = entity.com('position')
const size = entity.com('size')
const image = assets.get(imgPath)
const animation = entity.com('animation') // Add This Line
const frame = animation.frames[animation.frame] // Change This Line
context.drawImage(image, frame.x, frame.y, size.w, size.h, pos.x, pos.y, size.w, size.h)
}))
// ...
}
Add an animate()
method to engine/game.js
:
animate(entity) {
const animation = entity.com('animation')
animation.tick += 0.1
if (animation.tick * 100 >= animation.delay) {
animation.frame++
animation.tick = 0
}
if (animation.frame >= animation.frames.length) {
if (!animation.next) return
if (typeof animation.next === 'function') return animation.next()
if (animation.next.name === 'repeat') animation.frame = 0
if (animation.next.name === 'animation') entity.set(animation.next)
}
}
Animation Changes
Add our different animation directions to our movement binding in engine/input.js
up.press = () => {
this.entity.com('velocity').vx = 0
this.entity.com('velocity').vy = -3
this.entity.set(Characters.walk('up')) // Add This Line
}
// ...
down.press = () => {
this.entity.com('velocity').vx = 0
this.entity.com('velocity').vy = 3
this.entity.set(Characters.walk('down')) // Add This Line
}
// ...
left.press = () => {
this.entity.com('velocity').vx = -3;
this.entity.com('velocity').vy = 0;
this.entity.set(Characters.walk('left')) // Add This Line
}
// ...
right.press = () => {
this.entity.com('velocity').vx = 3;
this.entity.com('velocity').vy = 0;
this.entity.set(Characters.walk('right')) // Add This Line
}
And let’s reset the animation of our player when the level changes. In engine/levels.js
:
static SquareIsland1(game) {
const entities = Maps.SquareIsland()
// todo: add enemies to the level
game.player.com('position').x = 32
game.player.com('position').y = 32
game.player.set(Characters.walk('down')) // Add This Line
entities.push(game.player)
return entities
}
static WhyIsland1(game) {
const entities = Maps.WhyIsland()
// todo: add enemies to the level
game.player.com('position').x = 13 * 32
game.player.com('position').y = 32
game.player.set(Characters.walk('down')) // Add This Line
entities.push(game.player)
return entities
}
Test in the browser that the game actually animates the character on direction change. Move the character left or right and then change the level and make sure the animation resets to down.
Enemies
The goal is to have our skeleton enemies bounce between the boundaries and each other. In order to do that
we need to record which direction the skeleton is moving so add this method to engine/components.js
:
static direction(value) {
return {
name: 'direction',
value: value
}
}
Next lets make our skeleton assembler. In engine/characters.js
:
static Skeleton(dir, x, y) {
const entity = Characters.entity(x, y, 'assets/skeleton.png')
entity.set(Components.type('enemy'))
entity.set(Components.direction(dir))
// set initial movement direction and animation
switch (dir) {
case 'left':
entity.set(Components.velocity(-1, 0))
entity.set(Characters.walk('left'))
break
case 'right':
entity.set(Components.velocity(1, 0))
entity.set(Characters.walk('right'))
break
case 'up':
entity.set(Components.velocity(0, -1))
entity.set(Characters.walk('up'))
break
case 'down':
entity.set(Components.velocity(0, 1))
entity.set(Characters.walk('down'))
break
}
entity.set(Components.collision(target => {
const redirect = target.is('wall') || target.is('enemy') || target.is('player')
if (!redirect) return false // no collision
// change direction and animation
switch (entity.com('direction').value) {
case 'left':
entity.set(Components.direction('right'))
entity.set(Components.velocity(1, 0))
entity.set(Characters.walk('right'))
break
case 'right':
entity.set(Components.direction('left'))
entity.set(Components.velocity(-1, 0))
entity.set(Characters.walk('left'))
break
case 'up':
entity.set(Components.direction('down'))
entity.set(Components.velocity(0, 1))
entity.set(Characters.walk('down'))
break
case 'down':
entity.set(Components.direction('up'))
entity.set(Components.velocity(0, -1))
entity.set(Characters.walk('up'))
break
}
return true // collided
}))
return entity
}
And finally, add them to our levels in engine/levels.js
:
static SquareIsland1(game) {
const entities = Maps.SquareIsland()
entities.push( // Add These Lines
Characters.Skeleton('down', 256, 32),
Characters.Skeleton('left', 32, 256)
)
game.player.com('position').x = 32
game.player.com('position').y = 32
game.player.set(Characters.walk('down'))
entities.push(game.player)
return entities
}
static WhyIsland1(game) {
const entities = Maps.WhyIsland()
entities.push( // Add These Lines
Characters.Skeleton('down', 32, 32),
Characters.Skeleton('down', 160, 224),
Characters.Skeleton('left', 128, 400),
)
game.player.com('position').x = 13 * 32
game.player.com('position').y = 32
game.player.set(Characters.walk('down'))
entities.push(game.player)
return entities
}
Refresh the game in the browser and run around blocking the skeletons:
Health Bars
To measure interactions between our player and the enemies we add health bars.
Create a new method in engine/components.js
:
static health(value) {
return {
name: 'health',
value: value
}
}
Give our characters health and adjust the renderer to create a visual bar in engine/characters.js
:
static entity(x = 0, y = 0, imgPath) {
// ...
entity.set(Components.velocity(0, 0))
entity.set(Components.health(100)) // Add This Line
entity.set(Components.renderer(context => {
const pos = entity.com('position')
const size = entity.com('size')
const image = assets.get(imgPath)
const animation = entity.com('animation')
const frame = animation.frames[animation.frame]
context.drawImage(image, frame.x, frame.y, size.w, size.h, pos.x, pos.y, size.w, size.h)
context.strokeStyle = 'darkred' // Add These Lines
context.fillStyle = 'red'
const width = (entity.com('health').value / 100) * 30
context.strokeRect(pos.x + 16, pos.y, 32, 5)
context.fillRect(pos.x + 17, pos.y + 1, width, 4)
}))
// ...
return entity
}
Refresh the game in the browser and we should see red bars floating above the characters’ heads:
Next Time
My next post on this project will include the following:
- Hit Detection
- Death Animations
- Audio Manager
- Music Key Binding
- Hit Audio
- Death Audio
- Game Over Conditions
- Game Over Screen
- Title Screen
- Options Menu
See you then!