[Javascript] パックマンをHTMLで作ってみるブログ#17 モバイルコントロール機能

2023年7月31日

Javascript

eyecatch SNSでパックマンを公開してみたところ、ほとんどの人がスマホで見ていたらしく、「操作できない」コメントをたくさんいただいたので、重い腰を上げてスマホ対応をしてみたいと思います。 スマホゲームはこれまで何度も作ってきたのでポイントは押さえているのですが、比較的直感的に操作できる方法で構築してみました。

今回の作業内容

  1. 前回修正した自機と敵キャラの変更に伴うモジュール修正
  2. スマホ用のタッチイベント操作の導入

修正ファイル一覧

  1. index.html
  2. assets/ghost.html
  3. css/footer.css
  4. css/ghost.css
  5. css/mobile.css
  6. css/pacman.css
  7. css/style.css
  8. js/control.js
  9. js/frame.js
  10. js/ghost.js
  11. js/main.js
  12. js/mobile.js
  13. js/pacman.js

ソースコード

[全部更新] index.html

<!DOCTYPE html> <html lang='ja'> <head> <meta charset='utf-8' /> <title>Pacman for HTML</title> <meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0" name="viewport"> <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='pacman-root'> <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> </div> </body> </html>

[全部更新] assets/ghost.html

<div class='eye-l'></div> <div class='eye-r'></div> <div class='mouse'></div>

[全部更新] css/footer.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; } .frame-footer .memo{ color:white; /* display:none; */ }

[全部更新] css/ghost.css

.ghost{ position : absolute; z-index : 10; display : inline-block; width : 32px; height : 32px; transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)); } .ghost::before{ content:''; position:absolute; display : block; width : 100%; height : 100%; clip-path : path('M 0,24 Q 0,0 16,0 Q 32,0 32,24 L 32,30 Q 30,34 26,30 Q 23,26 19,30 Q 16,34 12,30 Q 10,26 6,30 Q 2,34 0,30 T 0,30 z'); } .ghost[data-color='1']::before{ background-color : red; } .ghost[data-color='2']::before{ background-color : orange; } .ghost[data-color='3']::before{ background-color : lightblue; } .ghost[data-color='4']::before{ background-color : pink; } .ghost[data-status='weak']::before{ background-color : blue; } .ghost .eye-l, .ghost .eye-r{ position:absolute; top:8px; width:8px; background-color:white; width:9px; height:9px; border-radius:50%; } .ghost .eye-l{ left:5px; } .ghost .eye-r{ right:5px; } .ghost .eye-l::before, .ghost .eye-r::before{ content:''; position:absolute; left:50%; top:50%; width:50%; height:50%; background-color:black; transform:translate(-50%,-50%); border-radius:50%; } .ghost[data-direction='left'] .eye-l{ left:0px; } .ghost[data-direction='left'] .eye-r{ right:10px; } .ghost[data-direction='right'] .eye-l{ left:10px; } .ghost[data-direction='right'] .eye-r{ right:0px; } .ghost[data-direction='up'] .eye-l, .ghost[data-direction='up'] .eye-r{ top:2px; } .ghost[data-direction='down'] .eye-l, .ghost[data-direction='down'] .eye-r{ top:16px; } .ghost[data-direction='left'] .eye-l::before, .ghost[data-direction='left'] .eye-r::before{ left:0; right:auto; transform:translate(0,-50%); } .ghost[data-direction='right'] .eye-l::before, .ghost[data-direction='right'] .eye-r::before{ left:auto; right:0; transform:translate(0,-50%); } .ghost[data-direction='up'] .eye-l::before, .ghost[data-direction='up'] .eye-r::before{ top:0; bottom:auto; transform:translate(-50%,0); } .ghost[data-direction='down'] .eye-l::before, .ghost[data-direction='down'] .eye-r::before{ top:auto; bottom:0; transform:translate(-50%,0); } .ghost[data-status='weak'] .eye-l{ top:8px; left:8px; } .ghost[data-status='weak'] .eye-r{ top:8px; right:8px; } .ghost[data-status='weak'] .eye-l, .ghost[data-status='weak'] .eye-r{ width:6px; height:6px } .ghost[data-status='weak'] .eye-l::before, .ghost[data-status='weak'] .eye-r::before{ opacity:0; } .ghost[data-status='weak'] .mouse{ position:absolute; top:16px; left:0; width:100%; height:8px; background-color:#FFF; clip-path : path('M 2,6 Q 4,2 6,4 Q 8,8 10,4 Q 12,2 14,4 Q 16,8 18,4 Q 20,2 22,4 Q 24,8 26,4 Q 28,2 30,6 L 30,7 Q 28,3 26,5 Q 24,9 22,5 Q 20,3 18,5 Q 16,9 14,5 Q 12,3 10,5 Q 8,9 6,5 Q 4,3 2,7'); } .ghost[data-status='dead']::before{ background-color : transparent; }

[新規追加] css/mobile.css

html{ width:100%; padding:0; margin:0; } body{ width:100%; overflow:hidden; padding:0; margin:0; } .pacman-root{ transform-origin:top left; }

[全部更新] css/pacman.css

.pacman{ position : absolute; z-index : 10; display : inline-block; width : var(--size-chara); height : var(--size-chara); } .pacman::before{ content:''; background-color : yellow; display : block; width : 100%; height : 100%; border-radius : 50%; } /* パクパク処理 */ .pacman[data-status="anim"]::before{ animation-name : pacman_anim; animation-duration:var(--anim-speed); animation-timing-function: ease-in-out; animation-iteration-count: infinite; } @keyframes pacman_anim{ 0%{ clip-path: polygon( 50% 60%, 50% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%, 50% 0%, 50% 60% ); } 50%{ clip-path: polygon( 50% 60%, 100% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%, 0% 0%, 50% 60% ); } 100%{ clip-path: polygon( 50% 60%, 50% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%, 50% 0%, 50% 60% ); } } /* ステージ座標とのズレ解消用処理 */ .pacman{ transform : translate(calc(var(--block) / -2) , calc(var(--block) / -2)); } .pacman::before, .pacman[data-direction="left"]::before{ transform: rotate(-90deg); } .pacman[data-direction="right"]::before{ transform:rotate(90deg); } .pacman[data-direction="up"]::before{ transform: rotate(0deg); } .pacman[data-direction="down"]::before{ transform: scaleY(-1); } /* 動き停止 */ .frame-area[data-status='clear'] .pacman, .pacman[data-status='crashed']{ animation-play-state: paused; } /* Dead処理 */ .pacman[data-status="dead"]::before{ animation-name : pacman-dead; 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; } @keyframes pacman-dead{ 0%{ clip-path: polygon( 50% 50%, 50% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%, 50% 0%, 50% 50% ); } 25%{ clip-path: polygon( 50% 50%, 100% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%, 0% 0%, 50% 50% ); } 75%{ clip-path: polygon( 50% 50%, 100% 100%, 100% 100%, 100% 100%, 0% 100%, 0% 100%, 0% 100%, 50% 50% ); } 100%{ clip-path: polygon( 50% 50%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 50% ); } }

[一部更新] css/style.css

@import 'frame.css'; @import 'pacman.css'; @import 'ghost.css'; @import 'footer.css'; @import 'mobile.css'; :root{ --block : 16px; --size-chara : calc(var(--block) * 2); }

[全部更新] js/control.js

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 direction = Control.key2name(key) if(!direction){return} Control.direction = direction Pacman.move(Control.direction) } static keyup(e){ if(!Control.direction){return} // Control.clear() } static clear(){ if(!Control.direction){return} delete Control.direction } }

[全部更新] 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' import { Mobile } from './mobile.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 return Mobile.block_size } 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) elm_ghost.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

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' import { Mobile } from './mobile.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() new Mobile() }) } switch(document.readyState){ case 'complete': case 'interactive': init() break default: window.addEventListener('DOMContentLoaded' , (()=>init())) }

[新規追加] js/mobile.js

import { Frame } from './frame.js' import { Css } from './css.js' export class Mobile{ constructor(){ this.set_window_size() } get window_size(){ return window.innerWidth } get frame_size(){ return Frame.root.offsetWidth } get elm_pacman_root(){ return document.querySelector('.pacman-root') } static get block_size(){ return Mobile.data_block_size || document.querySelector('.S5').offsetWidth } set_window_size(){ if(this.window_size >= this.frame_size){return} const rate = this.window_size / this.frame_size // const value = Css.get_css(':root' , '--block') // const block_size = Number(value.replace('px','')) // const new_size = block_size * rate // Css.set_css(':root' , '--block' , `${new_size}px`) // Mobile.data_block_size = new_size // console.log(value,block_size,new_size) this.elm_pacman_root.style.setProperty('transform',`scale(${rate})`,'') // console.log(Css.get_css(':root' , '--block')) } set_event(){ if(typeof window.ontouchstart !== 'undefined'){ window.addEventListener('touchstart' , Control.touchstart.bind(this)) window.addEventListener('touchmove' , Control.touchmove.bind(this)) window.addEventListener('touchend' , Control.touchend.bind(this)) } } static touchstart(e){ console.log(e) } static touchmove(e){ console.log(e) } static touchend(e){ console.log(e) } }

[全部更新] js/mobile.js

import { Frame } from './frame.js' import { Pacman } from './pacman.js' import { Control } from './control.js' export class Mobile{ constructor(){ this.set_window_size() this.set_event() } get window_size(){ return window.innerWidth } get frame_size(){ return Frame.root.offsetWidth } get elm_pacman_root(){ return document.querySelector('.pacman-root') } static get block_size(){ return Mobile.data_block_size || document.querySelector('.S5').offsetWidth } static get elm_memo(){ return document.querySelector('.frame-footer .memo') } set_window_size(){ if(this.window_size >= this.frame_size){return} const rate = this.window_size / this.frame_size this.elm_pacman_root.style.setProperty('transform',`scale(${rate})`,'') } set_event(){ if(typeof window.ontouchstart !== 'undefined'){ window.addEventListener('touchstart' , Mobile.touchstart.bind(this)) window.addEventListener('touchmove' , Mobile.touchmove.bind(this)) window.addEventListener('touchend' , Mobile.touchend.bind(this)) } } static touchstart(e){ Mobile.touch_datas = { pos : { x : e.touches[0].pageX, y : e.touches[0].pageY, }, direction : null, } Mobile.view_memo() } static touchmove(e){ if(!Mobile.touch_datas){return} Mobile.touch_datas.direction = Control.direction || null const pos = { x : e.touches[0].pageX, y : e.touches[0].pageY, } Control.direction = Mobile.get_direction(pos) if(Control.direction === Mobile.touch_datas.direction){return} Mobile.view_memo() Mobile.pacman_move(Control.direction) } static touchend(e){ if(Mobile.touch_datas){ delete Mobile.touch_datas } if(Control.direction){ delete Control.direction } } static get_direction(pos){ if(!Mobile.touch_datas){return} const min = 10 const diff_pos = { x : pos.x - Mobile.touch_datas.pos.x, y : pos.y - Mobile.touch_datas.pos.y, } const volume = { x : Math.abs(diff_pos.x), y : Math.abs(diff_pos.y), } diff_pos.x = volume.x >= min ? diff_pos.x : 0 diff_pos.y = volume.y >= min ? diff_pos.y : 0 // vertical if(diff_pos.x && volume.x > volume.y){ if(diff_pos.x < 0){ return 'left' } else{ return 'right' } } // horizontal else if(diff_pos.y && volume.x < volume.y){ if(diff_pos.y < 0){ return 'up' } else{ return 'down' } } // other return null } static view_memo(str){ // console.log(Mobile.elm_memo,Mobile.touch_datas.direction) Mobile.elm_memo.textContent = str || Control.direction || '' } static pacman_move(direction){ if(!direction){return} // Control.direction = direction Pacman.move(direction) } }

[全部更新] 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' 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-status' , "anim") 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-status' , "") 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') } }

画面表示イメージ

デモプレイ

HTML版Pacman github Pagesで公開しています。 スマホでアクセスして、遊んでみてください。

知財

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

このブログを検索

ごあいさつ

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