[Javascript] パックマンをHTMLで作ってみるブログ#14 ステージクリア処理

2023年7月20日

Javascript

eyecatch ゲーム内部の処理が大体出来上がってきたので、今回はステージクリアの処理を追加したいと思います。

今回の目的

- ステージクリア(エサを全部食べた時)に次のステージに進む - クリア時に、敵キャラの動きを止めて非表示にする - クリア時に、自キャラの動きを止める - ステージ切り替えに必要な各種データのリセット処理

対象ファイル一覧

  1. index.html
  2. assets/ghost.json
  3. css/frame.css
  4. css/ghost.css
  5. css/pacman.css
  6. js/control.js
  7. js/feed.js
  8. js/frame.js
  9. js/ghost.js
  10. js/main.js
  11. js/pacman.js
軽い気持ちで作業をしてみたところ、結構大本の箇所の修正なども行う必要があったので、ほとんどのソース修正が必要になりました。

ソースコード

[全部更新] index.html

自キャラのエレメントを、動的に作成するようにしました。 <!DOCTYPE html> <html lang='ja'> <head> <link rel='stylesheet' href='css/style.css' /> <script type='module' src='js/main.js'></script> <style>body{background-color:black}</style> </head> <body> <div class='frame-area' data-power="0"></div> </body> </html>

[新規追加] assets/ghost.json

 敵キャラの設定データをjsonファイル化しました。 [ { "id" : 1, "direction" : null, "coodinate" : { "x" : 12, "y" : 11 } }, { "id" : 2, "direction" : null, "coodinate" : { "x" : 15, "y" : 11 } }, { "id" : 3, "direction" : null, "coodinate" : { "x" : 12, "y" : 14 } }, { "id" : 4, "direction" : null, "coodinate" : { "x" : 15, "y" : 14 } } ]

[最後に追加] css/frame.css

ステージクリアした時の、ステージ枠の色アニメーションをcssで追加しました。 .frame-area[data-status='clear']{ animation-name : stage-clear; animation-duration:2s; animation-timing-function: linear; animation-iteration-count: 1; } @keyframes stage-clear{ 0%{ --color:blue; } 10%{ --color:white; } 20%{ --color:blue; } 30%{ --color:white; } 40%{ --color:blue; } 50%{ --color:white; } 60%{ --color:blue; } 70%{ --color:white; } 80%{ --color:blue; } 90%{ --color:white; } 100%{ --color:blue; } }

[一行削除] css/ghost.css

不要な処理を1行だけ削除しました。 赤色の行を消してください。 .ghost{ position:absolute; z-index:9; display:inline-block; --size-width : calc(var(--size-chara) * 1.0); --size-eye : calc(var(--size-width) * 0.3); --size-under : calc(var(--size-width) * 0.2); --color-body : red; width:var(--size-chara); height:var(--size-chara); transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)); } .ghost[data-color='1']{--color-body : red;} .ghost[data-color='2']{--color-body : orange;} .ghost[data-color='3']{--color-body : lightblue;} .ghost[data-color='4']{--color-body : pink;} .ghost[data-status='weak']{--color-body : blue;} /* .frame-area .ghost[data-status='1']{--color-body : blue;} */ .ghost > *{ margin : 0 auto; width : var(--size-width); } .ghost > .head{ height : calc(var(--size-width) / 2); background-color : var(--color-body); border-radius : var(--size-chara) var(--size-chara) 0 0; position:relative; } .ghost > .head .eyes{ position:absolute; width:100%; bottom:0; z-index:10; } .ghost > .head .eyes *{ position:absolute; bottom:0%; width : var(--size-eye); height : var(--size-eye); background-color:white; border-radius:50%; } .ghost > .head .eyes .eye-left{ left:13%; } .ghost > .head .eyes .eye-right{ right:13%; } .ghost > .head .eyes *::before{ content:''; display:block; background-color:black; width : 50%; height : 50%; margin : 25%; border-radius:50%; transition-property : margin; transition-duration : 0.3s; } .ghost > .head[data-direction='right'] .eyes{ left:12%; } .ghost > .head[data-direction='left'] .eyes{ left:-12%; } .ghost > .head[data-direction='up'] .eyes{ bottom:20%; } .ghost > .head[data-direction='down'] .eyes{ bottom:-20%; } .ghost[data-status='weak'] > .head .eyes{ left:0; bottom:20%; } .ghost > .head[data-direction='right'] .eyes > *::before{ margin-left : 50%; margin-right : 0; } .ghost > .head[data-direction='left'] .eyes > *::before{ margin-left : 0; margin-right : 50%; } .ghost > .head[data-direction='up'] .eyes > *::before{ margin-top : 0; margin-bottom : 50%; } .ghost > .head[data-direction='down'] .eyes > *::before{ margin-top : 50%; margin-bottom : 0; } .ghost[data-status='weak'] > .head .eyes *::before{ display:none; } .ghost[data-status='weak'] > .head .eyes .eye-left, .ghost[data-status='weak'] > .head .eyes .eye-right{ width : calc(var(--size-eye) / 2); height : calc(var(--size-eye) / 2); background-color:white; border-radius:50%; } .ghost[data-status='weak'] > .head .eyes .eye-left{ left:25%; } .ghost[data-status='weak'] > .head .eyes .eye-right{ right:25%; } .ghost > .mouse{ height : 30%; background-color : var(--color-body); position:relative; } .frame-area .ghost > .mouse > svg{ display:none; } .ghost[data-status='weak'] > .mouse > svg{ position:absolute; top:-60%; display:block; margin-top:10px; } .ghost > .under{ height : var(--size-under); } .ghost > .under path{ fill:var(--color-body); } .ghost[data-status='dead']{ --color-body : blue; } .ghost[data-status='dead']{ --color-body : transparent; } /* power-limit-soon */ [data-power='2'] .ghost[data-status='weak']{ animation-name : power_soon_body; animation-duration:0.4s; animation-timing-function: ease-in-out; animation-iteration-count: infinite; } [data-power='2'] .ghost[data-status='weak'] > .head .eyes .eye-left, [data-power='2'] .ghost[data-status='weak'] > .head .eyes .eye-right{ animation-name : power_soon_eyes; animation-duration:0.4s; animation-timing-function: ease-in-out; animation-iteration-count: infinite; } [data-power='2'] .ghost[data-status='weak'] > .mouse svg path{ animation-name : power_soon_mouse; animation-duration:0.4s; animation-timing-function: ease-in-out; animation-iteration-count: infinite; } @keyframes power_soon_body{ 0%{ --color-body:blue; } 50%{ --color-body:white; } 100%{ --color-body:blue; } } @keyframes power_soon_eyes{ 0%{ background-color:white; } 50%{ background-color:red; } 100%{ background-color:white; } } @keyframes power_soon_mouse{ 0%{ stroke:white; } 50%{ stroke:red; } 100%{ stroke:white; } }

[一部追加] css/pacman.css

クリアした時のcss処理を追加しました。 赤字の箇所のみ更新してもらうだけで大丈夫です。 .pacman{ position:absolute; z-index:10; display:inline-block; width:var(--size-chara); height:var(--size-chara); } .frame-area[data-status='clear'] .pacman::before, .frame-area[data-status='clear'] .pacman::after, .pacman[data-status='crashed']::before, .pacman[data-status='crashed']::after{ animation-play-state: paused; } .pacman::before, .pacman::after{ content:''; display:block; width:100%; height:50%; background-color:yellow; animation-duration:var(--anim-speed); animation-timing-function: ease-in-out; animation-iteration-count: infinite; } .pacman::before{ transform-origin:center bottom; border-radius:var(--size-chara) var(--size-chara) 0 0; transform:rotate(45deg); } .pacman::after{ transform-origin:center top; border-radius:0 0 var(--size-chara) var(--size-chara); transform:rotate(-45deg); } .pacman[data-anim="true"]::before{ animation-name : pacman_before; } .pacman[data-anim="true"]::after{ animation-name : pacman_after; } @keyframes pacman_before{ 0%{ transform:rotate(45deg); } 50%{ transform:rotate(0deg); } 100%{ transform:rotate(45deg); } } @keyframes pacman_after{ 0%{ transform:rotate(-45deg); } 50%{ transform:rotate(0deg); } 100%{ transform:rotate(-45deg); } } .pacman, .pacman[data-direction="left"]{ transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)); } .pacman[data-direction="right"]{ transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)) scaleX(-1); } .pacman[data-direction="up"]{ transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)) rotate(90deg); } .pacman[data-direction="down"]{ transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)) rotate(-90deg); } .pacman[data-status="dead"]::before, .pacman[data-status="dead"]::after{ transform:rotate(0deg); animation-delay : 0.0s; animation-direction : normal; animation-duration : 2.0s; animation-fill-mode: forwards; animation-iteration-count: 1; animation-timing-function: linear; } .pacman[data-status="dead"]::before{ animation-name : pacman-dead-before; } .pacman[data-status="dead"]::after{ animation-name : pacman-dead-after; } @keyframes pacman-dead-before{ 0%{ clip-path: polygon( 50% 100%, 0% 100%, 0% 0%, 100% 0%, 100% 100% ); } 25%{ clip-path: polygon( 50% 100%, 0% 0%, 0% 0%, 100% 0%, 100% 100% ); } 75%{ clip-path: polygon( 50% 100%, 100% 0%, 100% 0%, 100% 0%, 100% 100% ); } 100%{ clip-path: polygon( 50% 100%, 100% 100%, 100% 100%, 100% 100%, 100% 100% ); } } @keyframes pacman-dead-after{ 0%{ clip-path: polygon( 50% 0%, 0% 0%, 0% 100%, 100% 100%, 100% 0% ); } 25%{ clip-path: polygon( 50% 0%, 0% 100%, 0% 100%, 100% 100%, 100% 0% ); } 75%{ clip-path: polygon( 50% 0%, 100% 100%, 100% 100%, 100% 100%, 100% 0% ); } 100%{ clip-path: polygon( 50% 0%, 100% 0%, 100% 0%, 100% 0%, 100% 0% ); } }

[全部更新] js/control.js

全体的に、private処理をstatic処理に切り替えました。 import { Pacman } from './pacman.js' import { Frame } from './frame.js' export class Control{ constructor(){ window.addEventListener('keydown' , Control.keydown.bind(this)) window.addEventListener('keyup' , Control.keyup.bind(this)) } static key2name(key){ switch(key){ case 37 : return 'left' case 38 : return 'up' case 39 : return 'right' case 40 : return 'down' } } static keydown(e){ if(e.repeat === true){return} if(Frame.is_clear){return} const key = e.keyCode const name = Control.key2name(key) if(!name){return} Control.direction = name Pacman.move(Control.direction) } static keyup(e){ if(!Control.direction){return} const name = Control.key2name(e.keyCode) Control.clear() } static clear(){ if(Control.direction !== name){return} delete Control.direction } }

[全部更新] js/feed.js

エサの残り数を計算する処理を全体的に追加しています。 ファイル内を全て入れ替えてください。 import { Frame } from './frame.js' import { Pacman } from './pacman.js' import { Ghost } from './ghost.js' export class Feed{ constructor(){ // Feed.number_of_bites = this.elm_number_of_bites Feed.reset_data() } static get elm_number_of_bites(){ const elms = document.querySelectorAll(`.frame-area .P1,.frame-area .P2`) return elms.length } static reset_data(){ Feed.number_of_bites = Feed.elm_number_of_bites } static move_map(){ const num = Frame.get_pos2num(Pacman.coodinates) const item = Frame.frame_datas[num] switch(item){ case 'P1': this.eat_normal_dot(num) Feed.number_of_bites-- break case 'P2': Feed.power_on() Feed.flg_soon = setTimeout(Feed.power_soon , 7000) Feed.flg_off = setTimeout(Feed.power_off , 10000) this.eat_big_dot(num) Feed.number_of_bites-- break } if(Feed.number_of_bites <= 0){ Frame.stage_clear() } } static eat_normal_dot(num){ Frame.frame_datas[num] = 'S5' const elm = Frame.get_elm(num) if(!elm){return} elm.setAttribute('class','S5') } static eat_big_dot(num){ Frame.frame_datas[num] = 'S5' const elm = Frame.get_elm(num) if(!elm){return} elm.setAttribute('class','S5') } static power_on(){ Frame.root.setAttribute('data-power' , '1') Ghost.power_on() if(Feed.flg_soon){ clearTimeout(Feed.flg_soon) } if(Feed.flg_off){ clearTimeout(Feed.flg_off) } } static power_soon(){ Frame.root.setAttribute('data-power' , '2') } static power_off(){ Frame.root.setAttribute('data-power' , '0') Ghost.power_off() delete Feed.flg_soon delete Feed.flg_off } }

[全部更新] js/frame.js

ステージ情報のモジュールなので、大幅に変更をしました。 import { Main } from './main.js' import { Pacman } from './pacman.js' import { Ghost } from './ghost.js' import { Feed } from './feed.js' import { Control } from './control.js' 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 } static get cols_count(){ return ~~(Frame.root.offsetWidth / Frame.block_size) } static get_elm(num){ return Frame.root.querySelector(`[data-num='${num}']`) } static get is_weak(){ const power = Frame.root.getAttribute('data-power') switch(power){ case '1': case '2': return true default: return false } } static get is_clear(){ return Frame.root.getAttribute('data-status') === 'clear' ? true : false } 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() // this.set_ghost_start_area() Frame.asset_json = e.target.response Frame.start() this.finish() } }).bind(this) xhr.send() } static start(){ Frame.frame_datas = JSON.parse(Frame.asset_json) Frame.view() Frame.set_collision() Frame.set_ghost_start_area() } static crear(){ Frame.root.innerHTML = '' } static view(){ for(let i=0; i<Frame.frame_datas.length; i++){ const p = document.createElement('p') p.className = Frame.frame_datas[i] Frame.root.appendChild(p) p.setAttribute('data-num' , i) } } 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を設置 static set_collision(type){ const cols_count = Frame.cols_count const maps = [] let row_count = 0 for(const frame_data of Frame.frame_datas){ maps[row_count] = maps[row_count] || [] // 移動できる if(frame_data.match(/^P/i) || frame_data.toUpperCase() === 'S5' || frame_data.match(/^W/i) || frame_data.match(/^T/i)){ 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){ if(!map || !Frame.map || !Frame.map[map.y]){return} return Frame.map[map.y][map.x] } // type @ [pacman , ghost] static is_through(map , direction, status){ const through_item = Frame.frame_datas[Frame.get_pos2num(map)] if(status === 'dead'){ // return true if(through_item === 'TU' && direction === 'up' || through_item === 'TD' && direction === 'down' || through_item === 'TL' && direction === 'left' || through_item === 'TR' && direction === 'right'){ return false } else{ return true } } else{ if(through_item === 'TU' && direction !== 'up' || through_item === 'TD' && direction !== 'down' || through_item === 'TL' && direction !== 'left' || through_item === 'TR' && direction !== 'right'){ return false } else{ return true } } } static get_pos2num(pos){ return pos.y * Frame.map[0].length + pos.x } static get_num2pos(num){ return { x : num % Frame.map[0].length, y : ~~(num / Frame.map[0].length), } } static is_warp(map){ const num = Frame.get_pos2num(map) return Frame.frame_datas[num] === 'W1' ? true : false } static get_another_warp_pos(map){ const warp_index_arr = Frame.filterIndex(Frame.frame_datas , 'W1') const current_index = Frame.get_pos2num(map) const another_num = warp_index_arr.find(e => e !== current_index) return Frame.get_num2pos(another_num) } static filterIndex(datas,target){ const res_arr = [] for(let i=0; i<datas.length; i++){ if(datas[i] === target){ res_arr.push(i) } } return res_arr } static next_pos(direction , pos){ const temp_pos = { x : pos.x, y : pos.y, } switch(direction){ case 'left': temp_pos.x -= 1 break case 'right': temp_pos.x += 1 break case 'up': temp_pos.y -= 1 break case 'down': temp_pos.y += 1 break default: return } return temp_pos } static set_ghost_start_area(){ const ghost_start_area = [] for(let i=0; i<Frame.frame_datas.length; i++){ if(Frame.frame_datas[i] !== 'TU' && Frame.frame_datas[i] !== 'TD' && Frame.frame_datas[i] !== 'TL' && Frame.frame_datas[i] !== 'TR'){continue} const pos = Frame.get_num2pos(i) ghost_start_area.push({ num : i, pos : pos, }) } const pos = { x : ghost_start_area.map(e => e.pos.x).reduce((sum , e)=>{ return sum + e}) / ghost_start_area.length, y : ghost_start_area.map(e => e.pos.y).reduce((sum , e)=>{ return sum + e}) / ghost_start_area.length, } Frame.ghost_start_area = pos } static stage_clear(){ // console.log('stage cleared !!') Main.is_clear = true // 画面の動きを止める Control.clear() Pacman.move_stop() Pacman.close_mouse() Ghost.move_stops() Ghost.hidden_all() setTimeout((()=>{ Frame.root.setAttribute('data-status' , 'clear') }),500) setTimeout((()=>{ Main.is_clear = false Frame.crear() Frame.root.setAttribute('data-status' , '') Frame.start() Feed.reset_data() Ghost.reset_data() Pacman.reset_data() }),4000) } }

[全部更新] js/ghost.js

モジュール内に書いていた、ghostのデータをjsonデータとして、別ファイルに移動して、ajax読み込みするようにしました。 import { Main } from './main.js' import { Frame } from './frame.js' import { Pacman } from './pacman.js' export class Ghost{ constructor(){ this.load_data() } static reset_data(){ Ghost.datas = JSON.parse(Ghost.data_json) Ghost.put_element() } static get elm_ghosts(){ return document.querySelectorAll('.ghost') } static get_data(elm){ const color_num = elm.getAttribute('data-color') return Ghost.datas.find(e => e.id === Number(color_num)) } static get_id(elm){ const data = Ghost.get_data(elm) return data.id } static get_coodinate(elm){ const data = Ghost.get_data(elm) return data.coodinate } load_data(){ const xhr = new XMLHttpRequest() xhr.open('get' , `assets/ghost.json` , true) xhr.setRequestHeader('Content-Type', 'text/json'); xhr.onreadystatechange = ((e) => { if(xhr.readyState !== XMLHttpRequest.DONE){return} if(xhr.status === 404){return} if (xhr.status === 200) { Ghost.data_json = e.target.response Ghost.datas = JSON.parse(Ghost.data_json) Ghost.put_element() } }) xhr.send() } static put_element(){ for(const data of Ghost.datas){ const elm = document.createElement('div') elm.className = 'ghost' elm.setAttribute('data-color' , data.id) Frame.root.appendChild(elm) } Ghost.load_asset() } static load_asset(){ const xhr = new XMLHttpRequest() xhr.open('get' , `assets/ghost.html` , 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) { Ghost.asset = e.target.response Ghost.set_ghost() Ghost.set_move() // setTimeout(this.set_move.bind(this) , 300) } }) xhr.send() } static set_ghost(){ const elm_ghosts = Ghost.elm_ghosts for(const elm_ghost of elm_ghosts){ const coodinate = Ghost.get_coodinate(elm_ghost) Frame.put(elm_ghost, coodinate) elm_ghost.innerHTML = Ghost.asset } } static set_move(){ const elm_ghosts = Ghost.elm_ghosts for(const elm_ghost of elm_ghosts){ Ghost.move(elm_ghost) } } static move(elm_ghost){ if(!elm_ghost){return} const data = Ghost.get_data(elm_ghost) const coodinate = Ghost.get_coodinate(elm_ghost) const status = Ghost.get_status(elm_ghost) const directions = Ghost.get_enable_directions(coodinate , data.direction , status) || Ghost.get_enable_directions(coodinate) const direction = Ghost.get_direction(elm_ghost, directions) const next_pos = Frame.next_pos(direction , coodinate) Ghost.set_direction(elm_ghost , direction) Ghost.moving(elm_ghost , next_pos) } static moving(elm_ghost , next_pos){ if(!elm_ghost || !next_pos){return} const data = Ghost.get_data(elm_ghost) if(!data){return} if(Main.is_dead){ return } //warp if(Frame.is_warp(next_pos)){ data.coodinate = Frame.get_another_warp_pos(next_pos) next_pos = Frame.next_pos(data.direction , data.coodinate) } if(!next_pos){return} const before_pos = { x : data.coodinate.x * Frame.block_size, y : data.coodinate.y * Frame.block_size, } const after_pos = { x : next_pos.x * Frame.block_size, y : next_pos.y * Frame.block_size, } if(Pacman.is_collision(next_pos)){ switch(elm_ghost.getAttribute('data-status')){ case 'weak': Ghost.dead(elm_ghost) break case 'dead': break default: Ghost.crashed(elm_ghost) Pacman.crashed(elm_ghost) return } } data.next_pos = next_pos const id = 'ghost_anim' elm_ghost.animate( [ { left : `${before_pos.x}px`, top : `${before_pos.y}px`, }, { left : `${after_pos.x}px`, top : `${after_pos.y}px`, } ], { id : id, duration: Ghost.get_speed(elm_ghost) } ) Promise.all([elm_ghost.getAnimations().find(e => e.id === id).finished]) .then(Ghost.moved.bind(this , elm_ghost)) } static moved(elm_ghost , e){ if(!elm_ghost){return} const data = Ghost.get_data(elm_ghost) elm_ghost.style.setProperty('left' , `${data.next_pos.x * Frame.block_size}px` , '') elm_ghost.style.setProperty('top' , `${data.next_pos.y * Frame.block_size}px` , '') data.coodinate = data.next_pos if(Main.is_dead){return} if(Pacman.is_collision(data.coodinate)){ switch(elm_ghost.getAttribute('data-status')){ case 'weak': Ghost.dead(elm_ghost) break case 'dead': break default: Ghost.crashed(elm_ghost) Pacman.crashed(elm_ghost) return } } // dead -> alive if(Ghost.get_status(elm_ghost) === 'dead'){ const current_stage_item = Frame.frame_datas[Frame.get_pos2num(data.coodinate)] if(current_stage_item.match(/^T/i)){ Ghost.alive(elm_ghost) } } if(elm_ghost.hasAttribute('data-reverse')){ elm_ghost.removeAttribute('data-reverse') Ghost.reverse_move(elm_ghost , data) } else{ Ghost.next_move(elm_ghost , data) } } static reverse_move(elm_ghost , data){ const direction = Ghost.reverse_direction(data.direction) Ghost.set_direction(elm_ghost , direction) data.direction = direction const next_pos = Frame.next_pos(data.direction , data.coodinate) Ghost.moving(elm_ghost , next_pos) } static next_move(elm_ghost , data){ const directions = Ghost.get_enable_directions(data.coodinate , data.direction , Ghost.get_status(elm_ghost)) const direction = Ghost.get_direction(elm_ghost, directions) Ghost.set_direction(elm_ghost , direction) const next_pos = Frame.next_pos(data.direction , data.coodinate) if(Frame.is_collision(next_pos)){ Ghost.move(elm_ghost) } else{ Ghost.moving(elm_ghost , next_pos) } } static get_direction(elm_ghost, directions){ if(!directions || !directions.length){return null} switch(Ghost.get_status(elm_ghost)){ // dead : go to the start-area case 'dead': if(directions.length === 1){ return directions[0] } const ghost_data = Ghost.get_data(elm_ghost) const start_datas = Frame.ghost_start_area if(directions.indexOf('right') !== -1 && ghost_data.coodinate.x > start_datas.x){ const index = directions.findIndex(e => e === 'right') directions.splice(index,1) } if(directions.indexOf('left') !== -1 && ghost_data.coodinate.x < start_datas.x){ const index = directions.findIndex(e => e === 'left') directions.splice(index,1) } if(directions.indexOf('bottom') !== -1 && ghost_data.coodinate.y > start_datas.y){ const index = directions.findIndex(e => e === 'bottom') directions.splice(index,1) } if(directions.indexOf('top') !== -1 && ghost_data.coodinate.y < start_datas.y){ const index = directions.findIndex(e => e === 'top') directions.splice(index,1) } const num = Math.floor(Math.random() * directions.length) return directions[num] || null // normal default: const direction_num = Math.floor(Math.random() * directions.length) return directions[direction_num] || null } } // 移動可能な方向の一覧を取得する static get_enable_directions(pos , direction , status){ const directions = [] // Through(通り抜け) const frame_data = Frame.frame_datas[Frame.get_pos2num(pos)] if(frame_data.match(/^T/i)){ return [direction] } // 右 : right if(pos.x + 1 < Frame.map[pos.y].length && !Frame.is_collision({x: pos.x + 1, y: pos.y}) && Frame.is_through({x: pos.x + 1, y: pos.y} , 'right' , status) && direction !== 'left'){ directions.push('right') } // 左 : left if(pos.x - 1 >= 0 && !Frame.is_collision({x: pos.x - 1, y: pos.y}) && Frame.is_through({x: pos.x - 1, y: pos.y} , 'left' , status) && direction !== 'right'){ directions.push('left') } // 上 : up if(pos.y - 1 >= 0 && !Frame.is_collision({x: pos.x, y: pos.y - 1}) && Frame.is_through({x: pos.x, y: pos.y - 1} , 'up' , status) && direction !== 'down' ){ directions.push('up') } // 下 : down if(pos.y + 1 < Frame.map.length && !Frame.is_collision({x: pos.x, y: pos.y + 1}) && Frame.is_through({x: pos.x, y: pos.y + 1} , 'down' , status) && direction !== 'up'){ directions.push('down') } if(directions.length){ return directions } else{ return [Ghost.reverse_direction(direction)] } } static reverse_direction(direction){ switch(direction){ case 'right' : return 'left' case 'left' : return 'right' case 'up' : return 'down' case 'down' : return 'up' } } static set_direction(elm_ghost , direction){ const data = Ghost.get_data(elm_ghost) data.direction = direction const head = elm_ghost.querySelector('.head') if(!head){return} head.setAttribute('data-direction' , direction) } static power_on(){ for(const elm of Ghost.elm_ghosts){ if(Ghost.get_status(elm) === 'dead'){continue} elm.setAttribute('data-reverse' , '1') elm.setAttribute('data-status' , 'weak') } } static power_off(){ for(const elm of Ghost.elm_ghosts){ if(elm.getAttribute('data-status') === 'weak'){ elm.setAttribute('data-status' , '') } } } static crashed(elm_ghost){ Main.is_crash = true Main.is_dead = true Ghost.move_stop(elm_ghost) } static move_stops(){ for(const elm_ghost of Ghost.elm_ghosts){ Ghost.move_stop(elm_ghost) } } static move_stop(elm_ghost){ const svg = elm_ghost.querySelector('.under svg') svg.pauseAnimations() const anim = elm_ghost.getAnimations() if(anim && anim.length){ anim[0].pause() } } static hidden_all(){ for(const elm of Ghost.elm_ghosts){ elm.style.setProperty('display' , 'none' , ''); } } static dead(elm_ghost){ elm_ghost.setAttribute('data-status' , 'dead') } static alive(elm_ghost){ elm_ghost.setAttribute('data-status' , '') } static get_status(elm_ghost){ return elm_ghost.getAttribute('data-status') } static get_speed(elm_ghost){ switch(Ghost.get_status(elm_ghost)){ case 'weak': return Main.ghost_weak_speed case 'dead': return Main.ghost_dead_speed default: return Main.ghost_normal_speed } } }

[全部更新] js/main.js

クリア処理に必要なフラグや、初期処理などを追加してます。 import { Ghost } from './ghost.js' import { Frame } from './frame.js' import { Control } from './control.js' import { Pacman } from './pacman.js' import { Feed } from './feed.js' export const Main = { anim_speed : 200, ghost_normal_speed : 200, ghost_weak_speed : 400, ghost_dead_speed : 50, is_crash : false, is_dead : false, is_clear : false, } function init(){ new Frame().then(()=>{ new Ghost() new Pacman() new Control() new Feed() }) } switch(document.readyState){ case 'complete': case 'interactive': init() break default: window.addEventListener('DOMContentLoaded' , (()=>init())) }

[全部更新] js/pacman.js

思っていたよりも変更箇所が多かったので、全部ファイルごと入れ替えちゃってください。 import { Main } from './main.js' import { Frame } from './frame.js' import { Control } from './control.js' import { Feed } from './feed.js' import { Ghost } from './ghost.js' export class Pacman{ // 初期表示座標処理 constructor(){ Pacman.start() } static start(){ Pacman.create() Pacman.coodinates = Pacman.start_coodinates Frame.put(Pacman.elm, Pacman.coodinates) Pacman.elm.style.setProperty('--anim-speed' , `${Main.anim_speed}ms` , '') } static reset_data(){ Pacman.direction = null Pacman.start() } static create(){ if(Pacman.elm){return} const div = document.createElement('div') div.className = 'pacman' Frame.root.appendChild(div) } static get start_coodinates(){ return { x : 14, y : 23, } } static get elm(){ return document.querySelector('.pacman') } static move(direction){ if(Pacman.direction){ return } Pacman.direction = direction Pacman.elm.setAttribute('data-anim' , "true") this.moving() } static moving(){ if(Main.is_dead || Main.is_clear){return} Pacman.next_pos = Frame.next_pos(Pacman.direction , Pacman.coodinates) //warp if(Frame.is_warp(Pacman.next_pos)){ Pacman.coodinates = Frame.get_another_warp_pos(Pacman.next_pos) Pacman.next_pos = Frame.next_pos(Pacman.direction , Pacman.coodinates) } if(Frame.is_collision(Pacman.next_pos) && !Pacman.is_wall(Pacman.next_pos)){ Pacman.elm.setAttribute('data-anim' , "") delete Pacman.direction return } Pacman.elm.setAttribute('data-direction' , Pacman.direction) Pacman.elm.animate( [ { left : `${Pacman.coodinates.x * Frame.block_size}px`, top : `${Pacman.coodinates.y * Frame.block_size}px`, }, { left : `${Pacman.next_pos.x * Frame.block_size}px`, top : `${Pacman.next_pos.y * Frame.block_size}px`, } ], { duration: Main.anim_speed } ) Promise.all(Pacman.elm.getAnimations().map(e => e.finished)).then(()=>{ Pacman.moved() }) } static moved(){ Pacman.coodinates = Pacman.next_pos Frame.put(Pacman.elm, Pacman.coodinates) Feed.move_map() if(Control.direction && Control.direction !== Pacman.direction){ const temp_pos = Frame.next_pos(Control.direction , Pacman.coodinates) if(!Frame.is_collision(temp_pos) && !Pacman.is_wall(temp_pos)){ Pacman.direction = Control.direction } } Pacman.moving() } static is_wall(map){ const through_item = Frame.frame_datas[Frame.get_pos2num(map)] if(through_item === 'TU' || through_item === 'TD' || through_item === 'TL' || through_item === 'TR'){ return true } else{ false } } static is_collision(pos){ if(!pos){return} if(Pacman.coodinates && pos.x === Pacman.coodinates.x && pos.y === Pacman.coodinates.y){ return true } else if(Pacman.next_pos && pos.x === Pacman.next_pos.x && pos.y === Pacman.next_pos.y){ return true } else{ return false } } static crashed(elm_ghost){ setTimeout(Pacman.dead , 1000) Pacman.move_stop() } static dead(){ Ghost.hidden_all() Pacman.elm.setAttribute('data-direction' , 'up') Pacman.elm.setAttribute('data-status' , 'dead') } static move_stop(){ const anim = Pacman.elm.getAnimations() if(anim && anim.length){ anim[0].pause() } } static hidden(){ Pacman.elm.style.setProperty('display','none','') } static close_mouse(){ // Pacman.elm.setAttribute('data-status' , 'mouse-close') } }

解説とポイント

初回の設定の甘さがここに来て出てしまいましたwww。 エサの残り数をFeedモジュールで処理させていますが、それをMainモジュールでセットしていなかったのと、敵キャラ、自キャラなどがステージクリアした時に、新しいステージでちゃんと最初の設定にもどるように、データリセットする事が考慮されていなかったので、全体の処理を見直しました。 なので、ステージは、assetデータを読み込んでいて、メモリに残した状態で、きれいに作り直すようにしました。 その時に、自キャラが消えてしまっていたのは、HTMLで直にタグを書いていたことが原因だったので、これを動的に処理するようにしました。 あと、ステージクリアした時に、画面が点滅する処理は、cssに全てやらせているので、さほど難しくなかったですね。

画面キャプチャ

あとがき

デバッグする時に、毎回1ステージをクリアする必要があったので、めちゃくちゃクリアしまくりました。 でも、全体処理として、足りていなかった箇所などがよく理解できて良かったです。 ただ、まだ自機の残り数などの設定自体が組み込まれていないので、一回でも死んでしまうと、ゲームがフリーズしてしまいます。 次回はその辺を進めましょうね。

知財

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

このブログを検索

ごあいさつ

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