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

2020年7月10日

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

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

本日の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

このブログを検索

プロフィール

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

ブログ アーカイブ

QooQ