La semana pasada dejamos listos los asteroides, y solo quedó pendiente el impacto entre estos y nuestro jugador. He querido dejarlo para un tema aparte, pues deseaba probar algo diferente para esta ocasión.
A diferencia de nuestro anterior juego de naves donde al ser impactado el jugador, este pierde parte de su vitalidad y obtiene inmunidad por unos instantes, los juegos de asteroides se caracterizan por hacer explotar al jugador al impactar contra él, antes de regenerarlo en el centro con su respectiva breve inmunidad. Este mismo efecto es el que aprenderemos a hacer ahora.
Comenzamos declarando el arreglo de las explosiones. Para dibujarles, usaremos los círculos dorados en la hoja de sprites que hasta ahora habíamos dejado sin usar, de la siguiente forma:
Con esto la explosión ha quedado lista. Tan solo queda hacer desaparecer la nave mientras la explosión se efectúa. Este truco visual puede ser fácilmente simulado junto al dibujado intermitente de la nave durante la inmunidad; solo agregamos también que se dibuje si el temporizador es menor que los ciclos donde no debería aparecer (más de 20):
A diferencia de nuestro anterior juego de naves donde al ser impactado el jugador, este pierde parte de su vitalidad y obtiene inmunidad por unos instantes, los juegos de asteroides se caracterizan por hacer explotar al jugador al impactar contra él, antes de regenerarlo en el centro con su respectiva breve inmunidad. Este mismo efecto es el que aprenderemos a hacer ahora.
Comenzamos declarando el arreglo de las explosiones. Para dibujarles, usaremos los círculos dorados en la hoja de sprites que hasta ahora habíamos dejado sin usar, de la siguiente forma:
ctx.strokeStyle='#ff0';
for(var i=0,l=explosion.length;i<l;i++)
explosion[i].drawImageArea(ctx,spritesheet, 35,(aTimer%2)*5,5,5);
Al colisionar un asteroide con el jugador restaremos una de sus vidas y le damos inmunidad de 60 ciclos, esto es, tres segundos. Posteriormente creamos la explosión con 8 círculos, uno cada 45 grados y un temporizador de 40 ciclos (2 segundos). Esto nos da un segundo extra para posicionar de nuevo el jugador en el centro y prepararlo para empezar de nuevo: // Collision Enemy-Player
if(player.timer<1&&enemies[i].distance(player)<0){
lives--;
player.timer=60;
for(var j=0;j<8;j++){
var e=new Circle(player.x,player.y,2.5);
e.rotation=45*j;
e.timer=40;
explosion.push(e);
}
}
Mover las explosiones ya no deben representar reto alguno a estos niveles: // Move Explosion
for(var i=0,l=explosion.length;i<l;i++){
explosion[i].move((explosion[i].rotation-90)*Math.PI/180,1);
explosion[i].timer--;
if(explosion[i].timer<1){
explosion.splice(i--,1);
l--;
}
}
Como mencioné antes, una vez terminada la explosión, 20 ciclos antes de terminar la inmunidad del jugador, es tiempo de regresarlo a la posición de inicio. No solo es cambiar su posición, si no también regresar su rotación y velocidad a 0. Esto lo haremos de forma más fácil en una función que llamaremos "playerReset": // Damaged
if(player.timer>0){
player.timer--;
if(player.timer==20){
playerReset();
}
}
Y la función ha de quedar de esta forma: function playerReset(){
player.x=canvas.width/2;
player.y=canvas.height/2;
player.rotation=0;
player.speed=0;
}
Podemos usar esta misma función dentro de la función "reset" para no tener que repetir los mismos comandos dos veces. No olvides también agregar en esta función la limpieza del arreglo de la explosión.Con esto la explosión ha quedado lista. Tan solo queda hacer desaparecer la nave mientras la explosión se efectúa. Este truco visual puede ser fácilmente simulado junto al dibujado intermitente de la nave durante la inmunidad; solo agregamos también que se dibuje si el temporizador es menor que los ciclos donde no debería aparecer (más de 20):
if(player.timer<21&&player.timer%2==0){
ctx.strokeStyle='#0f0';
if(pressing[KEY_UP])
player.drawImageArea(ctx,spritesheet, (aTimer%3)*10,0,10,10);
else
player.drawImageArea(ctx,spritesheet, 0,0,10,10);
}
¡Con esto el juego ha quedado terminado! Podemos comprobar como las explosiones y regeneración le dan un toque mas propio a este juego.Código final:
(function(){
'use strict';
window.addEventListener('load',init,false);
var KEY_ENTER=13;
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 pause=true;
var score=0;
var lives=0;
var aTimer=0;
var player=new Circle(150,75,5);
var shots=[];
var enemies=[];
var explosion=[];
var spritesheet=new Image();
var background=new Image();
spritesheet.src='assets/asteroids.png';
background.src='assets/nebula2.jpg';
function random(max){
return ~~(Math.random()*max);
}
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 playerReset(){
player.x=canvas.width/2;
player.y=canvas.height/2;
player.rotation=0;
player.speed=0;
}
function reset(){
playerReset();
player.timer=0;
shots.length=0;
enemies.length=0;
explosion.length=0;
score=0;
lives=3;
}
function act(deltaTime){
if(!pause){
// GameOver Reset
if(lives<1)
reset();
// 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&&player.timer<21){
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);
}
// New Enemies
if(enemies.length<1){
for(var i=0;i<3;i++){
var e=new Circle(-20,-20,20);
e.rotation=random(360);
enemies.push(e);
}
}
// Move Enemies
for(var i=0,l=enemies.length;i<l;i++){
enemies[i].move((enemies[i].rotation-90)*Math.PI/180,2);
// Collision Enemy-Player
if(player.timer<1&&enemies[i].distance(player)<0){
lives--;
player.timer=60;
for(var j=0;j<8;j++){
var e=new Circle(player.x,player.y,2.5);
e.rotation=45*j;
e.timer=40;
explosion.push(e);
}
}
// Collision Enemy-Shot
for(var j=0,ll=shots.length;j<ll;j++){
if(enemies[i].distance(shots[j])<0){
if(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);
}
}
score++;
enemies.splice(i--,1);
l--;
shots.splice(j--,1);
ll--;
}
}
}
// Move Explosion
for(var i=0,l=explosion.length;i<l;i++){
explosion[i].move((explosion[i].rotation-90)*Math.PI/180,1);
explosion[i].timer--;
if(explosion[i].timer<1){
explosion.splice(i--,1);
l--;
}
}
// Damaged
if(player.timer>0){
player.timer--;
if(player.timer==20){
playerReset();
}
}
// GameOver
if(lives<1){
pause=true;
}
// Animation Cicle
aTimer+=deltaTime;
if(aTimer>3600)
aTimer-=3600;
}
if(lastPress==KEY_ENTER)
pause=!pause;
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='#00f';
for(var i=0,l=enemies.length;i<l;i++)
enemies[i].drawImageArea(ctx,spritesheet, 0,10,40,40);
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);
if(player.timer<21&&player.timer%2==0){
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.strokeStyle='#ff0';
for(var i=0,l=explosion.length;i<l;i++)
explosion[i].drawImageArea(ctx,spritesheet, 35,(~~(aTimer*10)%2)*5,5,5);
ctx.fillStyle='#fff';
if(spritesheet.width)
for(var i=0;i<lives;i++)
ctx.drawImage(spritesheet, 0,0,10,10, canvas.width-20-20*i,10,10,10);
else
ctx.fillText('Lives: '+lives,canvas.width-50,20);
//ctx.fillText('Rotation: '+player.rotation,0,20);
ctx.fillText('Score: '+score,0,20);
if(pause){
ctx.textAlign='center';
if(lives<1)
ctx.fillText('GAME OVER',canvas.width/2,canvas.height/2);
else
ctx.fillText('PAUSE',canvas.width/2,canvas.height/2);
ctx.textAlign='left';
}
}
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);};
})();
})();
MUY BUENOS TUS TUTORIALES, PERO NO SERIA MEJOR USAR EN LA FUNCION CIRCLE , CIRCLE.PROTOTYPE.DISTANCE , CIRCLE.PROTOTYPE.MOVE, ETC.... Y TRATAR DE NO USAR TANTAS TRANFORMACIONES DEL CANVAS DIGO CTX,TRANSLATE,, Y ASI...
ResponderBorrarUsar prototype es una forma alternativa para na creación de funciones, pero realmente no tienen impacto mayor sobre el script final. Con respecto a las transformaciones, son usadas sólo las necesarias para rotar las imágenes. ¿Conoces alguna forma alterna que use menos transformaciones?
BorrarEL CREAR UN NUEVO OBJETO EN JS SIN EL PROTOTYPE ES HACER UN COPIA EN MEMORIA Y CLARO QUE EN UN PROJECTO MAS GRANDE ESO PASA FACTURA.. PARA ROTAR SE PUEDE HACE UNA FUNCION ROTAR
Borrarfunction rotar(punto, centro, angulo){
angulo = (angulo ) * (Math.PI/180); // A RADIANES
var rotarX = Math.cos(angulo) * (punto.x - centro.x) - Math.sin(angulo) * (punto.y-centro.y) + centro.x;
var rotarY = Math.sin(angulo) * (punto.x - centro.x) + Math.cos(angulo) * (punto.y - centro.y) + centro.y;
return {x:rotarX,y:rotarY}
}
esta seria para rotar respecto a un eje pero modificadonla sirve para hacer girar libremente.
La información sobre prototype parece un poco escondida, pero he logrado constatar la razón en tus palabras, y como mencionas, en proyectos más grandes sí puede marcar la diferencia, y en ejemplos como el presente que no usan variables privadas, no presenta conflictos, aunque podría ser confuso para programadores con mayor experiencia en otros lenguajes. Veré la forma de compensar este conocimiento para su mejor enseñanza. Muchas gracias por la información.
BorrarAhora, con respecto a la rotación ¿Tienes un ejemplo práctico de esta función de rotación aplicada a Canvas? Que no he podido encontrar información al respecto. Muchas gracias.
JA NO PASA NADA YO AUN ESTOY APRENDIENDO Y PERO CREO QUE YA LEIDO BASTANTE JEJEJ ESTARE MUY SEGUIDO POR EL BLOG PARA SEGUIR APERNDIENO :) , Y SI INVESTIGA PROTOTIPOS ES UNO DE LOS VERDADEROS POTENCIALES DE JS, EH ESA FUNCION SERIA ASI
ResponderBorrarCREAR DOS CIRCULO NORMALMENTE Y LOS DEMAS ASI
function ROTAR (punto, centro, angulo){
var rotarX = Math.cos(angulo) * (punto.x - centro.x) - Math.sin(angulo) * (punto.y-centro.y) + centro.x;
var rotarY = Math.sin(angulo) * (punto.x - centro.x) + Math.cos(angulo) * (punto.y - centro.y) + centro.y;
return {x:rotarX,y:rotarY}
}
FUNCION UPDATE(DELTA) {
VAR ROTACION = 90 * (MATH.PI/ 180) + DELTA;//USAS DELTA COMO QUIERAS
VAR LOCAL;
LOCAL = ROTAR(CIRCULO1,CIRCULO2,ROTACION)
CIRCULO1.X= LOCAL.X
CIRCULO1.Y=LOCAL.Y
}
SI NO TE SIRVE ME MUESTRAS EL CODIGO QUE TIENES
El código para calcular la rotación no parece tener conflicto alguno, el detalle es dibujar con esa información en el lienzo. ¿Tú sabes como hacerlo?
BorrarCOMO DIBUJARLO? SI YA LO TENGO HECHO ES UN PROTOTIPO DE UN JUEGO QUE PIENSO HACER... PERO NO TIENES NADA HECHO?
ResponderBorrar¿Dibujar una imagen rotada con la formula que me das en lugar de transformaciones? Me temo que no he encontrado la forma de hacerlo. ¿Como es que lo haces tú?
BorrarHola no te des mala vida segun lo que leo lo que te quiere decir es que esa funcion es para rotar pero no precisamente imagenes, eh y si hay que procurar no hacer tantas transformacion en el canvas en lo que sea posible pero tu ejemplo va bien. Y si no has leido sobre prototipos creo que te falta la mitad de js. muy buena pagina.
ResponderBorrarNoto que calcula información de rotación, pero no veo que se pueda dibujar con ello. Esperaba a ver si quizá el tuviera informacion de una forma alterna, ya que yo solo conozco usar transformaciones para ello.
BorrarAun cuando llevo mucho tiempo en desarrollo de juegos, aun me falta descubrir muchos de los secretos de JavaScript, que poco a poco recibo y transmito a los demas. Gracias por todo :)
Con esa funciones para transformaciones creo que son suficiente el problema de ctx.rotate es la dependencia con el ctx.translate, eh una pregunta no tienes twitter personal, ya que desarrolladores html5 hispanos pocos, la mayoria que sigo y conozco ya sabes otro idioma.
ResponderBorrarsobre js podrias empezar por aqui si aun no lo has visto
http://blog.amatiasq.com/2012/01/javascript-conceptos-basicos-herencia-por-prototipos
Asi es. Se requieren muchas transformaciones, pero parece ser la única forma de hacer dicho dibujado. Con respecto al Twitter, solo tengo este creado especialmente para el blog.
BorrarDe prototype ya tengo experiencia, aunque veo que este ejemplo usa una forma distinta para crear sus funciones. Parece que tendré que averiguar aun mas sobre el mejor metodo para hacer uso de ello...
pero si estas pendiente a esa cuenta la voy a seguir...
ResponderBorrareh si las tranformaciones son un problema pero ahi es donde entra la optimizacion y eso.. la manera en que estoy programando ahora es de esta forma espero te sirva http://codeincomplete.com/posts/2013/10/27/rotating_tower_platformer/
un ejemplo brutal... pero para desarrollo final estoy usando el framework Phaser js.