Webブラウザで簡単ゲームを作ってみよう: 壁打ちテニス編 #8「ボールの動き」

2021年10月14日

テクノロジー

eyecatch 実は今、成田空港の待合室で、2時間ほど時間が余っていたので、ブログを1記事書こうとしている、ユゲタです。 ブログの記事を書くのって、下記なれると、電車でもどこでも書けるし、書く内容って、自分のメモ帳に山のようにアイデアメモがあるので、 ゆっくり腰を据えて書けるスペースがあれば、安定した思考で書けるというモノですね。 偉そうに言ってますが、今回のようなプログラミング記事は、めちゃくちゃ事前にプログラムを作って、 ミスがないか、デバッグをしまくって、ヒーヒー言いながら書いていたりもしますwww ・・・が、それがまた楽しいというMっ気のある自分に気がつくポイントでもあるので、まずはやったもん勝ちという事なんですよね〜。 そして、今回の壁打ちテニスは、いよいよボールを動かしてみます。 というか、処理としては、これでほぼ終わりになるので、もはや終盤ですね。

ソースコード

html,body{ width:100%; height:100%; padding:0; margin:0; outline:0; border:0; } h1{ text-align:center; width:90%; margin:10px auto; background-color:#eee; padding:5px; font-weight:bold; font-size:24px; } :root{ --color-draw:black; } #tennis.full{ width:90%; height:calc(90% - 50px); } #tennis{ width:300px; height:300px; margin-bottom:100px; position:relative; margin-left:50%; transform:translate(-50%,0); background-color:#eee; } #racket{ width:50px; height:5px; background-color:var(--color-draw); position:absolute; bottom:20px; left:100px; } #ball{ width:10px; height:10px; border-radius:50%; background-color:var(--color-draw); position:absolute; top:50%; left:30%; } (function(){ function MAIN(){ this.get_racket(); this.get_ball(); this.set_space(); this.set_event(); } MAIN.prototype.get_tennis = function(){ if(!this.tennis){ this.tennis = document.getElementById("tennis"); } return this.tennis; }; MAIN.prototype.get_racket = function(){ if(!this.racket){ this.racket = document.getElementById("racket"); } return this.racket; }; MAIN.prototype.get_ball = function(){ if(!this.ball){ this.ball = document.getElementById("ball"); } return this.ball; }; MAIN.prototype.set_space = function(){ let tennis = this.get_tennis(); this.space = tennis.getBoundingClientRect(); }; MAIN.prototype.set_event = function(){ // キーボード操作 window.addEventListener("keydown" , this.keydown.bind(this)); window.addEventListener("keyup" , this.keyup.bind(this)); // マウス操作 window.addEventListener("mousemove" , this.mousemove.bind(this)); // スマホ操作 window.addEventListener("touchstart" , this.touchstart.bind(this)); window.addEventListener("touchmove" , this.touchmove.bind(this)); window.addEventListener("touchend" , this.touchend.bind(this)); // ゲームスタート this.tennis.addEventListener("click" , this.ball_start.bind(this)); }; MAIN.prototype.keydown = function(e){ // 押しっぱなし防止 if(e.repeat === true){return;} this.racket_offset = 0; switch(this.keyType(e.keyCode)){ case "left": // < this.racket_offset = -1; break; case "right": // > this.racket_offset = +1; break; } this.racket_move(); }; MAIN.prototype.keyup = function(e){ if(this.racket_flg){ clearTimeout(this.racket_flg); delete this.racket_flg; } }; MAIN.prototype.racket_move = function(){ let offset = this.racket_offset; if(!offset){return;} let x = this.get_racket_x(); let min = 0; let max = this.space.width - this.racket.offsetWidth; x = x + (offset * 10); if(x < min){x = 0;} else if(x > max){x = max;} this.racket.style.setProperty("left" , x +"px" , ""); this.racket_flg = setTimeout(this.racket_move.bind(this) , 10); if(this.mouse_pos){ delete this.mouse_pos; } }; // lib MAIN.prototype.keyType = function(keycode){ switch(keycode){ case 0: return "¥"; case 8: return "backspace"; case 9: return "tab"; case 13: return "return"; case 16: return "shift"; case 18: return "option"; case 32: return "space"; case 37: return "left"; case 38: return "up"; case 39: return "right"; case 40: return "down"; case 48: return "0"; case 49: return "1"; case 50: return "2"; case 51: return "3"; case 52: return "4"; case 53: return "5"; case 54: return "6"; case 55: return "7"; case 56: return "8"; case 57: return "9"; case 65: return "a"; case 66: return "b"; case 67: return "c"; case 68: return "d"; case 69: return "e"; case 70: return "f"; case 71: return "g"; case 72: return "h"; case 73: return "i"; case 74: return "j"; case 75: return "k"; case 76: return "l"; case 77: return "m"; case 78: return "n"; case 79: return "o"; case 80: return "p"; case 81: return "q"; case 82: return "r"; case 83: return "s"; case 84: return "t"; case 85: return "u"; case 86: return "v"; case 87: return "w"; case 88: return "x"; case 89: return "y"; case 90: return "z"; case 91: return "command"; case 187: return "^"; case 189: return "-"; } return null; }; MAIN.prototype.get_racket_x = function(){ let left = document.defaultView.getComputedStyle(this.racket,'').getPropertyValue("left"); return left ? Number(left.replace("px" , "")) : 0; }; MAIN.prototype.mousemove = function(e){ if('ontouchstart' in window === true){return;} if(typeof this.mouse_pos === "undefined"){ this.mouse_pos = e.pageX; } let base_offset = -this.space.left; this.racket_mouse_move(e.pageX + base_offset); }; MAIN.prototype.racket_mouse_move = function(x){ let min = 0; let max = this.space.width - this.racket.offsetWidth; if(x < min){x = 0;} else if(x > max){x = max;} this.racket.style.setProperty("left" , x +"px" , ""); this.mouse_pos = x; }; MAIN.prototype.touchstart = function(e){ if('ontouchstart' in window === false){return;} if(!e.touches || !e.touches.length){return;} this.touch_pos = e.touches[0].pageX; }; MAIN.prototype.touchmove = function(e){ if(typeof this.touch_pos === "undefined"){return} let touch_move_pos = e.touches[0].pageX; let diff = touch_move_pos - this.touch_pos; let x = this.get_racket_x(); this.racket_mouse_move(x + diff); this.touch_pos = touch_move_pos; }; MAIN.prototype.touchend = function(e){ if(typeof this.touch_pos !== "undefined"){ delete this.touch_pos; } }; // ボール移動開始 MAIN.prototype.ball_start = function(){ if(this.start){return;} this.start = true; let ball_distance = 1; this.ball_move(ball_distance); }; // ボール移動処理 MAIN.prototype.ball_move = function(ball_distance){ let ball_elm = this.get_ball(); if(!ball_elm){return;} // 現在のボールの位置を取得 let ball_pos_current = this.get_ball_pos_current(ball_elm); // 移動先の位置を取得 let ball_pos_next = this.get_ball_pos_next(ball_pos_current , ball_distance); // 壁を超えている場合は、壁の位置までの距離に修正、通常の場合はnext-posを採用 let ball_pos = this.get_ball_pos(ball_pos_current , ball_pos_next); // game-over if(!ball_pos){return;} // 表示処理 ball_elm.style.setProperty("left" , ball_pos.x +"px" , ""); ball_elm.style.setProperty("top" , ball_pos.y +"px" , ""); setTimeout(this.ball_move.bind(this , ball_distance) , 10); }; MAIN.prototype.get_ball_pos = function(ball_pos_current , ball_pos_next){ let collision = this.ball_collision(ball_pos_next); let max,rate,diff; switch(collision){ case "bottom": return null; case "top": max = 0; rate = (max - ball_pos_current.y) / (ball_pos_next.y - ball_pos_current.y); diff = ball_pos_next.x - ball_pos_current.x; return { x : ball_pos_next.x + (diff * rate), y : max }; case "right": max = this.space.width - this.ball.offsetWidth; rate = (max - ball_pos_current.x) / (ball_pos_next.x - ball_pos_current.x); diff = ball_pos_next.y - ball_pos_current.y; return { x : max, y : ball_pos_next.y + (diff * rate) }; case "left": max = 0; rate = (max - ball_pos_current.x) / (ball_pos_next.x - ball_pos_current.x); diff = ball_pos_next.y - ball_pos_current.y; return { x : max, y : ball_pos_next.y + (diff * rate) }; case "racket": let racket_pos = this.get_racket_pos(); max = racket_pos.y - this.ball.offsetHeight; rate = (max - ball_pos_current.y) / (ball_pos_next.y - ball_pos_current.y); diff = ball_pos_next.x - ball_pos_current.x; return { x : ball_pos_next.x + (diff * rate), y : max }; default: return ball_pos_next; } }; MAIN.prototype.get_ball_pos_next = function(ball_pos_current , ball_distance){ if(!this.ball_direction){ this.ball_direction = { x : 1, y : -1 }; } else{ let collision = this.ball_collision(ball_pos_current); this.ball_direction = this.get_ball_direction(collision); } return { x : ball_pos_current.x + this.ball_direction.x * ball_distance, y : ball_pos_current.y + this.ball_direction.y * ball_distance }; }; // ボールの進行方向決定 {x:* , y:*} MAIN.prototype.get_ball_direction = function(collision){ let direction = this.ball_direction; switch(collision){ case "top": direction.y *= -1; break; case "left": case "right": direction.x *= -1; break; case "top-left": case "top-right": direction.y *= -1; direction.x *= -1; case "bottom": return null; case "racket": direction.y *= -1; } return direction; }; // ボールの当たり判定 MAIN.prototype.ball_collision = function(ball_pos){ // check-top if(ball_pos.y <= 0){ return "top"; } // check-right else if(ball_pos.x + this.ball.offsetWidth >= this.space.width){ return "right"; } // check-left else if(ball_pos.x <= 0){ return "left"; } // check-bottom else if(ball_pos.y >= this.space.height){ return "bottom"; } // racket let racket_pos = this.get_racket_pos(); if(ball_pos.y + this.ball.offsetHeight >= racket_pos.y && ball_pos.x + this.ball.offsetWidth >= racket_pos.x && ball_pos.x <= racket_pos.x + racket_pos.w){ return "racket"; } }; // ボールの中心点と関連情報を返す MAIN.prototype.get_ball_pos_current = function(ball_elm){ if(!ball_elm){return;} let left = document.defaultView.getComputedStyle(ball_elm,'').getPropertyValue("left"); let top = document.defaultView.getComputedStyle(ball_elm,'').getPropertyValue("top"); let x = left ? Number(left.replace("px" , "")) : 0; let y = top ? Number(top.replace("px" , "")) : 0; return { x : x, y : y }; }; MAIN.prototype.get_racket_pos = function(){ let left = document.defaultView.getComputedStyle(this.racket,'').getPropertyValue("left"); let top = document.defaultView.getComputedStyle(this.racket,'').getPropertyValue("top"); return { x : left ? Number(left.replace("px" , "")) : 0, y : top ? Number(top.replace("px" , "")) : 0, w : this.racket.offsetWidth, h : this.racket.offsetHeight }; }; switch(document.readyState){ case "complete" : new MAIN();break; default : window.addEventListener("load" , (function(options){new MAIN()}).bind(this));break; } })();

デモ

解説

今回は、jsだけじゃなく、cssファイルも少しだけ変更をしています。 壁の厚みが、ボールの跳ね返り判定で複雑になるので、一旦取り除いて処理をするようにしました。 壁打ちテニスの画面をクリックすると、ボールが動き始めます。 処理としては、ボールの動く方向を決めるのと、壁の当たり判定を行っていますが、少しだけ複雑な処理になっているのは、 ボールが壁にあたって跳ね返る時に、ボールのエレメントサイズの分調整をしなければいけないのと、ボールの移動距離を壁にピッタリと合わせる処理を加えています。 もう少し簡単な書き方もできると思った方は、是非、プログラミングに挑戦してみてください。 あと、ラケットに当たった場所で、ボールの移動する角度が変わるようにすると、ゲーム性が複雑になって面白くなるかもしれません。 このへんもお試しあれ。

壁打ちテニスのまとめ

とりあえず、今回で一通りの処理が入ったので、このシリーズの終了になります。 簡単と思っていざ作ってみると、思いの外、たくさんの行をプログラミングしたと、自分で関心してしまいましたが、 こういう、処理をたくさん作れば作るほど、スキルがアップすると思えば、やってみる価値はあると思いますよ。 ユゲタのソースコードをコピペするだけでなく、自分でイチから打ち込んでみることをオススメしたいと思います。

人気の投稿

このブログを検索

ごあいさつ

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

ブログ アーカイブ