Minuto 3:48
Introducción
Programemos un juego con JavaScript
Canvas
¿Qué es canvas en JavaScript?
Tamaño del canvas y sus elementos
Canvas responsive
Mapa del juego
¿Qué es un array bidimensional?
Arreglos multidimensionales en JavaScript
Refactor del mapa de juego
Movimientos del jugador
Eventos y botones
Objeto playerPosition
Limpieza de movimientos
No te salgas del mapa
Colisiones
Detectando colisiones fijas
Detectando colisiones con arrays
Victoria: subiendo de nivel
Derrota: perdiendo vidas
Bonus: adictividad
Sistema de vidas y corazones
Sistema de tiempo y puntajes
¿Qué es localStorage?
Guardando records del jugador
Deploy
Depurando errores del juego
Desplegando el juego a GitHub Pages
Próximos pasos
Reto: reinicio del juego
Reto: timeouts de victoria o derrota
¿Quieres un simulador laboral?
No tienes acceso a esta clase
¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera
Juan David Castro Gallego
Aportes 22
Preguntas 3
Minuto 3:48
Le construí un format para que se lea mejor 😉
también decidí agregarle un timer al contiue no mas para que vea mas bonito
//contect
const canvas = document.querySelector('#game');
const btnArriba = document.querySelector('#arriba');
const btnAbajo = document.querySelector('#abajo');
const btnDer = document.querySelector('#derecha');
const btnIzq = document.querySelector('#izquierda');
const viewlive = document.querySelector('#lives');
const viewTime = document.querySelector('#time');
const juego = canvas.getContext('2d');
//signals:
window.addEventListener('load', loadEvent);
window.addEventListener('resize', resizeEvent);
let posJugador = undefined; let mapa = Array(); let nivel = 0;
let prePosJugador = undefined; let celda_t = 0; let nivel_actual = undefined;
let puntoDePartida = Number(0); let canvas_t = 0; var actualizado = Boolean(false);
let explociones = Array(); let vidas = 3; let timeStart = undefined;
let timeInterval = undefined;
let playerTime = 0;
const re_iniciar = {'select': 'si', 'resetStart': 'indeciso','exec' : false};
function loadEvent(){
viewTime.innerHTML = '0:0:0';
timeEvent();
resizeEvent();}
function resizeEvent(){
actualizado = false;
canvas_t = (window.innerWidth < window.innerHeight ? window.innerWidth : window.innerHeight) *.75 ;
canvas.setAttribute('width', canvas_t);
canvas.setAttribute('height',canvas_t);
celda_t = canvas_t / 10;
juego.textAlign='end';
juego.font = font_t() +'px arial';
liveEvent();
update();}
function font_t(){ return celda_t - celda_t / 10;}
function liveEvent(){
const corazon = Array(vidas).fill(emojis['HEART']);
viewlive.innerHTML = corazon.join('');}
function timeFormat(time_msec){
const time = ~~(time_msec /1000);
const min = (time / 60) | 0;
const sec = time - (min * 60);
const msec = ((time_msec / 10) | 0) - (time * 100);
return min +':'+ ((sec < 10 ? '0' : 0) + sec) + ':' + ((msec < 10 ? '0' : 0) + msec);}
function paintTimeEvent(){
playerTime = Date.now() - timeStart;
viewTime.innerHTML = timeFormat(playerTime);}
function timeEvent(var_function = paintTimeEvent){
if(!timeInterval){
timeStart = Date.now();
timeInterval = setInterval(var_function, 100);}
else{
clearInterval(timeInterval);
timeStart = undefined;
timeInterval = undefined;}}
function resetStartEvent(){
posJugador = prePosJugador = undefined;
nivel = 0;
nivel_actual = undefined;
puntoDePartida = 0;
vidas = 3;
actualizado = false;
explociones = Array();
re_iniciar['select'] = 'si';
re_iniciar['exec'] = false;
re_iniciar['resetStart'] = 'indeciso';
timeEvent();
liveEvent();
playerTime = 0;
update();}
function update(){
if(re_iniciar['exec']){paintScreenFinal(); return;}
cargarMapa();
clear();
paintEvent();
paintEventPlayer();
paintEventExplosion();
actualizado = true;}
function cargarMapa(){
if(nivel_actual == nivel || Victoria()) return;
nivel_actual = nivel;
mapa = map[nivel].match(/[IOX-]/g);}
function clear(){
if(!actualizado) {
juego.clearRect(0,0, canvas_t, canvas_t);
return;}
if(posJugador == prePosJugador) return;
clearRect(posJugador);
clearRect(prePosJugador);}
function clearRect(indice){
const x = posX(indice ) - celda_t / 10;
const y = posY(indice ) + celda_t / 5;
juego.clearRect(x - 1 ,y , - celda_t, - (celda_t + 1));}
function posY(indice){ return (~~(indice/10) + 1) * celda_t - celda_t / 5; }
function posX(indice){ return (indice - (~~(indice/10) * 10) + 1) * celda_t + celda_t / 10;}
function paintEvent(){
if(!actualizado)
mapa.forEach((char, idx) => {
if(char == 'O' && posJugador == undefined) puntoDePartida = posJugador = idx;
juego.fillText(emojis[char], posX(idx), posY(idx) );});}
function paintEventPlayer(){
if(victoryEvent()) paintScreenEvent();
else {
if(posJugador != undefined && prePosJugador != posJugador){
juego.fillText(emojis[mapa[prePosJugador]],posX(prePosJugador),posY(prePosJugador));
if(!gameOverEvent()) juego.fillText(emojis['PLAYER'], posX(posJugador),posY(posJugador));
else paintScreenEvent();}}}
function gameOverEvent(){
if(vidas && mapa[posJugador] == 'X'){
explociones.push(posJugador);
--vidas;
liveEvent();
juego.fillText(emojis['BOMB_COLLISION'],posX(posJugador),posY(posJugador));
posJugador = puntoDePartida;}
if(gameFinishEvent()) {
explociones = Array();
return true;} /*juego terminado*/
return false; /*continuar con el juego*/}
function paintEventExplosion(){
if(!explociones.length) return;
explociones.forEach((pos) =>{
clearRect(pos);
juego.fillText(emojis['BOMB_COLLISION'],posX(pos),posY(pos));});}
function victoryEvent(){
if(mapa[posJugador] != 'I') return false;
else if(nivel < map.length) ++nivel;
explociones = Array();
if(gameFinishEvent()) return true;/*juego terminado*/
posJugador = prePosJugador = undefined;
actualizado = false;
update();
return false;/*continuar con el juego*/}
function Victoria(){return nivel >= map.length;}
function gameFinishEvent(){
if(!(nivel >= map.length) == (vidas <= 0) && !re_iniciar['exec']) {
timeEvent();
re_iniciar['exec'] = true;}
return !(nivel >= map.length) == (vidas <= 0);}
function paintScreenEvent(){
if(!re_iniciar['exec']) return;
paintScreen(Victoria() ? 'WIN': 'GAME_OVER');
paintScreenFinal();}
function paintScreenFinal(){
if(!re_iniciar['exec']) return;
if(!actualizado) paintScreen(Victoria() ? 'WIN': 'GAME_OVER');
if(!timeInterval && !Victoria() && re_iniciar['select'] != 'exit') {
timeEvent(paintScreenFinal);}
const media = canvas_t / 2;
const media_alta = media - font_t() + 1;
const media_baja = media + font_t() - 1;
let color, backgroundColor, text = 'continuar', y = media_alta;
if(Victoria()){ color = '#ffd700';
backgroundColor = '#01433A';}
else { color = '#FF2F22';
backgroundColor = '#211A27';}
if(re_iniciar['resetStart'] == 'no' || re_iniciar['select'] == 'exit'){
if(actualizado)paintScreen(Victoria() ? 'WIN': 'GAME_OVER');
if(timeStart) timeEvent();
text = Victoria() ? 'Has Ganado' : 'Game Over';
y = media + (font_t() / 2);
re_iniciar['select'] = 'exit';}
else if(re_iniciar['resetStart'] == 'si') {
timeEvent();
resetStartEvent();
return;}
else{
if(timeStart){
const contador = 10 - (((Date.now() - timeStart) / 1000) | 0);
text = text + ' ' + contador;
if(contador < 0){
timeEvent();
re_iniciar['resetStart'] = 'no';
update();
return;}}
paintScreen(Victoria() ? 'WIN': 'GAME_OVER');
paintRectText('', '' , backgroundColor,'center',media, media);
paintRectText('', '' , backgroundColor,'center',media, media_baja);
let siCa = color, siCb = backgroundColor;
let noCa = color, noCb = backgroundColor;
if(re_iniciar['select'] == 'no'){ noCa = backgroundColor; noCb = color;}
else { siCa = backgroundColor; siCb = color;}
paintRectText('no', noCa, noCb,'center',media + font_t(), media + (font_t() / 2));
paintRectText('si', siCa, siCb,'center',media - font_t(), media + (font_t() / 2));}
paintRectText('', '' , backgroundColor,'center',media, y);
paintRectText(text, color , '','center',media, y);}
function paintScreen(emoji){
juego.clearRect(0,0, canvas_t, canvas_t);
mapa.forEach((char, idx) =>{
if(char == 'X') char = emoji;
juego.fillText(emojis[char],posX(idx),posY(idx));});}
function paintRectText(txt, color = '', backgroundColor = '', aling = 'center', x = 0,y = 0){
if(aling) juego.textAlign = aling;
if(backgroundColor){
const rect = rectBgText(txt, x, y, aling);
juego.fillStyle = backgroundColor;
juego.fillRect(rect['x'], rect['y'], rect['width'], rect['height']);}
if(color) juego.fillStyle = color;
if(txt) juego.fillText(txt, x, y);
juego.textAlign = 'end';}
function rectBgText(txt, x = 0, y = 0, align=''){
const height = font_t();
const width = txt == ''|| txt == ' ' ? canvas_t : (font_t()*.75)* txt.length;
let px = x; let py = y - (height - (height / 6));
if(align == 'center'){px = x - (width / 2);}
else if(align != 'left')px = x - width;
return {'width': width, 'height':height, 'x': px, 'y' : py};}
//signals:
window.addEventListener('keydown',keyMov);
btnArriba.addEventListener('click', movArriba);
btnAbajo.addEventListener('click', movAbajo);
btnDer.addEventListener('click', movDer);
btnIzq.addEventListener('click', movIzq);
//slots:
function keyMov(event){
switch(event.keyCode){
//enter S N Y
case 13: case 83: case 78: case 89:
selectOptionEvent(event.keyCode);
break;//selecionar opciones
case 37: movIzq(); break;//izquierda
case 38: movArriba(); break;//arriba
case 39: movDer(); break; //derecha
case 40: movAbajo(); break;//abajo
default: break;}}
function selectOptionEvent(key){
if(key == 83 || key == 89) re_iniciar['resetStart'] = 'si';
else if(key == 78) re_iniciar['resetStart'] = 'no';
else re_iniciar['resetStart'] = re_iniciar['select'];
update();}
function movArriba(){
prePosJugador = posJugador;
posJugador -= 10;
if(posJugador < 0) posJugador = prePosJugador;
update();}
function movAbajo(){
prePosJugador = posJugador;
posJugador += 10;
if(posJugador >= 100) posJugador = prePosJugador;
update();}
function movDer(){
if(re_iniciar['exec']) re_iniciar['select'] = 'no';
else{
prePosJugador = posJugador;
const pered = (~~(posJugador / 10)) *10 + 10;
++posJugador;
if(posJugador >= pered) posJugador = prePosJugador;}
update();}
function movIzq(){
if(re_iniciar['exec']) re_iniciar['select'] = 'si';
else{
prePosJugador = posJugador;
const pered = ~~(posJugador /10) * 10;
--posJugador;
if(posJugador < pered) posJugador = prePosJugador;}
update();}
Date.now()
El método estático Date.now()
devuelve el número de milisegundos transcurridos desde el 1 de enero de 1970 a las 00:00:00 UTC.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
Desarrolle una función que de al tiempo en milisegundos, un formato en el que se puede apreciar las horas, minutos, segundos y centisegundos a partir de los milisegundos; tal cual como lo haría un cronómetro.
Utilicé una conversión para que se vea mejor el tiempo.
function showTime(){
textTime.innerText = +((Date.now()-timeStart)/1000);
}
Además especifiqué en la etiqueta <p> que el tiempo es en segundos. Este curso está super.
Con formato fácil:
Se crea una función:
function formatTime(ms){
const cs = parseInt(ms/10) % 100
const seg = parseInt(ms/1000) % 60
const min = parseInt(ms/60000) % 60
const csStr = `${cs}`.padStart(2,"0")
const segStr = `${seg}`.padStart(2,"0")
const minStr = `${min}`.padStart(2,"0")
return`${minStr}:${segStr}:${csStr}`
}
Se modifica el string de showTime():
function showTime(){
spanTime.innerHTML = formatTime(Date.now()-timeStart);
}
Time: mm:ss:ms
¡Que maravilloso profesor eres, Juan!
Yo lo que hice fué cambiar el formato de tiempo a como se conoce normalmente ya que como aparece sin dos puntos ni nada de eso no me parece legible, entonces mi solución es que el tiempo tenga esto
function showTime() {
const time = Date.now() - timeStart;
const minutes = Math.floor(time / 60000);
const seconds = Math.floor((time % 60000) / 1000);
const milliseconds = time % 1000;
spanTime.innerHTML = `${minutes}:${seconds}.${milliseconds}`;
}
eso es lo que cambiaria en la función de showTime, el código completo es este obviamente sin contar lo de las próximas clases
const canvas = document.getElementById('game');
const game = canvas.getContext('2d');
const btnUp = document.getElementById('up');
const btnLeft = document.getElementById('left');
const btnRight = document.getElementById('right');
const btnDown = document.getElementById('down');
const spanLives = document.getElementById('lives');
const spanTime = document.getElementById('time');
let canvasSize;
let elementsSize;
let level = 0;
let lives = 3;
let timeStart;
let timePlayer;
let timeInterval;
const playerPos = {
x: undefined,
y: undefined,
};
const finishPos = {
x: undefined,
y: undefined,
}
let enemiesPos = [];
window.addEventListener("load", setCanvasSize);
window.addEventListener("resize", setCanvasSize);
function setCanvasSize() {
if (window.innerHeight > window.innerWidth) {
canvasSize = window.innerWidth * 0.8;
} else {
canvasSize = window.innerHeight * 0.8;
}
canvas.setAttribute('width', canvasSize);
canvas.setAttribute('height', canvasSize);
elementsSize = canvasSize / 10;
startGame();
}
function startGame() {
game.font = (elementsSize - 12) + 'px Verdana';
game.textAlign = 'end';
game.textBaseline = 'bottom';
const map = maps[level];
if (!map) {
gameWin();
return;
}
if(!timeStart) {
timeStart=Date.now();timeInterval=setInterval(showTime,100);
}
const mapRows = map.trim().split('\n');
const mapRowCols = mapRows.map(row => row.trim().split(''));
showLives();
enemiesPos = [];
game.clearRect(0,0, canvasSize, canvasSize);
mapRowCols.forEach((row, rowI) => {
row.forEach((col, colI) => {
const emoji = emojis[col];
const posX = elementsSize * (colI + 1);
const posY = elementsSize * (rowI + 1);
if(col == "O"){
if (!playerPos.x && !playerPos.y) {
playerPos.x = posX;
playerPos.y = posY;
}
} else if(col == "I"){
finishPos.x = posX;
finishPos.y = posY;
} else if (col == 'X') {
enemiesPos.push({
x: posX,
y: posY,
});
}
game.fillText(emoji, posX, posY);
});
});
movePlayer();
}
function movePlayer(){
const giftColX = playerPos.x.toFixed(2) == finishPos.x.toFixed(2);
const giftColY = playerPos.y.toFixed(2) == finishPos.y.toFixed(2);
const giftCol = giftColX && giftColY;
if (giftCol) {
levelWin();
}
const enemiesCol = enemiesPos.find( enemy => {
const enemyColX = enemy.x.toFixed(2) == playerPos.x.toFixed(2);
const enemyColY = enemy.y.toFixed(2) == playerPos.y.toFixed(2);
return enemyColX && enemyColY;
});
if (enemiesCol) {
levelFail();
}
game.fillText(emojis['PLAYER'], playerPos.x, playerPos.y);
}
function levelWin() {
console.log('Subiste de nivel');
level++;
startGame();
}
function levelFail() {
console.log('Chocaste');
lives--;
timeStart=undefined;
console.log(lives);
if (lives <= 0) {
level = 0;
lives = 3;
}
playerPos.x = undefined;
playerPos.y = undefined;
startGame();
}
function gameWin() {
console.log('¡Terminaste el juego!');
clearInterval(timeInterval);
}
function showLives() {
spanLives.innerHTML = emojis["HEART"].repeat(lives)
}
function showTime() {
const time = Date.now() - timeStart;
const minutes = Math.floor(time / 60000);
const seconds = Math.floor((time % 60000) / 1000);
const milliseconds = time % 1000;
spanTime.innerHTML = `${minutes}:${seconds}.${milliseconds}`;
}
window.addEventListener('keydown', moveKeysDir);
btnUp.addEventListener('click', moveUp);
btnLeft.addEventListener('click', moveLeft);
btnRight.addEventListener('click', moveRight);
btnDown.addEventListener('click', moveDown);
function moveKeysDir(event){
if (event.code == "KeyW" || event.code == "ArrowUp") {
moveUp();
} else if (event.code == "KeyA" || event.code == "ArrowLeft") {
moveLeft();
} else if (event.code == "KeyD" || event.code == "ArrowRight") {
moveRight();
} else if (event.code == "KeyS" || event.code == "ArrowDown") {
moveDown();
}
}
function moveUp(){
if ((playerPos.y - elementsSize) < elementsSize ) {
} else {
playerPos.y -= elementsSize;
startGame();
}
}
function moveLeft(){
if ((playerPos.x - elementsSize) < elementsSize ) {
} else {
playerPos.x -= elementsSize;
startGame();
}
}
function moveRight(){
if ((playerPos.x + elementsSize) > canvasSize ) {
} else {
playerPos.x += elementsSize;
startGame();
}
}
function moveDown(){
if ((playerPos.y + elementsSize) > canvasSize ) {
} else {
playerPos.y += elementsSize;
startGame();
}
}
Solución a contador de tiempo de juego a partir de minuto 11:40 de la clase
Solución
Paso a paso
hice los tres niveles en 10 segundos ajajajajaj
Función global: clearInterval( )
_
En la función levelFail
se debería aumentar:
if (lives <= 0) {
level = 0;
lives = 3;
clearInterval(timeInterval);
timeStart = undefined;
}
Para evitar una memory leak.
La variable fecha incluye el tiempo en milisegundos por lo que una diferencia entre la fecha en que inicio y en la que termino, puede ser la medida de tiempo.
// Al iniciar el juego, se guarda el tiempo actual
startTime = new Date();
Esta se coloca en movePlayer cuando sale de O, y esta sería la función de winLevel:
function winLevel() {
const endTime = new Date();
const elapsedSeconds = Math.floor((endTime.getTime() - (startTime as Date).getTime()) / 1000);
console.log('Ganaste eeee');
console.log('Tiempo transcurrido:', elapsedSeconds, 'segundos');
if (level >= 0 && level < maps.length -1) {
level += 1
mapaActual = []
startGame()
} else {
console.error('Fin del juego Ganaste!!');
}
}
Para insertar emojies usar tecla de Windows
y .
Reinventando la rueda para formatear el timer 😅
function showTime() {
const msToSec = (Date.now() - timeStart) / 1000;
const milliseconds = (String(msToSec - Math.floor(msToSec)).split('.')[1]).substring(0,2);
const milliInterval = Date.now() - timeStart;
let seconds = Math.floor(milliInterval / 1000);
let minutes = Math.floor(seconds / 60);
seconds = (seconds % 60) < 10 ? `0${seconds % 60}` : seconds % 60;
minutes = (minutes % 60) < 10 ? `0${minutes % 60}` : minutes % 60;
spanTime.innerHTML = `${minutes}:${seconds}:${milliseconds}`;
}
Para añadir mayor dificultad al juego, en lugar de crear un cronómetro, he decidido poner un temporizador en el que el tiempo vaya hacia atrás y cuando llegue a 0 el jugador pierda la partida.
Para ello, he creado las variables:
let totalTime;
let timeInterval;
He añadido el siguiente condicional dentro de la función startGame:
if (!timeInterval) {
timeInterval = setInterval(showTime, 100);
}
Y he creado la siguiente función, en la que establezco el tiempo inicial multiplicando el número total de mapas por 5 segundos cada uno, y posteriormente se van restando 10 al tiempo total cada 10 milisegundos hasta llegar a 0, momento en el que limpio el canvas con clearRect y reinicio los valores de la posición del jugador, del tiempo total, el nivel y el número de vidas:
function showTime() {
if (!totalTime) {
totalTime = maps.length * 500;
} else {
if (totalTime > 10) {
totalTime -= 10;
} else {
game.clearRect(0, 0, canvasSize, canvasSize);
playerPosition.col = undefined;
playerPosition.row = undefined;
totalTime = null;
level = 0;
lives = 3;
startGame();
}
}
spanTime.innerText = totalTime;
}
Contador de tiempo utilizando setInterval y clearInterval
Función global: setTimeout( )
_
Función global: setInterval()
_
profe, alguna cosa, ya se como generar el error que tanto lo atormenta jajajajaja, solo tiene que cambiar la resolución de la pantalla. cámbiele el tamaño al navegador, y no le generara un error como tal, pero le dejara de detectar los enemigos y el regalito. a mi me pasa. dure 6 hora buscando porque no me detectaba las bombitas. lo que hice para solucionar eso, es poner un condicional. si la resolución de la pantalla cambia, entonces se reimprime el juego. utilizando el atributo window.innerWidth =P espero que a mis colegas les funcione si tienen el mismo problema.
Hice esto:
Me parecio mas sencillo de leer de esta manera siendo que por ahora son solo 3 niveles de juego
function showTime(){
// Mostramos el tiempo en segundos
spanTime.innerHTML = ((Date.now() - timeStart)/1000).toFixed(1);
}
¿Quieres ver más aportes, preguntas y respuestas de la comunidad?