ゲームライブラリ構築までの道「インベーダー編」#5 : 弾の発射

2020年7月24日

Javascript テクノロジー プログラミング 特集

ファミコンのシューティングゲームが大好きだった、ユゲタです。 ゼビウス、スターソルジャー、グラディウス、ツインビー・・・ どれも、BMGが流れると今でもテンションが上ります。

本日のIT謎掛け

「シューティングゲーム」と、かけまして・・・ 「友達の内間君と待ち合わせ」と、ときます。 そのココロは・・・ うちまくる。

ソースコード

今回は、自機から弾を発射させてみたいと思います。 弾の発射は、マウスクリック、スペースバーを使いますが、スマートフォンは、別途コントローラを考えたいので、今回はPC仕様で作業を薦めます。 (function(w,d){ var __options = { canvas_size : { x : null , y : null }, "cannon" : { type : "bit", fill : "#57F2D6", bitSize : 4, src : "images/dot/cannon.dot", x : 10, y : 200, w : 64, h : 64, moveX : 10 }, "uso" : { type : "bit", fill : "white", bitSize : 4, src : "images/dot/ufo.dot", x : 10, y : 10, w : 64, h : 64, moveX : 10 }, "invader" : { "crab" : [ { type : "bit", fill : "#F22786", src : 'images/dot/crab_1.dot', bitSize : 4, x : 10, y : 64, w : 64, h : 64 }, { type : "bit", fill : "#F22786", src : "images/dot/crab_2.dot", bitSize : 4, x : 10, y : 64, w : 64, h : 64 } ], "octpus" : [ { type : "bit", fill : "#57F2D6", src : "images/dot/octpus_1.dot", bitSize : 4, x : 80, y : 64, w : 64, h : 64 }, { type : "bit", fill : "#57F2D6", src : "images/dot/octpus_2.dot", bitSize : 4, x : 80, y : 64, w : 64, h : 64 } ], "squid" : [ { type : "bit", fill : "#68F205", bitSize : 4, src : "images/dot/squid_1.dot", x : 150, y : 64, w : 64, h : 64 }, { type : "bit", fill : "#68F205", bitSize : 4, src : "images/dot/squid_2.dot", x : 150, y : 64, w : 64, h : 64 } ] } }; var MAIN = function(canvas_selector){ this.canvas_selector = canvas_selector || "canvas"; this.shoots = []; this.setCanvas(this.canvas_selector); this.set_imageMax(); this.pattern = 0; this.view(this.pattern); this.event_set(); this.animation_roop(30); }; MAIN.prototype.setCanvas = function(selector){ this.canvas_elm = d.querySelector(selector); if(w.innerWidth < this.canvas_elm.offsetWidth){ this.canvas_elm.setAttribute("width" , w.innerWidth); } if(w.innerHeight < this.canvas_elm.offsetHeight){ this.canvas_elm.setAttribute("height" , w.innerHeight); } __options.canvas_size.x = this.canvas_elm.offsetWidth; __options.canvas_size.y = this.canvas_elm.offsetHeight; __options.cannon.y = __options.canvas_size.y - 100; // smooth this.ctx = this.canvas_elm.getContext("2d"); this.ctx.imageSmoothingEnabled = false; this.ctx.mozImageSmoothingEnabled = false; this.ctx.webkitImageSmoothingEnabled = false; this.ctx.msImageSmoothingEnabled = false; }; MAIN.prototype.clear = function(){ this.ctx.clearRect(0, 0, this.canvas_elm.width, this.canvas_elm.height); }; MAIN.prototype.view = function(pattern){ // invader for(var i in __options.invader){ this.image(__options.invader[i][pattern]); } // canon this.image(__options.cannon); } MAIN.prototype.image_cache = []; MAIN.prototype.image = function(options){ if(!this.canvas_elm){return;} if(!options){return;} // 新規読み込み if(typeof this.image_cache[options.src] === "undefined"){ switch(options.type){ case "file": this.image_file_set(options); break; case "bit": this.image_bit_set(options); break; } } // キャッシュ利用 else{ switch(options.type){ case "file": this.image_draw(options); break; case "bit": this.image_bit_make(options); break; } } }; MAIN.prototype.image_file_set = function(options){ this.image_cache[options.src] = new Image(); var img = this.image_cache[options.src]; img.src = options.src; img.onload = (function(options){ this.image_draw(options , img); }).bind(this , options); }; MAIN.prototype.image_bit_set = function(options){ this.image_cache[options.src] = ""; new AJAX({ url : options.src, methdo : "GET", async : true, onSuccess : (function(options , data){ var char16 = data.split("\n"); options.bits = []; for(var i in char16){ // 16進数を2進数に変換 var char2 = parseInt(char16[i] , 16).toString(2); // ゼロパディング用桁数算出 var str_count = Math.pow(char16[i].length , 2); var zero = new Array(str_count).fill("0").join("") // サイズ取得 options.bitSize = options.w / str_count; char2 = (zero + char2).slice(-str_count); options.bits.push(char2); } this.image_bit_make(options); }).bind(this , options) }); }; MAIN.prototype.image_bit_make = function(options){ if(!options.bits){return;} this.ctx.fillStyle = options.fill; this.ctx.strokeStyle = null; this.ctx.strikeWidth = 0; for(var i=0; i<options.bits.length; i++){ for(var j=0; j<options.bits[i].length; j++){ var bit = options.bits[i].charAt(j); if(bit == 0){continue;} var w = options.bitSize; var h = options.bitSize; var x = options.x + (j * w); var y = options.y + (i * h); this.ctx.fillRect(x , y , Math.ceil(w) , Math.ceil(h)); } } }; MAIN.prototype.image_draw = function(options){ if(typeof this.image_cache[options.src] !== "object"){return} var img = this.image_cache[options.src]; this.ctx.drawImage(img , options.x, options.y ,options.w , options.h); }; MAIN.prototype.animation_roop = function(time){ var func = (function(e){this.animation(e)}).bind(this); this.animation_proc(); if(window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame){ window.requestAnimationFrame(func); } else{ time = time || 10; anim_flg = setTimeout(func , time); } }; MAIN.prototype.animation_proc = function(){ switch(this.keydown_flg){ case "right": this.cannon_move(__options.cannon.x + __options.cannon.moveX); break; case "left": this.cannon_move(__options.cannon.x - __options.cannon.moveX); break; } if(this.shoots.length){ for(var i in this.shoots){ var w = 1 * __options.cannon.bitSize; var h = 2 * __options.cannon.bitSize; if(this.shoots[i].y < h){ this.shoots.splice(i,1); } else{ this.ctx.fillStyle = "white"; this.ctx.fillRect(this.shoots[i].x , this.shoots[i].y , w , h); this.shoots[i].y -= __options.cannon.bitSize * 2; } } } }; MAIN.prototype.cannon_move = function(pos){ if(pos < 0){ pos = 0; } else if(pos > this.canvas_elm.offsetWidth - __options.cannon.w){ pos = this.canvas_elm.offsetWidth - __options.cannon.w; } __options.cannon.x = pos; }; MAIN.prototype.animation = function(){ this.clear(); this.nextPattern(300); this.view(this.pattern); this.animation_roop(30); } MAIN.prototype.nextPattern = function(frame_rate){ this.prev_time = this.prev_time || 0; if((+new Date()) - this.prev_time < frame_rate){return} this.prev_time = (+new Date()); this.pattern++; if(this.pattern >= this.image_max){ this.pattern = 0; } }; MAIN.prototype.set_imageMax = function(){ for(var i in __options.invader){ this.image_max = __options.invader[i].length; break; } }; /* Event */ MAIN.prototype.event_set = function(){ new LIB().event(w , "click" , (function(e){this.click(e)}).bind(this)); new LIB().event(w , "keydown" , (function(e){this.keydown(e)}).bind(this)); new LIB().event(w , "keyup" , (function(e){this.keyup(e)}).bind(this)); new LIB().event(w , "mousemove" , (function(e){this.mousemove(e)}).bind(this)); new LIB().event(w , "touchmove" , (function(e){this.touchmove(e)}).bind(this)); new LIB().event(w , "touchend" , (function(e){this.touchend(e)}).bind(this)); }; MAIN.prototype.click = function(e){ // if(this.flg_gamestart === true){return} if(this.shoots.length < 2){ this.shoots.push({ x : __options.cannon.x + (__options.cannon.w / 2), y : __options.cannon.y + (__options.cannon.bitSize * 4) }); } }; MAIN.prototype.keydown = function(e){ switch(e.keyCode){ case 37: // <- case 'ArrowLeft': this.keydown_flg = "left"; break; case 39: // -> case 'ArrowRight': this.keydown_flg = "right"; break; } }; MAIN.prototype.keyup = function(e){ this.keydown_flg = false; }; MAIN.prototype.mousemove = function(e){ // if(this.flg_gamestart !== true){return;} this.mousePos = this.mousePos || e.clientX; this.cannon_move(__options.cannon.x + (e.clientX - this.mousePos)); this.mousePos = e.clientX; }; MAIN.prototype.touchmove = function(e){ // if(this.flg_gamestart !== true){return;} if(!e || !e.touches || e.touches.length > 1){ this.mousePos = null; return; } this.mousePos = typeof this.mousePos === "number" ? this.mousePos : e.touches[0].clientX; this.cannon_move(__options.cannon.x + (e.touches[0].clientX - this.mousePos)); this.mousePos = e.touches[0].clientX; }; MAIN.prototype.touchend = function(e){ this.mousePos = null; }; var LIB = function(){}; LIB.prototype.event = function(target, mode, func , flg){ flg = (flg) ? flg : false; if (target.addEventListener){target.addEventListener(mode, func, flg)} else{target.attachEvent('on' + mode, function(){func.call(target , window.event)})} }; var AJAX = function(option){//console.log(option); if(!option){return} var httpoj = this.createHttpRequest(); if(!httpoj){return;} // var option = new MAIN().setOption(options); var data = this.setQuery(option); if(!data.length){ option.method = "get"; } httpoj.open( option.method , option.url , option.async ); httpoj.setRequestHeader('Content-Type', option.type); httpoj.onreadystatechange = function(){ if (this.readyState==4 && httpoj.status == 200){ option.onSuccess(this.responseText); } }; // FormData 送信用 if(typeof option.form === "object" && Object.keys(option.form).length){ httpoj.send(option.form); } // query整形後 送信 else{ if(data.length){ httpoj.send(data.join("&")); } else{ httpoj.send(); } } }; AJAX.prototype.dataOption = { url:"", query:{}, querys:[], data:{}, form:{}, async:"true", method:"POST", type:"application/x-www-form-urlencoded", onSuccess:function(res){}, onError:function(res){} }; AJAX.prototype.createHttpRequest = function(){ //Win ie用 if(window.ActiveXObject){ //MSXML2以降用; try{return new ActiveXObject("Msxml2.XMLHTTP")} catch(e){ //旧MSXML用; try{return new ActiveXObject("Microsoft.XMLHTTP")} catch(e2){return null} } } //Win ie以外のXMLHttpRequestオブジェクト実装ブラウザ用; else if(window.XMLHttpRequest){return new XMLHttpRequest()} else{return null} }; // URL-Queryの作成 AJAX.prototype.setQuery = function(option){ var data = []; if(typeof option.datas !== "undefined"){ for(var key of option.datas.keys()){ data.push(key + "=" + option.datas.get(key)); } } if(typeof option.query !== "undefined"){ for(var i in option.query){ data.push(i+"="+encodeURIComponent(option.query[i])); } } if(typeof option.querys !== "undefined"){ for(var i=0;i<option.querys.length;i++){ if(typeof option.querys[i] == "Array"){ data.push(option.querys[i][0]+"="+encodeURIComponent(option.querys[i][1])); } else{ var sp = option.querys[i].split("="); data.push(sp[0]+"="+encodeURIComponent(sp[1])); } } } return data; } new LIB().event(w , "load" , function(){new MAIN("#mycanvas")}); })(window,document);

解説

なるべく、本家に似せるために、背景色を黒にして、インベーダーなどのドット絵に色をセットしてみました。 今回の目的の弾の発射ですが、this.shootsという、内部配列を持たせて、クリックした時(スペースを押した時)に、この配列に自機の座標をpush(配列追加)します。 これにより、弾の発射位置を特定します。 あとは、animationタイミングで、y座標にマイナス値を加えていくと、弾が発射されたように見えます。 this.shootsを配列にした点は、弾を連打すると、後発の弾が、前の弾をかき消してしまうため、複数保持する必要があったためです。 もし、1発だけでいいのだとすると、弾が消えるまでは、次の弾の座標登録をしないようにしないといけないのですが、配列にしていると、配列数が指定数を超えたら、登録をしなければいいだけなので、1つでも2つでも好きな数値でプレイできます。 今回は、2発動時発射を許可しています。 もはや、コリジョン判定を入れてないのに、無意識に敵キャラに弾を当ててしまいますよねwww

Github

ソースは以下にアップしています。(ver:0.4) https://github.com/yugeta/game_invader

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。