[HTML + CSS + Javascript] パックマンをHTMLで作ってみるブログ#6 自機コントロール

2023年6月16日

Javascript ゲーム

eyecatch 見た目はもうパックマンそのものなので、前回まででもテンションが上った人も多いと思います。(自分もそうです) 今回は、自機をパソコンの上下左右カーソルキーで動かしてみます。 単に動かすだけですが、前回までのHTML構造ではうまく出来ないことがわかったので、大幅に変更をしています。

今回の目的

・パソコンのキーボードのカーソルキーで上下左右を押した時に、自キャラを移動させる

修正ファイル一覧

  1. index.html
  2. css/frame.css
  3. css/ghost.css
  4. css/pacman.css
  5. css/style.css
  6. js/control.js
  7. js/frame.js
  8. js/ghost.js
  9. js/main.js
  10. js/pacman.js

ソースコード(前回からの差分)

[全部更新]index.html

キャラクターの配置を、.frame-areaの中に入れました。 これにより、理由としては、ステージ内の座標を相対的に取得したかったので、左上をx:0,y:0とする必要があったので、移動させました。 <!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'> <div class='pacman'></div> <div class='ghost' data-color='1'></div> <div class='ghost' data-color='2'></div> <div class='ghost' data-color='3'></div> <div class='ghost' data-color='4'></div> </div> </body> </html>

[全部更新] css/frame.css

index.htmlの修正に伴って、cssも変更する必要があったので、内容をガラッと変更。 すべて書き換えて下さい。 "position:relative"を追加して、それに伴って、内部にアクセスするセレクタを変更しています。 :root{ --color : blue; --border : 2px; } .frame-area{ position:relative; display:inline-grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } .frame-area, .frame-area p, .frame-area p::before, .frame-area p::after{ font-size:0; -webkit-box-sizing : border-box; -moz-box-sizing : border-box; -o-box-sizing : border-box; -ms-box-sizing : border-box; box-sizing : border-box; } .frame-area > p{ width : var(--block); height : var(--block); font-size : 0; display : block; position : relative; /* border : 1px solid rgba(255,0,0,0.5); */ } .frame-area .D1::after, .frame-area .D2::after, .frame-area .D3::after, .frame-area .D4::after, .frame-area .D6::after, .frame-area .D7::after, .frame-area .D8::after, .frame-area .D9::after, .frame-area .Da::after, .frame-area .Db::after, .frame-area .Dc::after, .frame-area .Dd::after, .frame-area .De::after, .frame-area .Df::after, .frame-area .Dg::after, .frame-area .Dh::after{ width : 100%; height: 100%; top:0; left:0; border-style : solid; border-color : var(--color); } .frame-area > p::before, .frame-area > p::after{ content : ''; font-size : 0; display : block; border-style : solid; border-color : var(--color); border-width : 0; position : absolute; } .frame-area .S1::before, .frame-area .D1::before, .frame-area .Db::before, .frame-area .Dd::before{ width : calc(var(--block) / 2); height : calc(var(--block) / 2); bottom : 0; right : 0; border-top-width : var(--border); border-left-width : var(--border); border-top-left-radius : 100%; } .frame-area .S2::before, .frame-area .D2::before{ width : 100%; height : calc(var(--block) / 2); bottom : 0; left : 0; border-top-width : var(--border); } .frame-area .S3::before, .frame-area .D3::before, .frame-area .Da::before, .frame-area .Df::before{ width : calc(var(--block) / 2); height : calc(var(--block) / 2); bottom : 0; left : 0; border-top-width : var(--border); border-right-width : var(--border); border-top-right-radius : 100%; } .frame-area .S4::before, .frame-area .D4::before{ width : calc(var(--block) / 2); height : 100%; top : 0; right : 0; border-left-width : var(--border); } .frame-area .S5::before, .frame-area .D5::before, .frame-area .D5::after{ border-width : 0; } .frame-area .S6::before, .frame-area .D6::before{ width : calc(var(--block) / 2); height : 100%; top : 0; left : 0; border-right-width : var(--border); } .frame-area .S7::before, .frame-area .D7::before, .frame-area .Dc::before, .frame-area .Dh::before{ width : calc(var(--block) / 2); height : calc(var(--block) / 2); top : 0; right : 0; border-bottom-width : var(--border); border-left-width : var(--border); border-bottom-left-radius : 100%; } .frame-area .S8::before, .frame-area .D8::before{ width : 100%; height : calc(var(--block) / 2); top : 0; left : 0; border-bottom-width : var(--border); } .frame-area .S9::before, .frame-area .D9::before, .frame-area .De::before, .frame-area .Dg::before{ width : calc(var(--block) / 2); height : calc(var(--block) / 2); top : 0; left : 0; border-bottom-width : var(--border); border-right-width : var(--border); border-bottom-right-radius : 100%; } .frame-area .D1::after{ border-width : var(--border) 0 0 var(--border); border-radius : 100% 0 0 0; } .frame-area .D2::after, .frame-area .Da::after, .frame-area .Db::after{ border-top-width : var(--border); } .frame-area .D3::after{ border-width : var(--border) var(--border) 0 0; border-radius : 0 100% 0 0; } .frame-area .D4::after, .frame-area .Dc::after, .frame-area .Dd::after{ border-left-width : var(--border); } .frame-area .D6::after, .frame-area .De::after, .frame-area .Df::after{ border-right-width : var(--border); } .frame-area .D7::after{ border-width : 0 0 var(--border) var(--border); border-radius : 0 0 0 100%; } .frame-area .D8::after, .frame-area .Dg::after, .frame-area .Dh::after{ border-bottom-width : var(--border); } .frame-area .D9::after{ border-width : 0 var(--border) var(--border) 0; border-radius : 0 0 100% 0; } .frame-area .P1, .frame-area .P2{ position:relative; } .frame-area .P1:before{ position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width : calc(var(--block) / 4); height : calc(var(--block) / 4); border-radius : 50%; background-color:yellow; } .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; }

[一部更新] css/ghost.css

先頭の、.ghost部分のみ、下記のコードに入れ替えて下さい。 .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)); }

[一部更新+追加] css/pacman.css

セレクターが.pacmanの箇所(一番上です)を下記のコードに入れ替えてください。 これにより、pacmanが座標でコントロールできるようになります。 .pacman{ position:absolute; display:inline-block; width:var(--size-chara); height:var(--size-chara); } 同じファイルの最後に次のコードを追加 .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); }

[一部更新] css/style.css

グローバルプロパティ変数に、ブロックのサイズを追加しました。 下記の赤い箇所1行のみを追加してください。 :root{ --block : 16px; --size-chara : 32px; }

[新規追加] js/control.js

キー操作の為のイベント管理をする命令を追加しました。 パソコンのキーボードだけでの操作にしています。(スマホは動作しません、スミマセン・・・) import { Pacman } from './pacman.js' export class Control{ constructor(){ window.addEventListener('keydown' , this.keydown.bind(this)) window.addEventListener('keyup' , this.keyup.bind(this)) } static key2name(key){ switch(key){ case 37 : return 'left' case 38 : return 'up' case 39 : return 'right' case 40 : return 'down' } } keydown(e){ if(e.repeat === true){return} const key = e.keyCode const name = Control.key2name(key) if(!name){return} Control.key_data = { key_code : key, name : name, } Pacman.move(Control.key_data) } keyup(e){ if(!Control.key_data){return} const name = Control.key2name(e.keyCode) if(Control.key_data.name !== name){return} delete Control.key_data } }

[全部更新] 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 } 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() } }).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) } this.finish() } 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` , '') } }

[全部更新] js/ghost.js

キャラクタ配置を変更したため、このモジュールも大きく書き換えました。 import { Frame } from './frame.js' export class Ghost{ constructor(){ this.load_asset() } static get elm_ghosts(){ return document.querySelectorAll('.ghost') } static start_coodinates = [ { x : 12, y : 11 }, { x : 15, y : 11 }, { x : 12, y : 14 }, { x : 15, y : 14 }, ] 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.ghost_set() } }).bind(this) xhr.send() } ghost_set(){ const elm_ghosts = Ghost.elm_ghosts for(const elm_ghost of elm_ghosts){ Frame.put(elm_ghost, Ghost.start_coodinates.shift()) elm_ghost.innerHTML = this.asset this.change_eye(elm_ghost) } } static get directions(){ return ['right','left','top','bottom'] } change_eye(elm_ghost){ const num = Math.floor(Math.random() * Ghost.directions.length) elm_ghost.querySelector('.face').setAttribute('data-direction' , Ghost.directions[num]) setTimeout(this.change_eye.bind(this , elm_ghost) , 3000) } }

[全部更新] js/main.js

frame.jsを起動時に実行する必要があったので、それに伴って起動処理をするmain.jsを書き換えました。 import { Ghost } from './ghost.js' import { Frame } from './frame.js' import { Control } from './control.js' import { Pacman } from './pacman.js' export const Main = {} function init(){ new Frame().then(()=>{ new Ghost() new Pacman() new Control() }) } switch(document.readyState){ case 'complete': case 'interactive': init() break default: window.addEventListener('DOMContentLoaded' , (()=>init())) }

[新規追加] js/pacman.js

自キャラの動きを制御する処理を作りました。 単純にキー操作で動くだけで、まだ壁判定やワープ処理などはやっていません。 import { Frame } from './frame.js' import { Control } from './control.js' export class Pacman{ // 初期表示座標処理 constructor(){ Pacman.anim_speed = 400 Pacman.coodinates = this.start_coodinates Frame.put(this.elm, Pacman.coodinates) this.elm.style.setProperty('--anim-speed' , `${Pacman.anim_speed}ms` , '') } get start_coodinates(){ return { x : 14, y : 23, } } get elm(){ return Pacman.elm } static get elm(){ return document.querySelector('.pacman') } static move(key_data){ if(Pacman.key_data ){ if(Pacman.key_data.name !== key_data.name){ Pacman.key_data = key_data } return } Pacman.key_data = key_data this.elm.setAttribute('data-anim' , "true") this.moving() } static moving(){ const next_pos = { x : Pacman.coodinates.x, y : Pacman.coodinates.y, } switch(Pacman.key_data.name){ case 'left': next_pos.x -= 1 break case 'right': next_pos.x += 1 break case 'up': next_pos.y -= 1 break case 'down': next_pos.y += 1 break default: return } this.elm.setAttribute('data-direction' , Pacman.key_data.name) this.elm.animate( [ { left : `${Pacman.coodinates.x * Frame.block_size}px`, top : `${Pacman.coodinates.y * Frame.block_size}px`, }, { left : `${next_pos.x * Frame.block_size}px`, top : `${next_pos.y * Frame.block_size}px`, } ], { duration: Pacman.anim_speed } ) Promise.all(this.elm.getAnimations().map(e => e.finished)).then(()=>{ Pacman.moved(next_pos) }) } static moved(next_pos){ if(!Pacman.key_data){return} Pacman.coodinates = next_pos Frame.put(this.elm, Pacman.coodinates) if(Control.key_data){ Pacman.moving() } else{ this.elm.setAttribute('data-anim' , "") delete Pacman.key_data } } }

画面キャプチャ

あとがき

キーボードのカーソルキーを押している間に、その方向の座標の値をインクリメントすればいいか・・・と甘く考えていましたが、 実際は、壁やエサパーツのグリッドに沿ってそのサイズ単位で移動しないといけないという事がわかり、少し特殊な方法で座標移動をさせるようにしました。 pacman.jsの下の方に書いてある、move -> moving -> movedの箇所がソレで、animate()機能を使って、移動処理をしています。 0.3秒ほどの操作誤差が発生していますが、それほど違和感がないので、この方式での安定感を感じました。 次回は、コリジョン判定(壁の衝突判定)を行います。

知財

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

人気の投稿

このブログを検索

ごあいさつ

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

ブログ アーカイブ