
長らく掛かってしまったパックマン開発も、今回で最終回になってしまいました。
とりあえず、ゲームとしての足りていない部分を一気に詰め込んでみました。
あと、色々と整っていなかったプログラム箇所をリファクタリングしたので、修正ファイルがたくさんになってしまいました。
今回作業のメニュー
  - ゲームスタート処理(READY!表示)
- ゲームオーバー処理(GAME OVER表示)
- パワーエサの点滅
- リファクタリング作業
修正ファイル一覧
  - index.html
- css/footer.css
- css/frame.css
- css/style.css
- js/asset.js
- js/feed.js
- js/footer.js
- js/frame.js
- js/ghost.js
- js/main.js
- js/pacman.js
ソースコード
[一部追加] index.html
ステージ下部に、自機の残りライフアイコンを表示するためのエリアを追加しました。
※今回は使いませんが、ScoreやFruitエリアも、念のために追加しておきました。
<!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>
  <div class='frame-footer'>
    <div class='life-count'></div>
    <div class='score'></div>
    <div class='fruit'></div>
  </div>
</body>
</html>
[新規追加] css/footer.css
ステージ下箇所のcssファイルの追加です。
.frame-footer .life-count{
  --size-life : 32px;
}
.frame-footer .life-count .pacman{
  position:static;
  width:var(--size-life);
  height:var(--size-life);
  margin:10px 5px;
  display:inline-block;
}
[要素追加] css/frame.css
パワーエサの点滅と、ゲームスタート、ゲームオーバーのメッセージ表示用の設定を追加。
.frame-area .P2:before{
  position:absolute;
  top:50%;
  left:50%;
  transform:translate(-50%,-50%);
  width  : calc(var(--block) / 1);
  height : calc(var(--block) / 1);
  border-radius : 50%;
  background-color:yellow;
}
.frame-area .P2{
  animation-name : P2;
  animation-duration:0.5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes P2{
  0%{
    opacity:1.0;
  }
  50%{
    opacity:0.0;
  }
  100%{
    opacity:1.0;
  }
}
.frame-area .message{
  position:absolute;
  display:block;
  text-align:center;
  width:100%;
  height:var(--block);
  left:0;
  top:calc(var(--block) * 17 - 4px);
  color:yellow;
  font-weight:bold;
  font-size:calc(var(--block) * 1.2);
}
[一行追加] css/style.css
footer.cssを読み込む1行を追加してください。
@import 'footer.css';
[新規追加] js/asset.js
assetや各種データファイル(json)の読み込みを一元管理するために、asset.jsというモジュールを追加しました。
import { Frame } from './frame.js'
import { Ghost } from './ghost.js'
export class Asset{
  constructor(options){
    this.options = options || {}
    return new Promise(resolve => {
      this.resolve = resolve
      this.loads()
    })
  }
  loads(){
    for(const data of this.options.files){
      const xhr = new XMLHttpRequest()
      xhr.open('get' , data.file , true)
      xhr.setRequestHeader('Content-Type', 'text/json');
      xhr.onload = this.loaded.bind(this , data)
      xhr.send()
    }
  }
  loaded(data , e){
    data.finished = true
    switch(data.target){
      case 'frame':
        Frame[data.name] = this.get_type(data.type, e.target.response)
        break
      case 'ghost':
        Ghost[data.name] = this.get_type(data.type, e.target.response)
        break
    }
    this.check()
  }
  
  get_type(type , data){
    switch(type){
      case 'json':
        return JSON.parse(data)
      case 'html':
      default:
        return data
    }
  }
  check(){
    for(const data of this.options.files){
      if(data.finished !== true){return}
    }
    this.finish()
  }
  finish(){
    if(!this.resolve){return}
    this.resolve()
  }
}
[全部更新] js/feed.js
ステージクリア時などエサ情報をリセットする処理を追加して全体的に書き直しました。
import { Frame }     from './frame.js'
import { Pacman }    from './pacman.js'
import { Ghost }     from './ghost.js'
export class Feed{
  constructor(){
    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/footer.js
自機のライフ表示をするためのjsファイルを追加。
import { Main } from './main.js'
export class Footer{
  constructor(){
    this.view()
  }
  static get area(){
    return document.querySelector(`.frame-footer`)
  }
  static get life(){
    return document.querySelector(`.life-count`)
  }
  static get life_count(){
    return Footer.life.querySelectorAll(`.pacman`).length
  }
  view(){
    for(let i=0; i<Main.life_count; i++){
      const div = document.createElement('div')
      div.className = 'pacman'
      Footer.life.appendChild(div)
    }
  }
  static delete_life(){
    const elms = Footer.life.querySelectorAll(`:scope .pacman`)
    if(!elms.length){return}
    const target_elm = elms[elms.length-1]
    target_elm.parentNode.removeChild(target_elm)
  }
}
[全部更新] 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'
import { Footer }  from './footer.js'
export class Frame{
  constructor(){
    Frame.create()
    Frame.start()
  }
  static get root(){
    return document.querySelector(`.frame-area`)
  }
  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 is_ready(){
    return Frame.root.getAttribute('data-status') === 'ready' ? true : false
  }
  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
  }
  static get is_game_over(){
    return Footer.life_count <= 0 ? true : false
  }
  static start(){
    if(!Frame.is_game_over){
      Frame.root.setAttribute('data-status' , 'ready')
      Ghost.init()
      Pacman.init()
      Frame.message_on(`READY!`)
      setTimeout((()=>{
        Frame.root.setAttribute('data-status' , '')
        Frame.message_off()
        Ghost.start()
        Pacman.start()
      }),2000)
    }
    else{
      Frame.game_over()
    }
  }
  static game_over(){
    Frame.message_on(`GAME OVER`)
  }
  static get elm_message(){
    return Frame.root.querySelector('.message')
  }
  static message_on(message){
    const div = document.createElement('div')
    div.className = 'message'
    div.innerHTML = message
    Frame.root.appendChild(div)
  }
  static message_off(){
    Frame.root.removeChild(this.elm_message)
  }
  static create(){
    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)
    }
  }
  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(){
    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((()=>{
      Control.direction = null
      Main.is_clear = false
      Frame.crear()
      Frame.root.setAttribute('data-status' , '')
      Frame.create()
      Feed.reset_data()
      Frame.start()
    }),4000)
  }
  static crashed(){
    Control.clear()
    Ghost.move_stops()
    Ghost.remove_all()
    setTimeout((()=>{
      Control.direction = null
      Main.is_dead = false
      Frame.root.setAttribute('data-status' , '')
      Frame.set_ghost_start_area()
      Pacman.remove()
      Frame.start()
    }),3000)
  }
}
[全部更新] js/ghost.js
敵キャラの処理も大幅に書き換えました。
各種データ読み込みと、起動処理などがごちゃごちゃしていたので、スッキリと関数分けした感じで、全体構成を変更してます。
import { Main }   from './main.js'
import { Frame }  from './frame.js'
import { Pacman } from './pacman.js'
export class Ghost{
  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
  }
  static init(){
    Ghost.create_element()
    Ghost.set_ghost_asset()
  }
  static start(){
    Ghost.set_move()
  }
  static create_element(){
    Ghost.datas = JSON.parse(Ghost.data_json)
    for(const data of Ghost.datas){
      const elm = document.createElement('div')
      elm.className = 'ghost'
      elm.setAttribute('data-color' , data.id)
      Frame.root.appendChild(elm)
    }
  }
  static set_ghost_asset(){
    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
    next_pos = Ghost.warp(data , next_pos)
    if(!next_pos){return}
    const before_pos = Ghost.get_pos(data.coodinate)
    const after_pos  = Ghost.get_pos(next_pos)
    if(Pacman.is_collision(next_pos)){
      Ghost.hit(elm_ghost)
    }
    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) && 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)
    
    Ghost.set_pos(elm_ghost , Ghost.get_pos(data.next_pos))
    
    data.coodinate = data.next_pos
    
    if(Main.is_dead){return}
    if(Pacman.is_collision(data.coodinate)){
      Ghost.hit(elm_ghost)
    }
    // 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 warp(data, next_pos){
    if(!Frame.is_warp(next_pos)){return next_pos}
    data.coodinate = Frame.get_another_warp_pos(next_pos)
    return Frame.next_pos(data.direction , data.coodinate)
  }
  static hit(elm_ghost){
    switch(elm_ghost.getAttribute('data-status')){
      case 'weak':
        Ghost.dead(elm_ghost)
        break
      case 'dead':
        break
      default:
        Pacman.crashed(elm_ghost)
        Ghost.crashed(elm_ghost)
        return
    }
  }
  static get_pos(pos){
    return {
      x : pos.x * Frame.block_size,
      y : pos.y * Frame.block_size,
    }
  }
  static set_pos(elm_ghost , pos){
    elm_ghost.style.setProperty('left' , `${pos.x}px` , '')
    elm_ghost.style.setProperty('top'  , `${pos.y}px` , '')
  }
  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_stops()
    Frame.crashed()
  }
  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 remove_all(){
    for(const elm of Ghost.elm_ghosts){
      elm.parentNode.removeChild(elm)
    }
  }
  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
asset.jsを機能に加えたことで、フローにまとまりがでましたが、読み込みをするファイル一覧を指定する初期設定処理を追加しています。
import { Asset }   from './asset.js'
import { Frame }   from './frame.js'
import { Control } from './control.js'
import { Feed }    from './feed.js'
import { Footer }  from './footer.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,
  life_count         : 3,
}
function init(){
  new Asset({
    files:[
      {
        file   : 'assets/frame.json',
        target : 'frame',
        name   : 'asset_json',
        type   : 'data',
      },
      {
        file   : 'assets/ghost.json',
        target : 'ghost',
        name   : 'data_json',
        type   : 'data',
      },
      {
        file   : 'assets/ghost.html',
        target : 'ghost',
        name   : 'asset',
        type   : 'html',
      }
    ]
  }).then(()=>{
    new Footer()
    new Frame()
    new Control()
    new Feed()
  })
}
switch(document.readyState){
  case 'complete':
  case 'interactive':
    init()
    break
  default:
    window.addEventListener('DOMContentLoaded' , (()=>init()))
}
[全部更新] js/pacman.js
自機処理もできる限りstatic処理になるように変更しました。
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'
import { Footer }   from './footer.js'
export class Pacman{
  static init(){
    Footer.delete_life()
    Pacman.direction = null
    Pacman.next_pos  = null
    Pacman.create()
    Pacman.coodinates = Pacman.start_coodinates
    Frame.put(Pacman.elm, Pacman.coodinates)
    Pacman.elm.style.setProperty('--anim-speed' , `${Main.anim_speed}ms` , '')
  }
  static start(){
    if(!Control.direction){return}
    Pacman.move(Control.direction)
  }
  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('.frame-area .pacman')
  }
  static move(direction){
    if(Frame.is_ready){return}
    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){
    Pacman.elm.setAttribute('data-anim' , '')
    setTimeout(Pacman.dead , 1000)
  }
  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){
      console.log(anim.length)
      anim[0].pause()
    }
    else{
    }
  }
  static hidden(){
    Pacman.elm.style.setProperty('display','none','')
  }
  static remove(){
    Pacman.elm.parentNode.removeChild(Pacman.elm)
  }
  static close_mouse(){
    // Pacman.elm.setAttribute('data-status' , 'mouse-close')
  }
}
解説
無駄に長いな〜と思える関数は、できるだけ機能に応じた関数を作成して、分割することにより、再利用性をアップさせてみました。
ステージを切り替える時など、敵キャラと自キャラがうまくリセットされていなかったバグなどが見つかったので、内部データのリセットなどをframe.jsにまとめるのと、ghost.jsと、pacman.jsは、外部から実行するstatic関数をできるだけ同じ関数名にするようにしてみました。
このような、システム全体的な関数名仕様なども統一感がある方がその後の機能追加などの時に迷わなくて良くなりますね。
youtubeなどで、pacmanのプレイ動画を見ながらできる限り表示になるように、アニメーションなどを追加してみましたが、どうでしょう?
READY!と表示される文字フォントを、できる限り合わせようかな〜と思ったんですが、そのためにフォントデータを読み込むというデータの無駄ダウンロードは避けることにしました。
今後、どうしても雰囲気重視でフォントを合わせたいと思ったら、画像などでの対応にしたいと思います。
ゲームスタート時や、敵に喰われてしまった時など、ゲームを再開する時に、少し間を置く処理などを追加したら、いい感じにゲーム感が出てきた感じがします。
デモ
画面キャプチャ

簡単に遊べるように、リンク先にアクセスするだけで動くデモを用意しました。
お手軽にお楽しみください。
リンク : 
https://mynt-games.blogspot.com/p/pacman.html
あとがき
パックマンというゲームは、子供の頃に、学校で出入り禁止のゲームセンターに置いてあった憧れのゲームでした。
Canvasバージョンでのリメイク版はネット上でたくさん見ることができたんですが、HTML要素で動くバージョンは見たことが無かったので、今回作ってみたところ思いの外よくできたので、個人的に満足しています。
こうしたプログラミングって、自分のスキルアップに大きくつながるのですが、それを人に説明するという難しさも同時に体験しています。
ゲームを作ることでプログラミング学習をするのでいちばん有名なのは、
ぷよぷよプログラミングじゃないですかね?
作って、学んで、遊んで、楽しんで、自分のスキルをアップさせるって、なんだかとても有意義な時間を過ごしている感じがしませんか?
こんな楽しいことを他の人と共有しないなんて、それも損ですからね。
今後ももっとゲーム学習コンテンツ作っていきますね。
ご要望があれば、是非おたよりくださいませ。
知財
パックマンは、バンダイナムコ社の登録商標です。
PAC-MAN™ & ©1980 BANDAI NAMCO Entertainment Inc.
 
0 件のコメント:
コメントを投稿