[Javascript] パックマンをHTMLで作ってみるブログ#7 壁のコリジョン判定

2023年6月21日

Javascript

eyecatch 自機を動かせるようになって、臨場感が増してきたと思います。 今回はステージ内の壁を判定して、衝突判定(コリジョン)を組み込んでみます。 ステージ内を壁に沿って動けるようになると、それだけでもパックマンゲームの醍醐味が味わえてしまいます。

今回の目的

- ステージの壁判定マップ作成 - キャラクター(自機)座標と壁マップの判定処理 - キャラクター(自機)移動処理の判定追加 - キャラクター(自機)が壁に当たるまで動き続ける処理 - 連続カーソル切り替え対応

対象ファイル一覧

  1. js/control.js
  2. js/frame.js
  3. js/pacman.js

ソースコード

カーソルキーのイベントコントロールをする処理を書き換えました。 部分的なんですが、保持するオブジェクトの仕様を変更したので、全部入れ替えて下さい。

[全部更新] js/control.js

import { Pacman } from './pacman.js' export class Control{ constructor(){ window.addEventListener('keydown' , this.keydown.bind(this)) window.addEventListener('keyup' , this.keyup.bind(this)) } static key2name(key){ switch(key){ case 37 : return 'left' case 38 : return 'up' case 39 : return 'right' case 40 : return 'down' } } keydown(e){ if(e.repeat === true){return} const key = e.keyCode const name = Control.key2name(key) if(!name){return} Control.direction = name Pacman.move(Control.direction) } keyup(e){ if(!Control.direction){return} const name = Control.key2name(e.keyCode) if(Control.direction !== name){return} delete Control.direction } }

[全部更新] js/frame.js

壁の判定をするためのオブジェクトを作る、set_collision()という関数を作って、座標からコリジョン判定をする処理を追加しました。 export class Frame{ constructor(){ return new Promise(resolve => { this.resolve = resolve Frame.stage_datas = this.stage_datas = [] this.load_asset() }) } static get root(){ return document.querySelector(`.frame-area`) } get block_size(){ return Frame.block_size } static get block_size(){ const s5 = document.querySelector('.S5') return s5.offsetWidth } get cols_count(){ return ~~(Frame.root.offsetWidth / Frame.block_size) } load_asset(){ const xhr = new XMLHttpRequest() xhr.open('get' , `assets/frame.json` , true) xhr.setRequestHeader('Content-Type', 'text/html'); xhr.onreadystatechange = ((e) => { if(xhr.readyState !== XMLHttpRequest.DONE){return} if(xhr.status === 404){return} if (xhr.status === 200) { Frame.frame_datas =this.frame_datas = JSON.parse(e.target.response) this.view() this.set_collision() this.finish() } }).bind(this) xhr.send() } view(){ for(let i=0; i<this.frame_datas.length; i++){ const p = document.createElement('p') p.className = this.frame_datas[i] Frame.root.appendChild(p) } } finish(){ if(this.resolve){ this.resolve(this) } } static put(elm , coodinates){ if(!elm){return} const pos = this.calc_coodinates2position(coodinates) this.pos(elm , pos) elm.setAttribute('data-x' , coodinates.x) elm.setAttribute('data-y' , coodinates.y) } static calc_coodinates2position(coodinates){ const size = Frame.block_size return { x : (coodinates.x) * size, y : (coodinates.y) * size, } } static pos(elm , pos){ elm.style.setProperty('left' , `${pos.x}px` , '') elm.style.setProperty('top' , `${pos.y}px` , '') } // 壁座標に1を設置 set_collision(){ const cols_count = this.cols_count const maps = [] let row_count = 0 for(const frame_data of this.frame_datas){ maps[row_count] = maps[row_count] || [] // 移動できる if(frame_data.match(/^P/i) || frame_data.toUpperCase() === 'S5'){ maps[row_count].push(0) } // 壁 else{ maps[row_count].push(1) } if(maps[row_count].length === cols_count){ row_count++ } } Frame.map = this.map = maps } static is_collision(map){ return Frame.map[map.y][map.x] } }

[全部更新] js/pacman.js

moved()関数で"Frame.is_collision()"を判定して、次に進む座標が壁かどうかの判定をしています。 壁の場合は進めないようにすると、一気にゲーム感が増します。 import { Frame } from './frame.js' import { Control } from './control.js' export class Pacman{ // 初期表示座標処理 constructor(){ Pacman.anim_speed = 300 Pacman.coodinates = this.start_coodinates Frame.put(this.elm, Pacman.coodinates) this.elm.style.setProperty('--anim-speed' , `${Pacman.anim_speed}ms` , '') } get start_coodinates(){ return { x : 14, y : 23, } } get elm(){ return Pacman.elm } static get elm(){ return document.querySelector('.pacman') } static move(direction){ if(Pacman.direction){ return } Pacman.direction = direction this.elm.setAttribute('data-anim' , "true") this.moving() } static moving(){ const next_pos = Pacman.next_pos(Pacman.direction) if(Frame.is_collision(next_pos)){ this.elm.setAttribute('data-anim' , "") delete Pacman.direction return } this.elm.setAttribute('data-direction' , Pacman.direction) this.elm.animate( [ { left : `${Pacman.coodinates.x * Frame.block_size}px`, top : `${Pacman.coodinates.y * Frame.block_size}px`, }, { left : `${next_pos.x * Frame.block_size}px`, top : `${next_pos.y * Frame.block_size}px`, } ], { duration: Pacman.anim_speed } ) Promise.all(this.elm.getAnimations().map(e => e.finished)).then(()=>{ Pacman.moved(next_pos) }) } static moved(next_pos){ Pacman.coodinates = next_pos Frame.put(this.elm, Pacman.coodinates) if(Control.direction && Control.direction !== Pacman.direction){ const temp_pos = Pacman.next_pos(Control.direction) if(!Frame.is_collision(temp_pos)){ Pacman.direction = Control.direction } } Pacman.moving() } static next_pos(name){ const next_pos = { x : Pacman.coodinates.x, y : Pacman.coodinates.y, } switch(name){ case 'left': next_pos.x -= 1 break case 'right': next_pos.x += 1 break case 'up': next_pos.y -= 1 break case 'down': next_pos.y += 1 break default: return } return next_pos } }

画面キャプチャ

あとがき

うまく、壁判定して、キャラクターを操作することができましたか? 前回からの変更点は3ファイルだけですが、都度内容を大きく変えてしまっているのは、自分の事前設計の甘いところですねwww。 それでも、なるべく分かりやすい関数を心がけているので、中を読み解いてもっと簡単にできるように改造してみてください。 ※この時点で改造してしまうと、この後の処理がうまく続かなくなるので、このシリーズが終わったあとで改修するか、gitで管理して、フォークして遊んでみて下さい。 操作系は、まだ完璧ではなくて、ステージ内にあるワープエリアで、座標をワープさせる必要があるので、次回はその処理を追加します。

知財

パックマンは、バンダイナムコ社の登録商標です。 PAC-MAN™ & ©1980 BANDAI NAMCO Entertainment Inc.

このブログを検索

ごあいさつ

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