folder names
12
6-space-game/5-keeping-score/.github/post-lecture-quiz.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
*Complete this quiz after the lesson by checking one answer per question.*
|
||||
|
||||
1. What's a fun way to show how many lifes a player has left
|
||||
|
||||
- [ ] a number of ships
|
||||
- [ ] a decimal number
|
||||
|
||||
2. How do you center text in the middle of the screen using the Canvas element
|
||||
|
||||
- [ ] You use Flexbox
|
||||
- [ ] You instruct the text to be drawn at the x coordinate of: the client window width/2
|
||||
- [ ] You set the `textAlign` property to the value `center` on the context object.
|
14
6-space-game/5-keeping-score/.github/pre-lecture-quiz.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
*A warm-up quiz about game development*
|
||||
|
||||
Complete this quiz in class
|
||||
|
||||
1. How do you draw text on a screen using the Canvas element?
|
||||
|
||||
- [ ] place text inside a div or span element
|
||||
- [ ] Call drawText() on the Canvas element
|
||||
- [ ] Call fillText() on the context object
|
||||
|
||||
2. Why do you have the concept of *lifes* in a game?
|
||||
|
||||
- [ ] to show how much damage you can take.
|
||||
- [ ] So that the game doesn't end straight away, but you have n number of chances before the game is over.
|
21
6-space-game/5-keeping-score/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 WebDev-For-Beginners
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
185
6-space-game/5-keeping-score/README.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Build a Space Game Part V: Scoring and Lives
|
||||
|
||||
## [Pre-lecture quiz](.github/pre-lecture-quiz.md)
|
||||
|
||||
In this lesson, you'll learn how to add scoring to a game and calculate lives.
|
||||
|
||||
## Draw text on the screen
|
||||
|
||||
To be able to display a game score on the screen, you'll need to know how to place text on the screen. The answer is using the `fillText()` method on the canvas object. You can also control other aspects like what font to use, the color of the text and even its alignment (left, right, center). Below is some code drawing some text on the screen.
|
||||
|
||||
```javascript
|
||||
ctx.font = "30px Arial";
|
||||
ctx.fillStyle = "red";
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText("show this on the screen", 0, 0);
|
||||
```
|
||||
|
||||
✅ Read more about [how to add text to a canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_text), and feel free to make yours look fancier!
|
||||
|
||||
## Life, as a game concept
|
||||
|
||||
The concept of having a life in a game is only a number. In the context of a space game it's common to assign a set of lives that get deducted one by one when your ship takes damage. It's nice if you can show a graphical representation of this like miniships or hearts instead of a number.
|
||||
|
||||
## What to build
|
||||
|
||||
Let's add the following to your game:
|
||||
|
||||
- **Game score**: For every enemy ship that is destroyed, the hero should be awarded some points, we suggest a 100 points per ship. The game score should be shown in the bottom left.
|
||||
- **Life**: Your ship has three lives. You lose a life every time an enemy ship collides with you. A life score should be displayed at the bottom right and be made out of the following graphic .
|
||||
|
||||
## Recommended steps
|
||||
|
||||
Locate the files that have been created for you in the `your-work` sub folder. It should contain the following:
|
||||
|
||||
```bash
|
||||
-| assets
|
||||
-| enemyShip.png
|
||||
-| player.png
|
||||
-| laserRed.png
|
||||
-| index.html
|
||||
-| app.js
|
||||
-| package.json
|
||||
```
|
||||
|
||||
You start your project the `your_work` folder by typing:
|
||||
|
||||
```bash
|
||||
cd your-work
|
||||
npm start
|
||||
```
|
||||
|
||||
The above will start a HTTP Server on address `http://localhost:5000`. Open up a browser and input that address, right now it should render the hero and all the enemies, and as you hit your left and right arrows, the hero moves and can shoot down enemies.
|
||||
|
||||
### Add code
|
||||
|
||||
1. **Copy over the needed assets** from the `solution/assets/` folder into `your-work` folder; you will add a `life.png` asset. Add the lifeImg to the window.onload function:
|
||||
|
||||
```javascript
|
||||
lifeImg = await loadTexture("assets/life.png");
|
||||
```
|
||||
|
||||
1. Add the `lifeImg` to the list of assets:
|
||||
|
||||
```javascript
|
||||
let heroImg,
|
||||
...
|
||||
lifeImg,
|
||||
...
|
||||
eventEmitter = new EventEmitter();
|
||||
```
|
||||
|
||||
2. **Add variables**. Add code that represents your total score (0) and lives left (3), display these scores on a screen.
|
||||
|
||||
3. **Extend `updateGameObjects()` function**. Extend the `updateGameObjects()` function to handle enemy collisions:
|
||||
|
||||
```javascript
|
||||
enemies.forEach(enemy => {
|
||||
const heroRect = hero.rectFromGameObject();
|
||||
if (intersectRect(heroRect, enemy.rectFromGameObject())) {
|
||||
eventEmitter.emit(Messages.COLLISION_ENEMY_HERO, { enemy });
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
4. **Add `life` and `points`**.
|
||||
1. **Initialize variables**. Under `this.cooldown = 0` in the `Hero` class, set life and points:
|
||||
|
||||
```javascript
|
||||
this.life = 3;
|
||||
this.points = 0;
|
||||
```
|
||||
|
||||
1. **Draw variables on screen**. Draw these values to screen:
|
||||
|
||||
```javascript
|
||||
function drawLife() {
|
||||
// TODO, 35, 27
|
||||
const START_POS = canvas.width - 180;
|
||||
for(let i=0; i < hero.life; i++ ) {
|
||||
ctx.drawImage(
|
||||
lifeImg,
|
||||
START_POS + (45 * (i+1) ),
|
||||
canvas.height - 37);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPoints() {
|
||||
ctx.font = "30px Arial";
|
||||
ctx.fillStyle = "red";
|
||||
ctx.textAlign = "left";
|
||||
drawText("Points: " + hero.points, 10, canvas.height-20);
|
||||
}
|
||||
|
||||
function drawText(message, x, y) {
|
||||
ctx.fillText(message, x, y);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
1. **Add methods to Game loop**. Make sure you add these functions to your window.onload function under `updateGameObjects()`:
|
||||
|
||||
```javascript
|
||||
drawPoints();
|
||||
drawLife();
|
||||
```
|
||||
|
||||
1. **Implement game rules**. Implement the following game rules:
|
||||
|
||||
1. **For every hero and enemy collision**, deduct a life.
|
||||
|
||||
Extend the `Hero` class to do this deduction:
|
||||
|
||||
```javascript
|
||||
decrementLife() {
|
||||
this.life--;
|
||||
if (this.life === 0) {
|
||||
this.dead = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **For every laser that hits an enemy**, increase game score with a 100 points.
|
||||
|
||||
Extend the Hero class to do this increment:
|
||||
|
||||
```javascript
|
||||
incrementPoints() {
|
||||
this.points += 100;
|
||||
}
|
||||
```
|
||||
|
||||
Add these functions to your Collision Event Emitters:
|
||||
|
||||
```javascript
|
||||
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
|
||||
first.dead = true;
|
||||
second.dead = true;
|
||||
hero.incrementPoints();
|
||||
})
|
||||
|
||||
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
|
||||
enemy.dead = true;
|
||||
hero.decrementLife();
|
||||
});
|
||||
```
|
||||
|
||||
✅ Do a little research to discover other games that are created using JavaScript/Canvas. What are their common traits?
|
||||
|
||||
By the end of this work, you should see the small 'life' ships at the bottom right, points at the bottom left, and you should see your life count decrement as you collide with enemies and your points increment when you shoot enemies. Well done! Your game is almost complete.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Challenge
|
||||
|
||||
Your code is almost complete. Can you envision your next steps?
|
||||
|
||||
## [Post-lecture quiz](.github/post-lecture-quiz.md)
|
||||
|
||||
## Review & Self Study
|
||||
|
||||
Research some ways that you can increment and decrement game scores and lives. There are some interesting game engines like [PlayFab](https://playfab.com). How could using one of these would enhance your game?
|
||||
|
||||
## Assignment
|
||||
|
||||
[Build a Scoring Game](assignment.md)
|
11
6-space-game/5-keeping-score/assignment.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Build a Scoring Game
|
||||
|
||||
## Instructions
|
||||
|
||||
Create a game where you display life and points in a creative way. A suggestion is to show the life as hearts and the points as a big number in the bottom center part of the screen. Have a look here for [Free game resources](https://www.kenney.nl/)
|
||||
|
||||
# Rubric
|
||||
|
||||
| Criteria | Exemplary | Adequate | Needs Improvement |
|
||||
| -------- | ---------------------- | --------------------------- | -------------------------- |
|
||||
| | full game is presented | game is partially presented | partial game contains bugs |
|
311
6-space-game/5-keeping-score/solution/app.js
Normal file
@@ -0,0 +1,311 @@
|
||||
// @ts-check
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
on(message, listener) {
|
||||
if (!this.listeners[message]) {
|
||||
this.listeners[message] = [];
|
||||
}
|
||||
this.listeners[message].push(listener);
|
||||
}
|
||||
|
||||
emit(message, payload = null) {
|
||||
if (this.listeners[message]) {
|
||||
this.listeners[message].forEach((l) => l(message, payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GameObject {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.dead = false;
|
||||
this.type = '';
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.img = undefined;
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
|
||||
}
|
||||
|
||||
rectFromGameObject() {
|
||||
return {
|
||||
top: this.y,
|
||||
left: this.x,
|
||||
bottom: this.y + this.height,
|
||||
right: this.x + this.width,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Hero extends GameObject {
|
||||
constructor(x, y) {
|
||||
super(x, y);
|
||||
(this.width = 99), (this.height = 75);
|
||||
this.type = 'Hero';
|
||||
this.speed = { x: 0, y: 0 };
|
||||
this.cooldown = 0;
|
||||
this.life = 3;
|
||||
this.points = 0;
|
||||
}
|
||||
fire() {
|
||||
gameObjects.push(new Laser(this.x + 45, this.y - 10));
|
||||
this.cooldown = 500;
|
||||
|
||||
let id = setInterval(() => {
|
||||
if (this.cooldown > 0) {
|
||||
this.cooldown -= 100;
|
||||
} else {
|
||||
clearInterval(id);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
canFire() {
|
||||
return this.cooldown === 0;
|
||||
}
|
||||
decrementLife() {
|
||||
this.life--;
|
||||
if (this.life === 0) {
|
||||
this.dead = true;
|
||||
}
|
||||
}
|
||||
incrementPoints() {
|
||||
this.points += 100;
|
||||
}
|
||||
}
|
||||
|
||||
class Enemy extends GameObject {
|
||||
constructor(x, y) {
|
||||
super(x, y);
|
||||
(this.width = 98), (this.height = 50);
|
||||
this.type = 'Enemy';
|
||||
let id = setInterval(() => {
|
||||
if (this.y < canvas.height - this.height) {
|
||||
this.y += 5;
|
||||
} else {
|
||||
console.log('Stopped at', this.y);
|
||||
clearInterval(id);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
class Laser extends GameObject {
|
||||
constructor(x, y) {
|
||||
super(x, y);
|
||||
(this.width = 9), (this.height = 33);
|
||||
this.type = 'Laser';
|
||||
this.img = laserImg;
|
||||
let id = setInterval(() => {
|
||||
if (this.y > 0) {
|
||||
this.y -= 15;
|
||||
} else {
|
||||
this.dead = true;
|
||||
clearInterval(id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function loadTexture(path) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = path;
|
||||
img.onload = () => {
|
||||
resolve(img);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function intersectRect(r1, r2) {
|
||||
return !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top);
|
||||
}
|
||||
|
||||
const Messages = {
|
||||
KEY_EVENT_UP: 'KEY_EVENT_UP',
|
||||
KEY_EVENT_DOWN: 'KEY_EVENT_DOWN',
|
||||
KEY_EVENT_LEFT: 'KEY_EVENT_LEFT',
|
||||
KEY_EVENT_RIGHT: 'KEY_EVENT_RIGHT',
|
||||
KEY_EVENT_SPACE: 'KEY_EVENT_SPACE',
|
||||
COLLISION_ENEMY_LASER: 'COLLISION_ENEMY_LASER',
|
||||
COLLISION_ENEMY_HERO: 'COLLISION_ENEMY_HERO',
|
||||
};
|
||||
|
||||
let heroImg,
|
||||
enemyImg,
|
||||
laserImg,
|
||||
lifeImg,
|
||||
canvas,
|
||||
ctx,
|
||||
gameObjects = [],
|
||||
hero,
|
||||
eventEmitter = new EventEmitter();
|
||||
|
||||
// EVENTS
|
||||
let onKeyDown = function (e) {
|
||||
// console.log(e.keyCode);
|
||||
switch (e.keyCode) {
|
||||
case 37:
|
||||
case 39:
|
||||
case 38:
|
||||
case 40: // Arrow keys
|
||||
case 32:
|
||||
e.preventDefault();
|
||||
break; // Space
|
||||
default:
|
||||
break; // do not block other keys
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
|
||||
// TODO make message driven
|
||||
window.addEventListener('keyup', (evt) => {
|
||||
if (evt.key === 'ArrowUp') {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_UP);
|
||||
} else if (evt.key === 'ArrowDown') {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_DOWN);
|
||||
} else if (evt.key === 'ArrowLeft') {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_LEFT);
|
||||
} else if (evt.key === 'ArrowRight') {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
|
||||
} else if (evt.keyCode === 32) {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_SPACE);
|
||||
}
|
||||
});
|
||||
|
||||
function createEnemies() {
|
||||
const MONSTER_TOTAL = 5;
|
||||
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
|
||||
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
|
||||
const STOP_X = START_X + MONSTER_WIDTH;
|
||||
|
||||
for (let x = START_X; x < STOP_X; x += 98) {
|
||||
for (let y = 0; y < 50 * 5; y += 50) {
|
||||
const enemy = new Enemy(x, y);
|
||||
enemy.img = enemyImg;
|
||||
gameObjects.push(enemy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createHero() {
|
||||
hero = new Hero(canvas.width / 2 - 45, canvas.height - canvas.height / 4);
|
||||
hero.img = heroImg;
|
||||
gameObjects.push(hero);
|
||||
}
|
||||
|
||||
function updateGameObjects() {
|
||||
const enemies = gameObjects.filter((go) => go.type === 'Enemy');
|
||||
const lasers = gameObjects.filter((go) => go.type === 'Laser');
|
||||
|
||||
enemies.forEach((enemy) => {
|
||||
const heroRect = hero.rectFromGameObject();
|
||||
if (intersectRect(heroRect, enemy.rectFromGameObject())) {
|
||||
eventEmitter.emit(Messages.COLLISION_ENEMY_HERO, { enemy });
|
||||
}
|
||||
});
|
||||
// laser hit something
|
||||
lasers.forEach((l) => {
|
||||
enemies.forEach((m) => {
|
||||
if (intersectRect(l.rectFromGameObject(), m.rectFromGameObject())) {
|
||||
eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, {
|
||||
first: l,
|
||||
second: m,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
gameObjects = gameObjects.filter((go) => !go.dead);
|
||||
}
|
||||
|
||||
function drawGameObjects(ctx) {
|
||||
gameObjects.forEach((go) => go.draw(ctx));
|
||||
}
|
||||
|
||||
function initGame() {
|
||||
gameObjects = [];
|
||||
createEnemies();
|
||||
createHero();
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_UP, () => {
|
||||
hero.y -= 5;
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
|
||||
hero.y += 5;
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
|
||||
hero.x -= 5;
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => {
|
||||
hero.x += 5;
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_SPACE, () => {
|
||||
if (hero.canFire()) {
|
||||
hero.fire();
|
||||
}
|
||||
// console.log('cant fire - cooling down')
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
|
||||
first.dead = true;
|
||||
second.dead = true;
|
||||
hero.incrementPoints();
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
|
||||
enemy.dead = true;
|
||||
hero.decrementLife();
|
||||
});
|
||||
}
|
||||
|
||||
function drawLife() {
|
||||
// TODO, 35, 27
|
||||
//
|
||||
|
||||
const START_POS = canvas.width - 180;
|
||||
for (let i = 0; i < hero.life; i++) {
|
||||
ctx.drawImage(lifeImg, START_POS + 45 * (i + 1), canvas.height - 37);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPoints() {
|
||||
ctx.font = '30px Arial';
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.textAlign = 'left';
|
||||
drawText('Points: ' + hero.points, 10, canvas.height - 20);
|
||||
}
|
||||
|
||||
function drawText(message, x, y) {
|
||||
ctx.fillText(message, x, y);
|
||||
}
|
||||
|
||||
window.onload = async () => {
|
||||
canvas = document.getElementById('canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
heroImg = await loadTexture('assets/player.png');
|
||||
enemyImg = await loadTexture('assets/enemyShip.png');
|
||||
laserImg = await loadTexture('assets/laserRed.png');
|
||||
lifeImg = await loadTexture('assets/life.png');
|
||||
|
||||
initGame();
|
||||
let gameLoopId = setInterval(() => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
drawPoints();
|
||||
drawLife();
|
||||
updateGameObjects();
|
||||
drawGameObjects(ctx);
|
||||
}, 100);
|
||||
};
|
BIN
6-space-game/5-keeping-score/solution/assets/enemyShip.png
Executable file
After Width: | Height: | Size: 4.1 KiB |
BIN
6-space-game/5-keeping-score/solution/assets/laserRed.png
Executable file
After Width: | Height: | Size: 1.1 KiB |
BIN
6-space-game/5-keeping-score/solution/assets/life.png
Executable file
After Width: | Height: | Size: 1.7 KiB |
BIN
6-space-game/5-keeping-score/solution/assets/player.png
Executable file
After Width: | Height: | Size: 4.3 KiB |
6
6-space-game/5-keeping-score/solution/index.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<html>
|
||||
<body>
|
||||
<canvas id ="canvas" width="1024" height="768"></canvas>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
13
6-space-game/5-keeping-score/solution/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "solution",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "npx http-server -c-1 -p 5000",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
182
6-space-game/5-keeping-score/translations/README.es.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Construye un juego espacial Parte V: Puntuación y vidas
|
||||
|
||||

|
||||
|
||||
## [Pre-lecture prueba](.github/pre-lecture-quiz.md)
|
||||
|
||||
En esta lección, aprenderá cómo agregar puntos a un juego y calcular vidas.
|
||||
|
||||
## Dibujar texto en la pantalla
|
||||
|
||||
Para poder mostrar la puntuación de un juego en la pantalla, necesitará saber cómo colocar texto en la pantalla. La respuesta es usar el método `fillText()` en el objeto de lienzo. También puedes controlar otros aspectos como qué tipo de letra usar, el color del texto e incluso su alineación (izquierda, derecha, centro). A continuación se muestra un código que dibuja un texto en la pantalla.
|
||||
|
||||
```javascript
|
||||
ctx.font = "30px Arial";
|
||||
ctx.fillStyle = "red";
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText("show this on the screen", 0, 0);
|
||||
```
|
||||
|
||||
✅ Lea más sobre [cómo agregar texto a un lienzo](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_text), ¡y siéntase libre de hacer que el suyo se vea más elegante!
|
||||
|
||||
## La vida, como concepto de juego
|
||||
|
||||
El concepto de tener una vida en un juego es solo un número. En el contexto de un juego espacial, es común asignar un conjunto de vidas que se deducen una por una cuando tu nave sufre daños. Es bueno si puede mostrar una representación gráfica de esto como miniships o corazones en lugar de un número.
|
||||
|
||||
## Qué construir
|
||||
|
||||
Agreguemos lo siguiente a tu juego:
|
||||
|
||||
- **Game score**** (Puntuación del juego): por cada barco enemigo que sea destruido, el héroe debería recibir algunos puntos, sugerimos 100 puntos por barco. La puntuación del juego debe mostrarse en la parte inferior izquierda.
|
||||
- **Life** (Vida): Tu nave tiene tres vidas. Pierdes una vida cada vez que un barco enemigo choca contigo. Se debe mostrar un puntaje de vida en la parte inferior derecha y estar compuesto por el siguiente gráfico: [life image](solution/assets/life.png)..
|
||||
|
||||
## Pasos recomendados
|
||||
|
||||
Busque los archivos que se han creado para usted en la subcarpeta `your-work`. Debe contener lo siguiente:
|
||||
|
||||
```bash
|
||||
-| assets
|
||||
-| enemyShip.png
|
||||
-| player.png
|
||||
-| laserRed.png
|
||||
-| index.html
|
||||
-| app.js
|
||||
-| package.json
|
||||
```
|
||||
|
||||
Comienzas tu proyecto en la carpeta `your_work` escribiendo:
|
||||
|
||||
```bash
|
||||
cd your-work
|
||||
npm start
|
||||
```
|
||||
|
||||
Lo anterior iniciará un servidor HTTP en la dirección `http://localhost:5000`. Abre un navegador e ingresa esa dirección, en este momento debería representar al héroe y a todos los enemigos, y cuando presionas las flechas izquierda y derecha, el héroe se mueve y puede derribar enemigos.
|
||||
|
||||
### Agregar código
|
||||
|
||||
1. **Copie los recursos necesarios** de la carpeta `solution/ assets/` a la carpeta `your-work`; agregará un activo `life.png`. Agregue el lifeImg a la función window.onload:
|
||||
|
||||
|
||||
```javascript
|
||||
lifeImg = await loadTexture("assets/life.png");
|
||||
```
|
||||
|
||||
1. Agregue el `lifeImg` a la lista de activos:
|
||||
|
||||
```javascript
|
||||
let heroImg,
|
||||
...
|
||||
lifeImg,
|
||||
...
|
||||
eventEmitter = new EventEmitter();
|
||||
```
|
||||
|
||||
2.**Agregar variables**. Agregue un código que represente su puntaje total (0) y las vidas restantes (3), muestre estos puntajes en una pantalla.
|
||||
|
||||
3. **Amplíe la función `updateGameObjects()`**. Amplíe la función `updateGameObjects()` para manejar las colisiones enemigas:
|
||||
|
||||
```javascript
|
||||
enemies.forEach(enemy => {
|
||||
const heroRect = hero.rectFromGameObject();
|
||||
if (intersectRect(heroRect, enemy.rectFromGameObject())) {
|
||||
eventEmitter.emit(Messages.COLLISION_ENEMY_HERO, { enemy });
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
4. **Agrega `life` y `points`**.
|
||||
1. **Inicializar variables**. En `this.cooldown = 0` en la clase `Hero`, establece la vida y los puntos:
|
||||
|
||||
```javascript
|
||||
this.life = 3;
|
||||
this.points = 0;
|
||||
```
|
||||
|
||||
1. **Dibujar variables en pantalla**. Dibuja estos valores en la pantalla:
|
||||
|
||||
```javascript
|
||||
function drawLife() {
|
||||
// TODO, 35, 27
|
||||
const START_POS = canvas.width - 180;
|
||||
for(let i=0; i < hero.life; i++ ) {
|
||||
ctx.drawImage(
|
||||
lifeImg,
|
||||
START_POS + (45 * (i+1) ),
|
||||
canvas.height - 37);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPoints() {
|
||||
ctx.font = "30px Arial";
|
||||
ctx.fillStyle = "red";
|
||||
ctx.textAlign = "left";
|
||||
drawText("Points: " + hero.points, 10, canvas.height-20);
|
||||
}
|
||||
|
||||
function drawText(message, x, y) {
|
||||
ctx.fillText(message, x, y);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
1. **Agregar métodos al bucle del juego**. Asegúrese de agregar estas funciones a su función window.onload en `updateGameObjects ()`:
|
||||
|
||||
```javascript
|
||||
drawPoints();
|
||||
drawLife();
|
||||
```
|
||||
|
||||
1. **Implementa las reglas del juego**. Implementa las siguientes reglas del juego:
|
||||
|
||||
1. **Por cada colisión entre héroes y enemigos**, resta una vida.
|
||||
|
||||
Extiende la clase `Hero` para hacer esta deducción:
|
||||
|
||||
```javascript
|
||||
decrementLife() {
|
||||
this.life--;
|
||||
if (this.life === 0) {
|
||||
this.dead = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Por cada láser que golpea a un enemigo**, aumenta la puntuación del juego con 100 puntos.
|
||||
|
||||
Extiende la clase Hero para hacer este incremento:
|
||||
|
||||
```javascript
|
||||
incrementPoints() {
|
||||
this.points += 100;
|
||||
}
|
||||
```
|
||||
|
||||
Agregue estas funciones a sus Collision Event Emitters:
|
||||
|
||||
```javascript
|
||||
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
|
||||
first.dead = true;
|
||||
second.dead = true;
|
||||
hero.incrementPoints();
|
||||
})
|
||||
|
||||
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
|
||||
enemy.dead = true;
|
||||
hero.decrementLife();
|
||||
});
|
||||
```
|
||||
|
||||
✅ Investigue un poco para descubrir otros juegos creados con JavaScript / Canvas. ¿Cuáles son sus rasgos comunes?
|
||||
|
||||
Al final de este trabajo, deberías ver las pequeñas naves de 'vida' en la parte inferior derecha, los puntos en la parte inferior izquierda, y deberías ver que tu cuenta de vidas disminuye cuando chocas con enemigos y tus puntos aumentan cuando disparas a los enemigos. ¡Bien hecho! Tu juego está casi completo.
|
||||
|
||||
🚀Challenge: Tu código está casi completo. ¿Puedes imaginar tus próximos pasos?
|
||||
|
||||
## [Post-lecture prueba](.github/post-lecture-quiz.md)
|
||||
|
||||
## Revisión y autoestudio
|
||||
|
||||
Investigue algunas formas en las que puede incrementar y disminuir las puntuaciones y vidas del juego. Hay algunos motores de juegos interesantes como [PlayFab](https://playfab.com). ¿Cómo podría mejorar tu juego el uso de uno de estos?
|
||||
|
||||
**Tarea**: [Crear un juego de puntuación](assignment.md)
|
11
6-space-game/5-keeping-score/translations/assignment.es.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Construye un juego de puntuación
|
||||
|
||||
## Instrucciones
|
||||
|
||||
Crea un juego en el que muestres la vida y los puntos de forma creativa. Una sugerencia es mostrar la vida como corazones y los puntos como un gran número en la parte inferior central de la pantalla. Eche un vistazo aquí para ver [Recursos de juegos gratuitos] (https://www.kenney.nl/)
|
||||
|
||||
# Rúbrica
|
||||
|
||||
| Criterios | Ejemplar | Adecuado | Necesita mejorar |
|
||||
| -------- | ---------------------- | --------------------------- | -------------------------- |
|
||||
| | se presenta el juego completo | se presenta parcialmente el juego | juego parcial contiene errores |
|
261
6-space-game/5-keeping-score/your-work/app.js
Normal file
@@ -0,0 +1,261 @@
|
||||
// @ts-check
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
on(message, listener) {
|
||||
if (!this.listeners[message]) {
|
||||
this.listeners[message] = [];
|
||||
}
|
||||
this.listeners[message].push(listener);
|
||||
}
|
||||
|
||||
emit(message, payload = null) {
|
||||
if (this.listeners[message]) {
|
||||
this.listeners[message].forEach((l) => l(message, payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GameObject {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.dead = false;
|
||||
this.type = '';
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.img = undefined;
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
|
||||
}
|
||||
|
||||
rectFromGameObject() {
|
||||
return {
|
||||
top: this.y,
|
||||
left: this.x,
|
||||
bottom: this.y + this.height,
|
||||
right: this.x + this.width,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Hero extends GameObject {
|
||||
constructor(x, y) {
|
||||
super(x, y);
|
||||
(this.width = 99), (this.height = 75);
|
||||
this.type = 'Hero';
|
||||
this.speed = { x: 0, y: 0 };
|
||||
this.cooldown = 0;
|
||||
}
|
||||
fire() {
|
||||
gameObjects.push(new Laser(this.x + 45, this.y - 10));
|
||||
this.cooldown = 500;
|
||||
|
||||
let id = setInterval(() => {
|
||||
if (this.cooldown > 0) {
|
||||
this.cooldown -= 100;
|
||||
} else {
|
||||
clearInterval(id);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
canFire() {
|
||||
return this.cooldown === 0;
|
||||
}
|
||||
}
|
||||
|
||||
class Enemy extends GameObject {
|
||||
constructor(x, y) {
|
||||
super(x, y);
|
||||
(this.width = 98), (this.height = 50);
|
||||
this.type = 'Enemy';
|
||||
let id = setInterval(() => {
|
||||
if (this.y < canvas.height - this.height) {
|
||||
this.y += 5;
|
||||
} else {
|
||||
console.log('Stopped at', this.y);
|
||||
clearInterval(id);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
class Laser extends GameObject {
|
||||
constructor(x, y) {
|
||||
super(x, y);
|
||||
(this.width = 9), (this.height = 33);
|
||||
this.type = 'Laser';
|
||||
this.img = laserImg;
|
||||
let id = setInterval(() => {
|
||||
if (this.y > 0) {
|
||||
this.y -= 15;
|
||||
} else {
|
||||
this.dead = true;
|
||||
clearInterval(id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function loadTexture(path) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = path;
|
||||
img.onload = () => {
|
||||
resolve(img);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function intersectRect(r1, r2) {
|
||||
return !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top);
|
||||
}
|
||||
|
||||
const Messages = {
|
||||
KEY_EVENT_UP: 'KEY_EVENT_UP',
|
||||
KEY_EVENT_DOWN: 'KEY_EVENT_DOWN',
|
||||
KEY_EVENT_LEFT: 'KEY_EVENT_LEFT',
|
||||
KEY_EVENT_RIGHT: 'KEY_EVENT_RIGHT',
|
||||
KEY_EVENT_SPACE: 'KEY_EVENT_SPACE',
|
||||
COLLISION_ENEMY_LASER: 'COLLISION_ENEMY_LASER',
|
||||
};
|
||||
|
||||
let heroImg,
|
||||
enemyImg,
|
||||
laserImg,
|
||||
canvas,
|
||||
ctx,
|
||||
gameObjects = [],
|
||||
hero,
|
||||
eventEmitter = new EventEmitter();
|
||||
|
||||
// EVENTS
|
||||
let onKeyDown = function (e) {
|
||||
// console.log(e.keyCode);
|
||||
switch (e.keyCode) {
|
||||
case 37:
|
||||
case 39:
|
||||
case 38:
|
||||
case 40: // Arrow keys
|
||||
case 32:
|
||||
e.preventDefault();
|
||||
break; // Space
|
||||
default:
|
||||
break; // do not block other keys
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
|
||||
// TODO make message driven
|
||||
window.addEventListener('keyup', (evt) => {
|
||||
if (evt.key === 'ArrowUp') {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_UP);
|
||||
} else if (evt.key === 'ArrowDown') {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_DOWN);
|
||||
} else if (evt.key === 'ArrowLeft') {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_LEFT);
|
||||
} else if (evt.key === 'ArrowRight') {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
|
||||
} else if (evt.keyCode === 32) {
|
||||
eventEmitter.emit(Messages.KEY_EVENT_SPACE);
|
||||
}
|
||||
});
|
||||
|
||||
function createEnemies() {
|
||||
const MONSTER_TOTAL = 5;
|
||||
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
|
||||
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
|
||||
const STOP_X = START_X + MONSTER_WIDTH;
|
||||
|
||||
for (let x = START_X; x < STOP_X; x += 98) {
|
||||
for (let y = 0; y < 50 * 5; y += 50) {
|
||||
const enemy = new Enemy(x, y);
|
||||
enemy.img = enemyImg;
|
||||
gameObjects.push(enemy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createHero() {
|
||||
hero = new Hero(canvas.width / 2 - 45, canvas.height - canvas.height / 4);
|
||||
hero.img = heroImg;
|
||||
gameObjects.push(hero);
|
||||
}
|
||||
|
||||
function updateGameObjects() {
|
||||
const enemies = gameObjects.filter((go) => go.type === 'Enemy');
|
||||
const lasers = gameObjects.filter((go) => go.type === 'Laser');
|
||||
// laser hit something
|
||||
lasers.forEach((l) => {
|
||||
enemies.forEach((m) => {
|
||||
if (intersectRect(l.rectFromGameObject(), m.rectFromGameObject())) {
|
||||
eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, {
|
||||
first: l,
|
||||
second: m,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
gameObjects = gameObjects.filter((go) => !go.dead);
|
||||
}
|
||||
|
||||
function drawGameObjects(ctx) {
|
||||
gameObjects.forEach((go) => go.draw(ctx));
|
||||
}
|
||||
|
||||
function initGame() {
|
||||
gameObjects = [];
|
||||
createEnemies();
|
||||
createHero();
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_UP, () => {
|
||||
hero.y -= 5;
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
|
||||
hero.y += 5;
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
|
||||
hero.x -= 5;
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => {
|
||||
hero.x += 5;
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.KEY_EVENT_SPACE, () => {
|
||||
if (hero.canFire()) {
|
||||
hero.fire();
|
||||
}
|
||||
// console.log('cant fire - cooling down')
|
||||
});
|
||||
|
||||
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
|
||||
first.dead = true;
|
||||
second.dead = true;
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = async () => {
|
||||
canvas = document.getElementById('canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
heroImg = await loadTexture('assets/player.png');
|
||||
enemyImg = await loadTexture('assets/enemyShip.png');
|
||||
laserImg = await loadTexture('assets/laserRed.png');
|
||||
|
||||
initGame();
|
||||
let gameLoopId = setInterval(() => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
updateGameObjects();
|
||||
drawGameObjects(ctx);
|
||||
}, 100);
|
||||
};
|
BIN
6-space-game/5-keeping-score/your-work/assets/enemyShip.png
Executable file
After Width: | Height: | Size: 4.1 KiB |
BIN
6-space-game/5-keeping-score/your-work/assets/laserRed.png
Executable file
After Width: | Height: | Size: 1.1 KiB |
BIN
6-space-game/5-keeping-score/your-work/assets/life.png
Executable file
After Width: | Height: | Size: 1.7 KiB |
BIN
6-space-game/5-keeping-score/your-work/assets/player.png
Executable file
After Width: | Height: | Size: 4.3 KiB |
6
6-space-game/5-keeping-score/your-work/index.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<html>
|
||||
<body>
|
||||
<canvas id ="canvas" width="1024" height="768"></canvas>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
13
6-space-game/5-keeping-score/your-work/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "solution",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "npx http-server -c-1 -p 5000",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|