ゲームライブラリ構築までの道「インベーダー編」#6 : 敵キャラの並び

2020年7月25日

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

eyecatch カラーリングの奥深さを思い知った、ユゲタです。 レトロゲームを再現するには、それぞれの色味をそのまま表す必要がありますが、昔のブラウン管テレビの表示カラーを、写真などからピックアップしても、全く再現性が無いことに気が付きました。 写真撮影された環境光によって、色味は変更されてしまうので、デジタル論理的な解釈で色を決めなければいけませんね。 恐らく原色と言われる数少ない色をそのまま表示するのでいいんでしょうが、デジタル原色って、どういうのがあるんだろ?

本日のIT謎掛け

「デジタルカラー」と、かけまし・・・ 「1日」と、ときます。 そのココロは・・・ 重要な数字は、24(24時間と24bit)です。

原色について

https://www.colordic.org/ WEB原色というのがあって、上記URLにその色見本が掲載されています。 そもそも、MSXなんかは、初期は16色しか持っておらず、MSX2になって256色使えるということで、衝撃を覚えた記憶もありますが、今では写真カラーの24ビット(約1,677万色)が当たり前になっていますが、そう考えると、レトロゲームの色は、限られた原色の中から素直に選択するのが良さそうです。

ソースコード

(function(w,d){ var __options = { canvas_size : { x : null , y : null }, bitmap_size : { w : 64 , h : 64}, cannon : { color : "#57F2D6", bitSize : 4, src : "images/dot/cannon.dot", x : 0 , y : 0 , w : 64, h : 64, moveX : 10 }, uso : { color : "white", bitSize : 4, src : "images/dot/ufo.dot", x : 0 , y : 0 , w : 64, h : 64, moveX : 10 }, bullet : { color : "white", bitSize : 4, src : "images/dot/bullet.dot", x : 0 , y : 0 , w : 64, h : 64, moveX : 10 }, invader_src : { "crab" : [ 'images/dot/crab_1.dot', "images/dot/crab_2.dot" ], "octpus" : [ "images/dot/octpus_1.dot", "images/dot/octpus_2.dot" ], "squid" : [ "images/dot/squid_1.dot", "images/dot/squid_2.dot" ] }, invader_info : {w : 64, h : 64 , bitSize : 4}, invader_row_infos : [ { invader : "squid" , color : "#68F205"}, { invader : "crab" , color : "#68F205"}, { invader : "crab" , color : "#57F2D6"}, { invader : "octpus", color : "#57F2D6"}, { invader : "octpus", color : "#F22786"} ] }; var MAIN = function(canvas_selector){ this.canvas_selector = canvas_selector || "canvas"; this.shoots = []; this.pattern = 0; this.setCanvas(this.canvas_selector); this.set_imageMax(); this.event_set(); this.bitmap_load(); this.view(); this.animation_roop(30); }; MAIN.prototype.setCanvas = function(selector){ this.canvas_elm = d.querySelector(selector); if(!this.canvas_elm){return;} this.canvas_elm.style.setProperty("background-color","black",""); 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.bitmap_load = function(){ var bitmap_files = []; bitmap_files.push(__options.cannon.src); bitmap_files.push(__options.uso.src); bitmap_files.push(__options.bullet.src); for(var i in __options.invader_src){ for(var j=0; j<__options.invader_src[i].length; j++){ bitmap_files.push(__options.invader_src[i][j]); } } for(var i=0; i<bitmap_files.length; i++){ this.image_bit_set(bitmap_files[i]); } }; MAIN.prototype.view = function(){ // invader this.view_invader(); // canon this.image(__options.cannon , "cannon"); } MAIN.prototype.view_invader = function(){ if(typeof __options.invaders === "undefined"){ this.init_invaders_info(); } for(var i in __options.invaders){ for(var j in __options.invaders[i]){ this.image(__options.invaders[i][j][this.pattern]); } } }; MAIN.prototype.init_invaders_info = function(){ // 配置マージン margin = 10; // 配置個数(w) var count_w = Math.floor((__options.canvas_size.x - margin * 2) / __options.bitmap_size.w) -1; // 敵機配置情報作成 __options.invaders = []; for(var i=0; i<__options.invader_row_infos.length; i++){ var type = __options.invader_row_infos[i].invader; __options.invaders[i] = []; var srcs = __options.invader_src[type]; for(var j=0; j<count_w; j++){ __options.invaders[i][j] = []; for(var k=0; k<srcs.length; k++){ // var options = JSON.parse(JSON.stringify(__options.invader[type][k])); __options.invaders[i][j][k] = { color : __options.invader_row_infos[i].color, x : (j * __options.bitmap_size.w) + margin, y : (i * __options.bitmap_size.h) + margin, type : type, src : srcs[k] }; } } } // return options_invaders; // console.log(__options.invaders); }; 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"){ this.image_bit_make(options); } }; // MAIN.prototype.image_file_set = function(options , key){ // this.image_cache[key] = new Image(); // var img = this.image_cache[key]; // img.src = options.src; // img.onload = (function(options){ // this.image_draw(options , key); // }).bind(this , options); // }; MAIN.prototype.image_bit_set = function(src){ this.image_cache[src] = { bitsize : 0, bitmap : [] }; new AJAX({ url : src, methdo : "GET", async : true, onSuccess : (function(src , data){ var char16 = data.split("\n"); 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("") // サイズ取得 this.image_cache[src].bitsize = __options.bitmap_size.w / str_count; char2 = (zero + char2).slice(-str_count); this.image_cache[src].bitmap.push(char2); } // if(flg === "view"){console.log(options); // this.image_bit_make(options); // } }).bind(this , src) }); }; MAIN.prototype.image_bit_make = function(options){ if(typeof this.image_cache[options.src] === "undefined" || typeof this.image_cache[options.src].bitmap === "undefined" || typeof this.image_cache[options.src].bitsize === "undefined"){return;} var bitmap = this.image_cache[options.src].bitmap; var bitsize = this.image_cache[options.src].bitsize; this.ctx.fillStyle = options.color; this.ctx.strokeStyle = null; this.ctx.strikeWidth = 0; for(var i=0; i<bitmap.length; i++){ for(var j=0; j<bitmap[i].length; j++){ var bit = bitmap[i].charAt(j); if(bit == 0){continue;} var w = bitsize; var h = 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 , key){ // if(typeof this.image_cache[key] !== "object"){return} // var img = this.image_cache[key]; // 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); } // anim_flg = setTimeout(func , 3000); }; 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; } this.animation_shoot(); }; MAIN.prototype.animation_shoot = function(){ if(!this.shoots.length){return;} 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 - __options.cannon.bitSize , 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.bitmap_size.w){ pos = this.canvas_elm.offsetWidth - __options.bitmap_size.w; } __options.cannon.x = pos; }; MAIN.prototype.animation = function(){ this.clear(); this.nextPattern(300); this.view(); 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_src){ this.image_max = __options.invader_src[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} this.shoot_add(); }; MAIN.prototype.shoot_add = function(){ if(this.shoots.length < 2){ this.shoots.push({ x : __options.cannon.x + (__options.bitmap_size.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; case 32: // space this.shoot_add(); 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){ 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);

解説

これまでのソースから思いっきり書き直してしまいました。 想定はしていたんですが、インベーダー(敵機)をロジック的に配置するのに、ちゃんとキャッシュデータで管理しなければいけなかったからと、 __optionsのデータの持ち方が効率的でなかったので、その仕組も含めて書き直しを行いました。 そのおかげで、無駄なメモリを使わずにすんだので、結果オーライです。 次回は、自機を守る壁(トーチカ)を設置して、より完成形に近づけます。

Github

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

人気の投稿

このブログを検索

ごあいさつ

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

ブログ アーカイブ