├── Bullet.js ├── BulletController.js ├── Enemy.js ├── EnemyController.js ├── MovingDirection.js ├── Player.js ├── README.md ├── cover.png ├── images ├── enemy1.png ├── enemy2.png ├── enemy3.png ├── player.png └── space.png ├── index.html ├── index.js └── sounds ├── enemy-death.wav └── shoot.wav /Bullet.js: -------------------------------------------------------------------------------- 1 | export default class Bullet { 2 | constructor(canvas, x, y, velocity, bulletColor) { 3 | this.canvas = canvas; 4 | this.x = x; 5 | this.y = y; 6 | this.velocity = velocity; 7 | this.bulletColor = bulletColor; 8 | 9 | this.width = 5; 10 | this.height = 20; 11 | } 12 | 13 | draw(ctx) { 14 | this.y -= this.velocity; 15 | ctx.fillStyle = this.bulletColor; 16 | ctx.fillRect(this.x, this.y, this.width, this.height); 17 | } 18 | 19 | collideWith(sprite) { 20 | if ( 21 | this.x + this.width > sprite.x && 22 | this.x < sprite.x + sprite.width && 23 | this.y + this.height > sprite.y && 24 | this.y < sprite.y + sprite.height 25 | ) { 26 | return true; 27 | } else { 28 | return false; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BulletController.js: -------------------------------------------------------------------------------- 1 | import Bullet from "./Bullet.js"; 2 | 3 | export default class BulletController { 4 | bullets = []; 5 | timeTillNextBulletAllowed = 0; 6 | 7 | constructor(canvas, maxBulletsAtATime, bulletColor, soundEnabled) { 8 | this.canvas = canvas; 9 | this.maxBulletsAtATime = maxBulletsAtATime; 10 | this.bulletColor = bulletColor; 11 | this.soundEnabled = soundEnabled; 12 | 13 | this.shootSound = new Audio("sounds/shoot.wav"); 14 | this.shootSound.volume = 0.1; 15 | } 16 | 17 | draw(ctx) { 18 | this.bullets = this.bullets.filter( 19 | (bullet) => bullet.y + bullet.width > 0 && bullet.y <= this.canvas.height 20 | ); 21 | 22 | this.bullets.forEach((bullet) => bullet.draw(ctx)); 23 | if (this.timeTillNextBulletAllowed > 0) { 24 | this.timeTillNextBulletAllowed--; 25 | } 26 | } 27 | 28 | collideWith(sprite) { 29 | const bulletThatHitSpriteIndex = this.bullets.findIndex((bullet) => 30 | bullet.collideWith(sprite) 31 | ); 32 | 33 | if (bulletThatHitSpriteIndex >= 0) { 34 | this.bullets.splice(bulletThatHitSpriteIndex, 1); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | shoot(x, y, velocity, timeTillNextBulletAllowed = 0) { 42 | if ( 43 | this.timeTillNextBulletAllowed <= 0 && 44 | this.bullets.length < this.maxBulletsAtATime 45 | ) { 46 | const bullet = new Bullet(this.canvas, x, y, velocity, this.bulletColor); 47 | this.bullets.push(bullet); 48 | if (this.soundEnabled) { 49 | this.shootSound.currentTime = 0; 50 | this.shootSound.play(); 51 | } 52 | this.timeTillNextBulletAllowed = timeTillNextBulletAllowed; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Enemy.js: -------------------------------------------------------------------------------- 1 | export default class Enemy { 2 | constructor(x, y, imageNumber) { 3 | this.x = x; 4 | this.y = y; 5 | this.width = 44; 6 | this.height = 32; 7 | 8 | this.image = new Image(); 9 | this.image.src = `images/enemy${imageNumber}.png`; 10 | } 11 | 12 | draw(ctx) { 13 | ctx.drawImage(this.image, this.x, this.y, this.width, this.height); 14 | } 15 | 16 | move(xVelocity, yVelocity) { 17 | this.x += xVelocity; 18 | this.y += yVelocity; 19 | } 20 | 21 | collideWith(sprite) { 22 | if ( 23 | this.x + this.width > sprite.x && 24 | this.x < sprite.x + sprite.width && 25 | this.y + this.height > sprite.y && 26 | this.y < sprite.y + sprite.height 27 | ) { 28 | return true; 29 | } else { 30 | return false; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /EnemyController.js: -------------------------------------------------------------------------------- 1 | import Enemy from "./Enemy.js"; 2 | import MovingDirection from "./MovingDirection.js"; 3 | 4 | export default class EnemyController { 5 | enemyMap = [ 6 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 7 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 8 | [2, 2, 2, 3, 3, 3, 3, 2, 2, 2], 9 | [2, 2, 2, 3, 3, 3, 3, 2, 2, 2], 10 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 11 | [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], 12 | ]; 13 | enemyRows = []; 14 | 15 | currentDirection = MovingDirection.right; 16 | xVelocity = 0; 17 | yVelocity = 0; 18 | defaultXVelocity = 1; 19 | defaultYVelocity = 1; 20 | moveDownTimerDefault = 30; 21 | moveDownTimer = this.moveDownTimerDefault; 22 | fireBulletTimerDefault = 100; 23 | fireBulletTimer = this.fireBulletTimerDefault; 24 | 25 | constructor(canvas, enemyBulletController, playerBulletController) { 26 | this.canvas = canvas; 27 | this.enemyBulletController = enemyBulletController; 28 | this.playerBulletController = playerBulletController; 29 | 30 | this.enemyDeathSound = new Audio("sounds/enemy-death.wav"); 31 | this.enemyDeathSound.volume = 0.1; 32 | 33 | this.createEnemies(); 34 | } 35 | 36 | draw(ctx) { 37 | this.decrementMoveDownTimer(); 38 | this.updateVelocityAndDirection(); 39 | this.collisionDetection(); 40 | this.drawEnemies(ctx); 41 | this.resetMoveDownTimer(); 42 | this.fireBullet(); 43 | } 44 | 45 | collisionDetection() { 46 | this.enemyRows.forEach((enemyRow) => { 47 | enemyRow.forEach((enemy, enemyIndex) => { 48 | if (this.playerBulletController.collideWith(enemy)) { 49 | this.enemyDeathSound.currentTime = 0; 50 | this.enemyDeathSound.play(); 51 | enemyRow.splice(enemyIndex, 1); 52 | } 53 | }); 54 | }); 55 | 56 | this.enemyRows = this.enemyRows.filter((enemyRow) => enemyRow.length > 0); 57 | } 58 | 59 | fireBullet() { 60 | this.fireBulletTimer--; 61 | if (this.fireBulletTimer <= 0) { 62 | this.fireBulletTimer = this.fireBulletTimerDefault; 63 | const allEnemies = this.enemyRows.flat(); 64 | const enemyIndex = Math.floor(Math.random() * allEnemies.length); 65 | const enemy = allEnemies[enemyIndex]; 66 | this.enemyBulletController.shoot(enemy.x + enemy.width / 2, enemy.y, -3); 67 | } 68 | } 69 | 70 | resetMoveDownTimer() { 71 | if (this.moveDownTimer <= 0) { 72 | this.moveDownTimer = this.moveDownTimerDefault; 73 | } 74 | } 75 | 76 | decrementMoveDownTimer() { 77 | if ( 78 | this.currentDirection === MovingDirection.downLeft || 79 | this.currentDirection === MovingDirection.downRight 80 | ) { 81 | this.moveDownTimer--; 82 | } 83 | } 84 | 85 | updateVelocityAndDirection() { 86 | for (const enemyRow of this.enemyRows) { 87 | if (this.currentDirection == MovingDirection.right) { 88 | this.xVelocity = this.defaultXVelocity; 89 | this.yVelocity = 0; 90 | const rightMostEnemy = enemyRow[enemyRow.length - 1]; 91 | if (rightMostEnemy.x + rightMostEnemy.width >= this.canvas.width) { 92 | this.currentDirection = MovingDirection.downLeft; 93 | break; 94 | } 95 | } else if (this.currentDirection === MovingDirection.downLeft) { 96 | if (this.moveDown(MovingDirection.left)) { 97 | break; 98 | } 99 | } else if (this.currentDirection === MovingDirection.left) { 100 | this.xVelocity = -this.defaultXVelocity; 101 | this.yVelocity = 0; 102 | const leftMostEnemy = enemyRow[0]; 103 | if (leftMostEnemy.x <= 0) { 104 | this.currentDirection = MovingDirection.downRight; 105 | break; 106 | } 107 | } else if (this.currentDirection === MovingDirection.downRight) { 108 | if (this.moveDown(MovingDirection.right)) { 109 | break; 110 | } 111 | } 112 | } 113 | } 114 | 115 | moveDown(newDirection) { 116 | this.xVelocity = 0; 117 | this.yVelocity = this.defaultYVelocity; 118 | if (this.moveDownTimer <= 0) { 119 | this.currentDirection = newDirection; 120 | return true; 121 | } 122 | return false; 123 | } 124 | 125 | drawEnemies(ctx) { 126 | this.enemyRows.flat().forEach((enemy) => { 127 | enemy.move(this.xVelocity, this.yVelocity); 128 | enemy.draw(ctx); 129 | }); 130 | } 131 | 132 | happy = () => {}; 133 | 134 | createEnemies() { 135 | this.enemyMap.forEach((row, rowIndex) => { 136 | this.enemyRows[rowIndex] = []; 137 | row.forEach((enemyNubmer, enemyIndex) => { 138 | if (enemyNubmer > 0) { 139 | this.enemyRows[rowIndex].push( 140 | new Enemy(enemyIndex * 50, rowIndex * 35, enemyNubmer) 141 | ); 142 | } 143 | }); 144 | }); 145 | } 146 | 147 | collideWith(sprite) { 148 | return this.enemyRows.flat().some((enemy) => enemy.collideWith(sprite)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /MovingDirection.js: -------------------------------------------------------------------------------- 1 | const MovingDirection = { 2 | left: 0, 3 | right: 1, 4 | downLeft: 2, 5 | downRight: 3, 6 | }; 7 | 8 | export default MovingDirection; 9 | -------------------------------------------------------------------------------- /Player.js: -------------------------------------------------------------------------------- 1 | export default class Player { 2 | rightPressed = false; 3 | leftPressed = false; 4 | shootPressed = false; 5 | 6 | constructor(canvas, velocity, bulletController) { 7 | this.canvas = canvas; 8 | this.velocity = velocity; 9 | this.bulletController = bulletController; 10 | 11 | this.x = this.canvas.width / 2; 12 | this.y = this.canvas.height - 75; 13 | this.width = 50; 14 | this.height = 48; 15 | this.image = new Image(); 16 | this.image.src = "images/player.png"; 17 | 18 | document.addEventListener("keydown", this.keydown); 19 | document.addEventListener("keyup", this.keyup); 20 | } 21 | 22 | draw(ctx) { 23 | if (this.shootPressed) { 24 | this.bulletController.shoot(this.x + this.width / 2, this.y, 4, 10); 25 | } 26 | this.move(); 27 | this.collideWithWalls(); 28 | ctx.drawImage(this.image, this.x, this.y, this.width, this.height); 29 | } 30 | 31 | collideWithWalls() { 32 | //left 33 | if (this.x < 0) { 34 | this.x = 0; 35 | } 36 | 37 | //right 38 | if (this.x > this.canvas.width - this.width) { 39 | this.x = this.canvas.width - this.width; 40 | } 41 | } 42 | 43 | move() { 44 | if (this.rightPressed) { 45 | this.x += this.velocity; 46 | } else if (this.leftPressed) { 47 | this.x += -this.velocity; 48 | } 49 | } 50 | 51 | keydown = (event) => { 52 | if (event.code == "ArrowRight") { 53 | this.rightPressed = true; 54 | } 55 | if (event.code == "ArrowLeft") { 56 | this.leftPressed = true; 57 | } 58 | if (event.code == "Space") { 59 | this.shootPressed = true; 60 | } 61 | }; 62 | 63 | keyup = (event) => { 64 | if (event.code == "ArrowRight") { 65 | this.rightPressed = false; 66 | } 67 | if (event.code == "ArrowLeft") { 68 | this.leftPressed = false; 69 | } 70 | if (event.code == "Space") { 71 | this.shootPressed = false; 72 | } 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Space Invaders - Game Dev 2 | 3 |  4 | 5 | YouTube Link: https://youtu.be/qCBiKJbLcFI 6 | 7 | In this exciting video we are going to make the classic game space invaders with JavaScript on a HTML canvas. Best of all we will code everything from scratch starting with an empty project. 8 | 9 | In this classic game we have our enemies at the top of the screen; they move side to side and down towards our player. At random the enemies will shoot bullets. At the bottom of the screen we have our spaceship which can shoot at the enemies. Unlike the original game our bullets shoot much faster, which also makes the game much more fun. The objective is to stay alive, avoid the enemies bullets and eliminate the enemies before they reach the bottom of the screen. 10 | 11 | ## We will cover the following topics and more: 12 | 13 | - Game loop 14 | - Keyboard input 15 | - Moving our enemies 16 | - Collision detection 17 | - Shooting bullets 18 | - Game audio 19 | 20 | If you enjoy this tutorial please subscribe, like and share on YouTube. 21 | 22 | Try it here 23 | https://codingwith-adam.github.io/space-invaders/index.html 24 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingWith-Adam/space-invaders/effc40688f10f33fbc1714e8844758a264cf4d18/cover.png -------------------------------------------------------------------------------- /images/enemy1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingWith-Adam/space-invaders/effc40688f10f33fbc1714e8844758a264cf4d18/images/enemy1.png -------------------------------------------------------------------------------- /images/enemy2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingWith-Adam/space-invaders/effc40688f10f33fbc1714e8844758a264cf4d18/images/enemy2.png -------------------------------------------------------------------------------- /images/enemy3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingWith-Adam/space-invaders/effc40688f10f33fbc1714e8844758a264cf4d18/images/enemy3.png -------------------------------------------------------------------------------- /images/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingWith-Adam/space-invaders/effc40688f10f33fbc1714e8844758a264cf4d18/images/player.png -------------------------------------------------------------------------------- /images/space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingWith-Adam/space-invaders/effc40688f10f33fbc1714e8844758a264cf4d18/images/space.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |