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

2020年7月24日

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

t f B! P L
ファミコンのシューティングゲームが大好きだった、ユゲタです。 ゼビウス、スターソルジャー、グラディウス、ツインビー・・・ どれも、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

このブログを検索

プロフィール

自分の写真
プログラミングとサーバーを心の底から楽しむクリエーターです。 経営者であり、開発者でもありますが、得意としているのは、アイデア創出で、出来高は無限大です。

ブログ アーカイブ

QooQ