[Javascript] パックマンをHTMLで作ってみるブログ#13 敵キャラdeadモーション

2023年7月16日

Javascript

eyecatch 前回、Pacmanがやられた時の処理を作りましたが、今回は敵キャラを食べた時の処理を実装してみました。 大幅に変更しなければいけない箇所があったので、対象ファイルはほぼ全入れ替えになってしまいました。

今回の目的

- パワーモードの時に敵キャラを食べる処理。 - 敵キャラを食べた時の敵キャラの移動モーション。 - パワーモードの終わりに近づくと、敵キャラを点滅させてもうすぐパワーモードが終わる処理の追加。 - [バグ] パワーエサを複数食べた時の継続時間不具合対応。

対象ファイル一覧

  1. css/ghost.css
  2. js/feed.js
  3. js/frame.js
  4. js/ghost.js
  5. js/main.js
  6. js/pacman.js

ソースコード

[全部更新] css/ghost.css

.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;} .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; } }

[全部更新] js/feed.js

import { Frame } from './frame.js' import { Pacman } from './pacman.js' import { Ghost } from './ghost.js' export class Feed{ 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) 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) break } } 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

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) } 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 } } 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() } }).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) 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を設置 set_collision(type){ 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' || 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'){ 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 } set_ghost_start_area(){ const ghost_start_area = [] for(let i=0; i<this.frame_datas.length; i++){ if(this.frame_datas[i] !== 'TU' && this.frame_datas[i] !== 'TD' && this.frame_datas[i] !== 'TL' && this.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 } }

[全部更新] js/ghost.js

import { Main } from './main.js' import { Frame } from './frame.js' import { Pacman } from './pacman.js' export class Ghost{ constructor(){ this.put_element() } static datas = [ { 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 }, }, ] 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 } 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) } this.load_asset() } 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) { this.asset = e.target.response this.set_ghost() setTimeout(this.set_move.bind(this) , 300) } }).bind(this) xhr.send() } 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 = this.asset } } set_move(){ const elm_ghosts = Ghost.elm_ghosts for(const elm_ghost of elm_ghosts){ this.move(elm_ghost) } } 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) this.moving(elm_ghost , next_pos) } 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(this.moved.bind(this , elm_ghost)) } moved(elm_ghost , e){ 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') this.reverse_move(elm_ghost , data) } else{ this.next_move(elm_ghost , data) } } 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) this.moving(elm_ghost , next_pos) } 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)){ this.move(elm_ghost) } else{ this.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 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

下記のMain変数内の赤字の箇所を追加してください。 export const Main = { anim_speed : 200, ghost_normal_speed : 200, ghost_weak_speed : 400, ghost_dead_speed : 50, is_crash : false, is_dead : false, }

[全部更新] 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.coodinates = this.start_coodinates Frame.put(this.elm, Pacman.coodinates) this.elm.style.setProperty('--anim-speed' , `${Main.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(){ if(Main.is_dead){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)){ 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 : `${Pacman.next_pos.x * Frame.block_size}px`, top : `${Pacman.next_pos.y * Frame.block_size}px`, } ], { duration: Main.anim_speed } ) Promise.all(this.elm.getAnimations().map(e => e.finished)).then(()=>{ Pacman.moved() }) } static moved(){ Pacman.coodinates = Pacman.next_pos Frame.put(this.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 || !Pacman.coodinates || !Pacman.next_pos){return} if(pos.x === Pacman.coodinates.x && pos.y === Pacman.coodinates.y){ return true } else if(pos.x === Pacman.next_pos.x && pos.y === Pacman.next_pos.y){ return true } else{ return false } } static crashed(elm_ghost){console.log('pacman-dead' , elm_ghost) setTimeout(Pacman.dead , 1000) const anim = Pacman.elm.getAnimations() if(anim && anim.length){ anim[0].pause() } } static dead(){ Ghost.hidden_all() Pacman.elm.setAttribute('data-direction' , 'up') Pacman.elm.setAttribute('data-status' , 'dead') } }

画面キャプチャ

解説とポイント

なかなかの更新ボリュームになりましたが、一番大きく変わった箇所は、敵キャラのモードにdeadモードを追加した箇所です。

敵キャラのdeadモード

自キャラのdeadモードは、その場でゲームステージの終了になりますが、敵キャラはdeadになっても、自分のエリアに戻っていくことで再び元のGhostキャラに戻るというゲーム性です。 そのため、敵キャラはdeadになっても動き続けるし、その間の処理は、自キャラに触れても何も処理をしない状態になるので、これまでとまるで違うモードを追加しました。 そして最大のポイントは、敵キャラがスタート地点のエリアに戻っていくといういわゆる迷路処理というのを実装する必要があるというのが今回の最大のポイントでした。 迷路をプログラミングでスタートからゴールまでの最短距離やパターン数を導き出す「迷路処理」は、なかなかの難しいアルゴリズムになるので、今回は厳密ではない迷路処理です。 たまに、帰り道がわからなくなって迷子になる敵キャラもいるので、クスッと笑ってください。 ちなみに、敵キャラはweakモードの時は、通常よりも半分ぐらいスピードが遅くなり、deadモードの時は、通常の倍ぐらいのスピードで進むので、スピードに関するデータにも影響を及ぼしています。

パワーモード終了のsoonモード

あと、以前に構築した、パワーエサを食べた時に一定時間パワーモードになりますが、いきなり終了していてなんか違和感があるな〜と思っていたら、 本家ゲームでは、パワーモードが終わる少し前に、青くなった敵キャラが点滅して、もうすぐ終わるよモードが存在していたので、それを実装しました。 ここで、javascriptのanimate機()関数の闇にぶち当たって、その修復に時間を割いてしまいました。 が、無事に実装完了できたので、結果オーライです。

敵キャラスタートエリアの闇

ゲーム開始時に2匹だけ敵キャラがスタートエリア内にいますが、上部のフタをくぐってステージ内に出てくる箇所が、一方通行の扉になっている仕様があり、 以前説明しなかったのですが、敵キャラがdeadモードの時は、この扉の一方通行が逆になります。 かつ、自キャラはいつでもこの扉を通ることができないので、この点スマートな記述が思いつかずにベタなプログラミングで実装してしまいました。 そのうち、リファクタリングしりゃあいいか。程度に考えています。

その他バグ修正

何故か自キャラがワープエリアに入ると、JSエラーが出ていたので調べてみたところ、指定する関数が間違っていました。 きっとどこかのリファクタリングでデバッグが行き届かないまま変更してしまっていたのでしょう。 他にも、パワーモードを2つ以上食べた時に、1つ目の終了時間で終わってしまうバグがあり、ちゃんとフラグを設置して、あと食べ有効にするように改善しておきました。

あとがき

このゲームの一つの大きな山を乗り越えたと思います。 ほぼ、ゲームとして成り立っている状態になりましたが、肝心のパワーエサを全て食べてもステージクリアの処理が入っていないので、達成感が得られません。 また、自キャラが敵に捕まって死んでしまっても、キャラの残り数などの処理が入っていないため、次のゲームスタートになりません。 次回からは、この辺のゲームシステム的な処理を実装していきたいと思います。

知財

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

このブログを検索

ごあいさつ

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