ゲームライブラリ構築までの道「ブロック崩し編」#7 : CPU負荷対応

2020年7月10日

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

自分の作りたいものが好きに作れる事に幸せを感じる、ユゲタです。 プログラミングで、人から「こんなの作れる?」と聞かれたら、すぐにその要件のモノを作ることができると、仕事としても、信頼されて依頼されるようになります。 自分としても、言われることで人の役に立つ成果物が作れるというモチベーションと、自分の知らなかった知識が必ず何かしら存在するので、確実にスキルアップできるという点で、 エンジニアという職業は、やればやるほど、成長していくものだと、感じますね。 でも、このブログは誰からも言われていなくて、自分で研究しているだけのブログですが、今日もブロック崩し、作っちゃいますよ。

本日のIT謎掛け

「プログラミングのスキルアップ」と、かけまして・・・ 「旅行先の山道でドライブ」と、ときます。 そのココロは・・・ 未知の領域に進んでいくことで、素早く到達します。

ソースコード

(function(){ var __options = { canvas : "#mycanvas", width : 400, height : 600, wall : { w : 400 , h : 600 , color_stroke : "transparent" , color_fill : "#382B8C" , size : 20 }, ball : { x : 50 , y : 50 , r : 6, color_stroke : "transparent", color_fill : "#F2B5A7" , moveX : 4 , moveY : 4 }, bar : { w : 60 , h : 10 , color_stroke : "transparent" , color_fill : "#958ABF" , moveX : 12 }, dialog : { color_fill : "white", color_stroke : "#382B8C", width_stroke : 2, text_color : "#382B8C", text_size : 16, text_width : 0.9, text_font : "sans-serif", radius : 4, w : 0.7, h : 0.3 }, block : { w : 50 , h : 15 , color_stroke : "transform", color_fill : "#958ABF", row : 6, col : 6, gap_x : 5, gap_y : 10, margin_top : 59, margin_left : 15, area_y : 0.4, collision_col : "red" } }; var MAIN = function(){ if(!this.check()){ alert("htmlに指定のcanvasがありません。"); return; } this.init(); this.draw(true); this.animation_set(); this.event_set(); this.game_start(); }; MAIN.prototype.init = function(){ this.data_reset(); // 画面サイズ調整 var canvas = this.canvas; canvas.setAttribute("width" , this.options.wall.w); canvas.setAttribute("height" , this.options.wall.h); }; MAIN.prototype.dialog_window = function(o){ var w = o.wall.w * o.dialog.w, h = o.wall.h * o.dialog.h, x = o.wall.w / 2 * (1 - o.dialog.w), y = o.wall.w / 2 * (1 - o.dialog.h), r = o.dialog.radius; h = h > 180 ? h : 180; this.ctx.fillStyle = o.dialog.color_fill; this.ctx.strokeStyle = o.dialog.color_stroke; this.ctx.lineWidth = o.dialog.width_stroke * 2; this.ctx.beginPath(); this.ctx.moveTo(x,y + r); this.ctx.arc(x+r , y+h-r , r , Math.PI , Math.PI*0.5 , true); this.ctx.arc(x+w-r , y+h-r , r , Math.PI*0.5,0 , 1); this.ctx.arc(x+w-r , y+r , r , 0 , Math.PI*1.5 , 1); this.ctx.arc(x+r , y+r , r , Math.PI*1.5 , Math.PI , 1); this.ctx.closePath(); this.ctx.stroke(); this.ctx.fill(); } MAIN.prototype.dialog_text = function(o , texts){ var w = (o.wall.w * o.dialog.w) * o.dialog.text_width; var x = o.wall.w / 2; var y = o.wall.w / 2 * (1 - o.dialog.h) + 30; var text_y = y; for(var i=0; i<texts.length; i++){ this.ctx.fillStyle = texts[i].color || o.dialog.text_color; this.ctx.textAlign = texts[i].align || "center"; this.ctx.textBaseline = texts[i].baseline || "top"; var font_size = texts[i].size || o.dialog.text_size; var font_weight = texts[i].weight || ""; this.ctx.font = font_weight +" "+ font_size+"px '"+ o.dialog.text_font +"' "; this.ctx.fillText(texts[i].text , x, text_y , w); text_y += font_size + (texts[i].margin || 10); } }; MAIN.prototype.game_start = function(){ this.dialog_window(this.options); this.dialog_text(this.options , [ {text:"壁打ちブロック" , size:30 , margin:30 , weight:"bold"}, {text:"画面をクリックすると"}, {text:"ゲームが開始します"} ]); }; MAIN.prototype.game_over = function(){ this.flg_gamestart = false; this.data_reset(); this.dialog_window(this.options); this.dialog_text(this.options , [ {text:"Game over", size:30 , margin:30 , color:"red" , weight:"bold"}, {text:"画面をクリックすると"}, {text:"ゲームが開始します"} ]); }; MAIN.prototype.data_reset = function(){ this.options = JSON.parse(JSON.stringify(__options)); // 画面サイズ調整 this.options.wall.w = window.innerWidth < this.options.wall.w ? window.innerWidth : this.options.wall.w; this.options.wall.h = window.innerHeight < this.options.wall.h ? window.innerHeight : this.options.wall.h; // blockサイズ調整 var w = this.options.wall.w - (this.options.wall.size*2) - this.options.block.margin_left; this.options.block.col = Math.round(w / (this.options.block.w + (this.options.block.gap_x * 2))); this.options.block.margin_left = (w - (this.options.block.col * this.options.block.w) - this.options.block.gap_x) * 0.5; this.options.wall.margin_top = (this.options.wall.h - this.options.wall.size) * 0.1; var h = (this.options.wall.h - this.options.wall.size - this.options.wall.margin_top) * this.options.block.area_y; this.options.block.row = Math.round(h / (this.options.block.h + this.options.block.gap_y)); // ボール初期値(画面の中心地) this.options.ball.x = this.options.wall.w * 0.5; this.options.ball.y = this.options.wall.h * 0.5; }; MAIN.prototype.getCanvas = function(){ if(typeof this.canvas === "undefined"){ this.canvas = document.querySelector(__options.canvas); } return this.canvas; }; MAIN.prototype.getContext = function(){ if(typeof this.ctx === "undefined"){ var canvas = this.getCanvas(); if(!canvas){return null;} this.ctx = canvas.getContext("2d"); } return this.ctx; }; MAIN.prototype.check = function(){ var ctx = this.getContext(); if(ctx){ return true; } else{ return false; } }; MAIN.prototype.ctx_clear = function(){ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); }; MAIN.prototype.draw = function(flg){ if(!flg && this.flg_gamestart !== true){return;} this.ctx_clear(); this.draw_wall(this.options.wall); this.draw_bar( this.options.wall , this.options.bar); this.draw_ball(this.options.wall , this.options.ball); this.draw_block(this.options.wall , this.options.block); }; MAIN.prototype.draw_wall = function(ow){ var ctx = this.ctx; ctx.strokeStyle = ow.color_stroke; ctx.strokeWidth = ow.color_stroke === "transparent" ? 0 : 1; ctx.fillStyle = ow.color_fill; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(ow.w , 0); ctx.lineTo(ow.w , ow.h); ctx.lineTo(ow.w - ow.size , ow.h); ctx.lineTo(ow.w - ow.size , ow.size); ctx.lineTo(ow.size , ow.size); ctx.lineTo(ow.size , ow.h); ctx.lineTo(0 , ow.h); ctx.lineTo(0 , 0); ctx.stroke(); ctx.fill(); }; MAIN.prototype.draw_bar = function(ow,ob){ this.ctx.strokeStyle = ob.color_stroke; this.ctx.strokeWidth = ob.color_stroke === "transparent" ? 0 : 1; this.ctx.fillStyle = ob.color_fill; ob.x = ob.x ? ob.x : (ow.w - ob.w) / 2; ob.y = ow.h - ob.h - 50; this.ctx.fillRect(ob.x , ob.y , ob.w , ob.h); }; MAIN.prototype.draw_ball = function(ow,ob){ this.ctx.strokeStyle = ob.color_stroke; this.ctx.strokeWidth = ob.color_stroke === "transparent" ? 0 : 1; this.ctx.fillStyle = ob.color_fill; this.ctx.beginPath(); ob.x = ob.x || ow.w / 2; ob.y = ob.y || ow.h / 2; this.ctx.arc( ob.x , ob.y , ob.r, 0, Math.PI * 2 ); this.ctx.fill(); }; MAIN.prototype.draw_block = function(wall , block){ this.ctx.strokeStyle = block.color_stroke; this.ctx.strokeWidth = block.color_stroke === "transparent" ? 0 : 1; this.ctx.fillStyle = block.color_fill; var buffer = []; for(var i=0; i<block.row; i++){ if(typeof buffer[i] === "undefined"){buffer[i] = [];} for(var j=0; j<block.col; j++){ var x = block.margin_left + wall.size + (block.w * j) + (block.gap_x * j); var y = block.margin_top + wall.size + (block.h * i) + (block.gap_y * i); if(typeof block.buffer === "undefined"){ buffer[i][j] = {x : x , y : y}; this.ctx.fillRect(x , y , block.w , block.h); } else if(block.buffer[i][j].collision === true){ continue; // this.ctx.fillStyle = block.collision_col; // this.ctx.fillRect(x , y , block.w , block.h); // this.ctx.fillStyle = block.color_fill; } else{ this.ctx.fillRect(x , y , block.w , block.h); } // if(typeof block.buffer === "undefined"){ // buffer[i][j] = {x : x , y : y}; // this.ctx.fillRect(x , y , block.w , block.h); // } // else if(typeof block.buffer !== "undefined" // && typeof block.buffer[j] !== "undefined" // && block.buffer[j][i]){ // this.ctx.fillRect(x , y , block.w , block.h); // } } } if(typeof block.buffer === "undefined"){ block.buffer = buffer; } }; MAIN.prototype.animation_set = function(){ if(this.flg_gamestart !== true){return;} new LIB().anim((function(e){this.animation(e)}).bind(this)); }; MAIN.prototype.animation = function(timestamp){ if(this.flg_gamestart !== true){return;} if (!this.time_start){this.time_start = timestamp;} // debug用(play上限秒数) // if(timestamp - this.time_start > 30000){return;} // keydown-bar-move switch(this.keydown_flg){ case "right": this.bar_move(this.options.bar.x + this.options.bar.moveX); break; case "left": this.bar_move(this.options.bar.x - this.options.bar.moveX); break; } this.ball_move(); this.draw(); this.animation_set(); }; MAIN.prototype.ball_move = function(){ if(this.flg_gamestart !== true){return;} this.options.ball.x += this.options.ball.moveX; this.options.ball.y += this.options.ball.moveY; this.collision_wall(this.options.wall , this.options.ball); this.collision_bar(this.options.bar , this.options.ball); this.collision_block(this.options.block , this.options.ball); }; // 当たり判定(壁) MAIN.prototype.collision_wall = function(ow , ob){ // <- : left if(ob.x - ob.r < ow.size){ ob.x = ow.size + ob.r; ob.moveX = ob.moveX * -1; } // ^ : top if(ob.y - ob.r < ow.size){ ob.y = ow.size + ob.r; ob.moveY = ob.moveY * -1; } // -> : right if(ob.x + ob.r > ow.w - ow.size){ ob.x = ow.w - ow.size - ob.r; ob.moveX = ob.moveX * -1; } // v : bottom (game-over) if(ob.y + ob.r > ow.h){ this.game_over(); } } // 当たり判定(ラケット) MAIN.prototype.collision_bar = function(bar , ball){ // ボールが上移動の場合は処理対象外 if(ball.moveY < 0){return;} // ball-direct-under (正反射) if(ball.y + ball.r > bar.y && ball.y + ball.r < bar.y + bar.h && ball.x > bar.x && ball.x < bar.x + bar.w){ ball.moveY = ball.moveY * -1; } // 左角判定(ボールと角との距離がボール半径以下の判定) else if(Math.sqrt(Math.pow(bar.x - ball.x, 2) + Math.pow(bar.y - ball.y, 2)) <= ball.r){ if(ball.moveX > 0){ this.calc_angle(bar , ball); } else{ ball.moveY = ball.moveY * -1; } } // 右角判定(ボールと角との距離がボール半径以下の判定) else if(Math.sqrt(Math.pow((bar.x + bar.w) - ball.x, 2) + Math.pow(bar.y - ball.y, 2)) <= ball.r){ if(ball.moveX < 0){ this.calc_angle(bar , ball); } else{ ball.moveY = ball.moveY * -1; } } }; // 当たり判定(ブロック) MAIN.prototype.collision_block = function(block , ball){ if(typeof block.buffer === "undefined"){return;} for(var i=0; i<block.row; i++){ if(typeof block.buffer[i] === "undefined"){continue;} for(var j=0; j<block.col; j++){ if(!block.buffer || typeof block.buffer[i][j] === "undefined" || !block.buffer[i][j] || block.buffer[i][j].collision === true){continue;} // 左右正反射 if(ball.x + ball.r > block.buffer[i][j].x && ball.x - ball.r < block.buffer[i][j].x + block.h && ball.y > block.buffer[i][j].y && ball.y < block.buffer[i][j].y + block.h){ block.buffer[i][j].collision = true; // ボール座標を調整 if(ball.moveX < 0){ ball.x = block.buffer[i][j].x + block.w + ball.r; } else{ ball.x = block.buffer[i][j].x - ball.r; } // 方向転換 ball.moveX = ball.moveX * -1; return; } // 上下正反射 if(ball.y + ball.r > block.buffer[i][j].y && ball.y - ball.r < block.buffer[i][j].y + block.h && ball.x > block.buffer[i][j].x && ball.x < block.buffer[i][j].x + block.w){ block.buffer[i][j].collision = true; // ボール座標を調整 if(ball.moveY < 0){ ball.y = block.buffer[i][j].y + block.h + ball.r; } else{ ball.y = block.buffer[i][j].y - ball.r; } // 方向転換 ball.moveY = ball.moveY * -1; return; } // 左上角判定 else if(Math.pow(block.buffer[i][j].x - ball.x, 2) + Math.pow(block.buffer[i][j].y - ball.y, 2) <= ball.r){ block.buffer[i][j].collision = true; if(ball.moveX > 0 && ball.moveY > 0){ ball.moveX = ball.moveX * -1; ball.moveY = ball.moveY * -1; } else if(ball.moveX < 0 && ball.moveY > 0){ ball.moveY = ball.moveY * -1; } else if(ball.moveX > 0 && ball.moveY < 0){ ball.moveX = ball.moveX * -1; } return; } // 右上角判定 else if(Math.sqrt(ball.x - block.buffer[i][j].x - block.w, 2) + Math.pow(block.buffer[i][j].y - ball.y, 2) <= ball.r){ block.buffer[i][j].collision = true; if(ball.moveX < 0 && ball.moveY > 0){ ball.moveX = ball.moveX * -1; ball.moveY = ball.moveY * -1; } else if(ball.moveX > 0 && ball.moveY > 0){ ball.moveY = ball.moveY * -1; } else if(ball.moveX < 0 && ball.moveY < 0){ ball.moveX = ball.moveX * -1; } return; } // 左下角判定 else if(Math.sqrt(block.buffer[i][j].x - ball.x, 2) + Math.pow(ball.y - block.buffer[i][j].y + block.h , 2) <= ball.r){ block.buffer[i][j].collision = true; if(ball.moveX > 0 && ball.moveY < 0){ ball.moveX = ball.moveX * -1; ball.moveY = ball.moveY * -1; } else if(ball.moveX < 0 && ball.moveY < 0){ ball.moveY = ball.moveY * -1; } else if(ball.moveX > 0 && ball.moveY > 0){ ball.moveX = ball.moveX * -1; } return; } // 右下角判定 else if(Math.sqrt(ball.x - block.buffer[i][j].x - block.w, 2) + Math.pow(ball.y - block.buffer[i][j].y + block.h , 2) <= ball.r){ block.buffer[i][j].collision = true; if(ball.moveX < 0 && ball.moveY < 0){ ball.moveX = ball.moveX * -1; ball.moveY = ball.moveY * -1; } else if(ball.moveX > 0 && ball.moveY < 0){ ball.moveY = ball.moveY * -1; } else if(ball.moveX < 0 && ball.moveY > 0){ ball.moveX = ball.moveX * -1; } return; } } } } MAIN.prototype.calc_angle = function(bar,ball){ // // 現在のボールの進行角度 // var angle1 = Math.atan2(Math.abs(ball.moveY) , Math.abs(ball.moveX)) * 180 / Math.PI; // // 角座標とボール中心地の角度 // var angle2 = Math.atan2(bar.y - ball.y , bar.x - ball.x) * 180 / Math.PI; // // 誤差角度を反転 // angle1 += -(angle1 - angle2); // // 角度を反映 // ball.moveY = Math.sin(angle1) * ball.r; // ball.moveX = Math.cos(angle1) * ball.r; // 反転 ball.moveX = -ball.moveX; ball.moveY = -ball.moveY; // console.log(angle1 +":"+ ball.moveX +"/"+ ball.moveY); // console.log(angle1 +"/"+ angle2 +":"+Math.abs(ball.moveY)+"/"+Math.abs(ball.moveX)); }; MAIN.prototype.event_set = function(){ new LIB().event(window , "click" , (function(e){this.click(e)}).bind(this)); new LIB().event(window , "keydown" , (function(e){this.keydown(e)}).bind(this)); new LIB().event(window , "keyup" , (function(e){this.keyup(e)}).bind(this)); new LIB().event(window , "mousemove" , (function(e){this.mousemove(e)}).bind(this)); new LIB().event(window , "touchmove" , (function(e){this.touchmove(e)}).bind(this)); new LIB().event(window , "touchend" , (function(e){this.touchend(e)}).bind(this)); }; MAIN.prototype.click = function(e){ if(this.flg_gamestart === true){return} this.flg_gamestart = true; this.animation_set(); }; MAIN.prototype.bar_move = function(bar_x){ if(bar_x < this.options.wall.size){ bar_x = this.options.wall.size; } if(bar_x + this.options.bar.w > this.options.wall.w - this.options.wall.size){ bar_x = this.options.wall.w - this.options.wall.size - this.options.bar.w; } this.options.bar.x = bar_x; }; 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.bar_move(this.options.bar.x + (e.clientX - this.mousePos)); this.mousePos = e.clientX; this.draw(); }; 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.bar_move(this.options.bar.x + (e.touches[0].clientX - this.mousePos)); this.mousePos = e.touches[0].clientX; this.draw(); }; 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)})} }; LIB.prototype.anim = function(func , time){ if(window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame){ window.requestAnimationFrame(func); } else{ time = time || 0; anim_flg = setTimeout(func , time); } }; LIB.prototype.construct = function(){ switch(document.readyState){ case "complete" : new MAIN();break; case "interactive" : this.event(window , "DOMContentLoaded" , (function(){new MAIN()}).bind(this));break; default : this.event(window , "load" , (function(){new MAIN()}).bind(this));break; } }; new LIB().construct(); })();

本日のまとめ

今回のポイントは、ブロックの配置情報をメモリ(buffer)として保持しておき、それらを計算で当たり判定を行う処理です。 もちろん、ブロックの数が増えると、処理が重くなってきて、CPU負荷が高くなってしまいますが、表示サイズに応じたCPUで、今のところはなんとか大丈夫なレベルの負荷のようです。 ブロック崩しは、制作プログラマによって、若干のルールが違っているのが作ってみてよく分かりました。 ブロックの横面に当たったら、必ず下方向を向くようにすることで、 かんたんに上段の連続モードに持って行きづらくするようにできたり、 2つのブロックを同時に消したりはできないようにしたり、 様々な詳細ルールを設けることにより、ゲーム性の向上につながるのが理解できます。 とりあえず、ブロック崩しは、今回で終了になります。 全体のソースは、githubにpushしているので、そちらから取得してください。 https://github.com/yugeta/game_block

このブログを検索

ごあいさつ

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