Mucha gente parece no estar muy segura de como dibujar una animación, solo cuando el personaje está en movimieno. En realidad esta es una técnica bastante sencilla, se trata tan solo de una animación condicional. Pero para evitar dudas, veremos a detalle como hacer dicha técnica.
Para empezar, dado que haremos una animación, cambiaremos nuestra imagen por una hoja de sprites. Para no editarla más tarde, les entregaré de una vez la hoja de sprites completa que usaremos para el juego en este curso:
Importemos la imagen. Aprovechando que estamos trabajando ya con imágenes, agreguemos de paso la imagen de fondo de nuestro juego:
Dibujar la imagen de fondo de nuestro juego no representará mayor reto:
Ya que deseamos que el jugador sea animado únicamente cuando esté acelerando, estableceremos la condicional al presionar la tecla de arriba (Que usaremos para aceleración en un momento), y en caso contrario, dibujaremos una imagen sin animar. Para la animación, usaremos la variable "aTimer" como la vimos en el tema Hojas de sprites y animaciones, y la dibujaremos de la siguiente forma:
Aprovechemos que estamos con ella para agregar sus disparos. Esto ya lo sabemos desde el tema Municiones dinámicas; solo nos falta adaptarlo para su movimiento angular. Para ello, asignaremos al disparo creado su rotación, su velocidad mas 10, y un temporizador de 15 ciclos, cuya variable agregaremos antes a la variable círculo. Por tanto, la creación de un nuevo disparo quedaría como se muestra a continuación:
Para empezar, dado que haremos una animación, cambiaremos nuestra imagen por una hoja de sprites. Para no editarla más tarde, les entregaré de una vez la hoja de sprites completa que usaremos para el juego en este curso:
Importemos la imagen. Aprovechando que estamos trabajando ya con imágenes, agreguemos de paso la imagen de fondo de nuestro juego:
var spritesheet=new Image();
var background=new Image();
spritesheet.src='assets/asteroids.png';
background.src='assets/nebula2.jpg';
Para la imagen de fondo en esta ocasión, usaré otra de las opciones que vimos en el tema Fondo en movimiento. Esta vez he elegido la siguiente imagen:Dibujar la imagen de fondo de nuestro juego no representará mayor reto:
if(background.width)
ctx.drawImage(background,0,0);
Para dibujar la animación de la nave, necesitamos para empezar, tener nuestra función personalizada "drawImageArea" dentro de la función "Circle", soportando la rotación de imágenes: Circle.prototype.drawImageArea=function(ctx,img,sx,sy,sw,sh){
if(img.width){
ctx.save();
ctx.translate(this.x,this.y);
//ctx.scale(this.scale,this.scale);
ctx.rotate(this.rotation*Math.PI/180);
ctx.drawImage(img,sx,sy,sw,sh,-img.width/2,-img.height/2,img.width,img.height);
ctx.restore();
}
else
this.stroke(ctx);
}
Dado que en el juego presente no necesitamos escalar la imagen de ningún objeto, he comentado dicha variable tanto en su creación, como en su dibujado. Si en algún juego futuro la necesitas, siéntete libre de volverla a descomentar. Ahora que esta función está lista, ya podemos dibujar la animación del jugador.Ya que deseamos que el jugador sea animado únicamente cuando esté acelerando, estableceremos la condicional al presionar la tecla de arriba (Que usaremos para aceleración en un momento), y en caso contrario, dibujaremos una imagen sin animar. Para la animación, usaremos la variable "aTimer" como la vimos en el tema Hojas de sprites y animaciones, y la dibujaremos de la siguiente forma:
ctx.strokeStyle='#0f0';
if(pressing[KEY_UP])
player.drawImageArea(ctx,spritesheet, (~~(aTimer*10)%3)*10,0,10,10);
else
player.drawImageArea(ctx,spritesheet, 0,0,10,10);
Ahora que ya animamos nuestra nave al acelerar, falta agregar dicha aceleración. Para podernos desplazar de acuerdo al ángulo de rotación de la imagen, agregaremos en la función "Circle" una variable "speed", así como la función "move" aprendida en el tema Desplazamiento angular. Posteriormente, agregamos la aceleración y movimiento del jugador: // Set Acceleration
if(pressing[KEY_UP]){
if(player.speed<5)
player.speed++;
}
if(pressing[KEY_DOWN]){
if(player.speed>-5)
player.speed--;
}
// Move Player
player.move((player.rotation-90)*Math.PI/180,player.speed);
Nota que, debido a que el juego se desarrolla en el espacio, no hemos agregado desaceleración para la nave. Con esto tendremos nuestra nave animada y en movimiento para nuestro juego.Aprovechemos que estamos con ella para agregar sus disparos. Esto ya lo sabemos desde el tema Municiones dinámicas; solo nos falta adaptarlo para su movimiento angular. Para ello, asignaremos al disparo creado su rotación, su velocidad mas 10, y un temporizador de 15 ciclos, cuya variable agregaremos antes a la variable círculo. Por tanto, la creación de un nuevo disparo quedaría como se muestra a continuación:
// New Shot
if(lastPress==KEY_SPACE){
var s=new Circle(player.x,player.y,2.5);
s.rotation=player.rotation;
s.speed=player.speed+10;
s.timer=15;
shots.push(s);
}
Posteriormente movemos cada disparo, y lo removemos del arreglo cuando su temporizador llegue a cero: // Move Shots
for(var i=0,l=shots.length;i<l;i++){
shots[i].timer--;
if(shots[i].timer<0){
shots.splice(i--,1);
l--;
continue;
}
shots[i].move((shots[i].rotation-90)*Math.PI/180,shots[i].speed);
}
Finalmente, solo resta dibujar los disparos animados, que ya teníamos incluidos en la hoja de sprites: ctx.strokeStyle='#f00';
for(var i=0,l=shots.length;i<l;i++)
shots[i].drawImageArea(ctx,spritesheet, 30,(~~(aTimer*10)%2)*5,5,5);
Como podrás ver, todo el conocimiento para lo hecho en esta entrada ya lo teníamos de antes. Solo falta usar la creatividad para aplicar lo ya conocido en nuevas formas. Con esto tendremos ya nuestra nave espacial moviéndose en distintos ángulos, y disparando en la dirección a la que viaja.Código final:
(function(){
'use strict';
window.addEventListener('load',init,false);
var KEY_SPACE=32;
var KEY_LEFT=37;
var KEY_UP=38;
var KEY_RIGHT=39;
var KEY_DOWN=40;
var canvas=null,ctx=null;
var lastPress=null;
var pressing=[];
var player=new Circle(150,100,5);
var aTimer=0;
var shots=[];
var spritesheet=new Image();
var background=new Image();
spritesheet.src='assets/asteroids.png';
background.src='assets/nebula2.jpg';
function init(){
canvas=document.getElementById('canvas');
ctx=canvas.getContext('2d');
canvas.width=300;
canvas.height=200;
run();
repaint();
}
function run(){
setTimeout(run,50);
act(0.05);
}
function repaint(){
requestAnimationFrame(repaint);
paint(ctx);
}
function act(deltaTime){
// Set Rotation
if(pressing[KEY_RIGHT]){
player.rotation+=10;
}
if(pressing[KEY_LEFT]){
player.rotation-=10;
}
// Set Acceleration
if(pressing[KEY_UP]){
if(player.speed<5)
player.speed++;
}
if(pressing[KEY_DOWN]){
if(player.speed>-5)
player.speed--;
}
// Move Player
player.move((player.rotation-90)*Math.PI/180,player.speed);
// New Shot
if(lastPress==KEY_SPACE){
var s=new Circle(player.x,player.y,2.5);
s.rotation=player.rotation;
s.speed=player.speed+10;
s.timer=15;
shots.push(s);
}
// Move Shots
for(var i=0,l=shots.length;i<l;i++){
shots[i].timer--;
if(shots[i].timer<0){
shots.splice(i--,1);
l--;
continue;
}
shots[i].move((shots[i].rotation-90)*Math.PI/180,shots[i].speed);
}
aTimer+=deltaTime;
if(aTimer>3600)
aTimer-=3600;
lastPress=null;
}
function paint(ctx){
ctx.fillStyle='#000';
if(background.width)
ctx.drawImage(background,0,0);
else
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.strokeStyle='#f00';
for(var i=0,l=shots.length;i<l;i++)
shots[i].drawImageArea(ctx,spritesheet, 30,(~~(aTimer*10)%2)*5,5,5);
ctx.strokeStyle='#0f0';
if(pressing[KEY_UP])
player.drawImageArea(ctx,spritesheet, (~~(aTimer*10)%3)*10,0,10,10);
else
player.drawImageArea(ctx,spritesheet, 0,0,10,10);
ctx.fillStyle='#fff';
ctx.fillText('Rotation: '+player.rotation,0,20);
}
document.addEventListener('keydown',function(evt){
lastPress=evt.keyCode;
pressing[evt.keyCode]=true;
},false);
document.addEventListener('keyup',function(evt){
pressing[evt.keyCode]=false;
},false);
function Circle(x,y,radius){
this.x=(x==null)?0:x;
this.y=(y==null)?0:y;
this.radius=(radius==null)?0:radius;
//this.scale=1;
this.rotation=0;
this.speed=0;
this.timer=0;
}
Circle.prototype.distance=function(circle){
if(circle!=null){
var dx=this.x-circle.x;
var dy=this.y-circle.y;
return (Math.sqrt(dx*dx+dy*dy)-(this.radius+circle.radius));
}
}
Circle.prototype.move=function(angle,speed){
if(speed!=null){
this.x+=Math.cos(angle)*speed;
this.y+=Math.sin(angle)*speed;
// Out Screen
if(this.x>canvas.width)
this.x=0;
if(this.x<0)
this.x=canvas.width;
if(this.y>canvas.height)
this.y=0;
if(this.y<0)
this.y=canvas.height;
}
}
Circle.prototype.stroke=function(ctx){
ctx.beginPath();
ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,true);
ctx.stroke();
}
Circle.prototype.drawImageArea=function(ctx,img,sx,sy,sw,sh){
if(img.width){
ctx.save();
ctx.translate(this.x,this.y);
//ctx.scale(this.scale,this.scale);
ctx.rotate(this.rotation*Math.PI/180);
ctx.drawImage(img,sx,sy,sw,sh,-this.radius,-this.radius,this.radius*2,this.radius*2);
ctx.restore();
}
else
this.stroke(ctx);
}
window.requestAnimationFrame=(function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback){window.setTimeout(callback,17);};
})();
})();
Hola!! Antes que nada felicitarte por los tutoriales, estan muy claros y son fáciles de seguir.
ResponderBorrarAl probar el código que indicas arriba. Me esta escalando la imagen de la Nave.
El motivo de dicho escalado, es que se esta indicando las dimensiones del spritesheet en ves de las dimensiones de la nave
ctx.drawImage(img, sx, sy, sw, sh, -img.width/2, -img.height/2, img.width, img.height);
Para resolver esto, he encontrado dos posibles soluciones.
1.- Trabajar con los valores pasados por parámetro
ctx.drawImage(img, sx, sy, sw, sh, -sw/2, -sh/2, sw, sh);
2.- Trabajar con el rádio del circulo
ctx.drawImage(img, sx, sy, sw, sh, -this.radius, -this.radius, this.radius*2, this.radius*2);
Ambas soluciones son validas y me han funcionado.
Tienes mucha razón. Temo que mientras actualizaba el código de la lección anterior, he olvidado reemplazar esta función para que funcione con hojas de sprites, curioso, porque en la siguiente lección este error ya estaba arreglado.
BorrarYa he corregido este detalle para que nadie más pase por este problema, muchas gracias por hacermelo notar. ¡Y muchas felicidades habiendo encontrado las soluciones para este problema! Ambas son igualmente válidas en su forma básica, pero tienen funciones diversas en técnicas avanzadas, ya que el primero permite colisiones fantasma (Gran arma de doble filo, muy útil para quien sabe manejarles, aunque puede causar problemas inesperados si no lo sabes), y el segundo permite imágenes escaladas, como se ve en el tema siguiente, aunque hay quienes no saben hacer buen uso de esta técnica y terminan creando imágenes poco optimizadas, abusando de las capacidades de esta segunda técnica.
¡Excelente trabajo! Y gracias una vez más.
Me ha gustado mucho su trabajo es genial, pero como se podría modifificarla para que las imagenes de naves, disparos y asteroides, se ingresen por separado.
ResponderBorrarOriginalmente se había aprendido cómo ingresar cada imagen por separado (http://juegos.canvas.ninja/2012/02/el-juego-de-la-serpiente.html), pero posteriormente aprendimos cómo usar hojas de sprite y sus ventajas (http://juegos.canvas.ninja/2012/05/hojas-de-sprites-y-animaciones.html)
BorrarLa única ocación donde no recomendaría usar hojas de sprite, es de forma local durante el desarrollo. Espero esto te sirva. ¡Felices códigos!
gracias, me has ayudado mucho.
Borrargracias, me has ayudado mucho, pero quisiera que los asteroides no se separaran en 3, como lo hago.
Borrar¿Hablas del tema siguiente? Esto ocurre en estas líneas:
Borrarif(enemies[i].radius>5){
for(var k=0;k<3;k++){
var e=new Circle(enemies[i].x,enemies[i].y,enemies[i].radius/2);
e.rotation=shots[j].rotation+120*k;
enemies.push(e);
}
}
Si las remueves, los asteroides se eliminarán de forma inmediata, sin fragmentarse.