[HTML + CSS + Javascript] パックマンをHTMLで作ってみるブログ#5 エサ表示と構造化

2023年6月12日

CSS HTML Javascript ゲーム

eyecatch 前回から2ヶ月近く空いてしまいましたが、今回は、パックマンの画面で一気にテンションがあがるモジュールが、エサ部分です。 前回まで作ったパーツを一同に画面に乗せて、さらに、ステージにエサを設置してみたいと思います。 あと、これまでと違って、プログラミングを意識した作業を行うので、新しいフォルダを作って今回のコードをコピペしてみてください。

今回の目的

1. プログラムファイルの構成とソースコード 2. ステージデータをjsonデータで管理する 3. 敵キャラのhtmlをJavascriptで設置
これまでは、HTML + CSSでの構成で単一パーツを構築してきましたが、 今回から、本格的にゲームページを意識したファイル構成にしてみたいと思います。 とりあえず、今回の構成は以下のように作ってみました。
index.html ├ assets/ │   ├ frame.json │   └ ghost.html ├css/ │ ├ frame.css │ ├ ghost.css │ ├ pacman.css │ └ style.css └js/ ├ frame.js ├ ghost.js └ main.js

index.html

<link rel='stylesheet' href='css/style.css' /> <script type='module' src='js/main.js'></script> <style>body{background-color:black}</style> <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> <div class='frame-area'></div> </div>

assets/frame.json

[ "D1","D2","D2","D2","D2","D2","D2","D2","D2","D2","D2","D2","D2","Da","Db","D2","D2","D2","D2","D2","D2","D2","D2","D2","D2","D2","D2","D3", "D4","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","S4","S6","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","D6", "D4","P1","S1","S2","S2","S3","P1","S1","S2","S2","S2","S3","P1","S4","S6","P1","S1","S2","S2","S2","S3","P1","S1","S2","S2","S3","P1","D6", "D4","P2","S4","S5","S5","S6","P1","S4","S5","S5","S5","S6","P1","S4","S6","P1","S4","S5","S5","S5","S6","P1","S4","S5","S5","S6","P2","D6", "D4","P1","S7","S8","S8","S9","P1","S7","S8","S8","S8","S9","P1","S7","S9","P1","S7","S8","S8","S8","S9","P1","S7","S8","S8","S9","P1","D6", "D4","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","D6", "D4","P1","S1","S2","S2","S3","P1","S1","S3","P1","S1","S2","S2","S2","S2","S2","S2","S3","S5","S1","S3","P1","S1","S2","S2","S3","P1","D6", "D4","P1","S7","S8","S8","S9","P1","S4","S6","P1","S7","S8","S8","S3","S1","S8","S8","S9","S5","S4","S6","P1","S7","S8","S8","S9","P1","D6", "D4","P1","P1","P1","P1","P1","P1","S4","S6","P1","S5","S5","S5","S4","S6","S5","S5","S5","S5","S4","S6","P1","P1","P1","P1","P1","P1","D6", "D7","D8","D8","D8","D8","S3","P1","S4","S7","S2","S2","S3","S5","S4","S6","S5","S1","S2","S2","S9","S6","P1","S1","D8","D8","D8","D8","D9", "S5","S5","S5","S5","S5","D4","P1","S4","S1","S8","S8","S9","S5","S7","S9","S5","S7","S8","S8","S3","S6","P1","D6","S5","S5","S5","S5","S5", "S5","S5","S5","S5","S5","D4","P1","S4","S6","S5","S5","S5","S5","S5","S5","S5","S5","S5","S5","S4","S6","P1","D6","S5","S5","S5","S5","S5", "S5","S5","S5","S5","S5","D4","P1","S4","S6","S5","S1","D8","D8","D8","D8","D8","D8","S3","S5","S4","S6","P1","D6","S5","S5","S5","S5","S5", "D2","D2","D2","D2","D2","S9","P1","S7","S9","S5","D6","S5","S5","S5","S5","S5","S5","D4","S5","S7","S9","P1","S7","D2","D2","D2","D2","D2", "S5","S5","S5","S5","S5","S5","P1","S5","S5","S5","D6","S5","S5","S5","S5","S5","S5","D4","S5","S5","S5","P1","S5","S5","S5","S5","S5","S5", "D8","D8","D8","D8","D8","S3","P1","S1","S3","S5","D6","S5","S5","S5","S5","S5","S5","D4","S5","S1","S3","P1","S1","D8","D8","D8","D8","D8", "S5","S5","S5","S5","S5","D4","P1","S4","S6","S5","S7","D2","D2","D2","D2","D2","D2","S9","S5","S4","S6","P1","D6","S5","S5","S5","S5","S5", "S5","S5","S5","S5","S5","D4","P1","S4","S6","S5","S5","S5","S5","S5","S5","S5","S5","S5","S5","S4","S6","P1","D6","S5","S5","S5","S5","S5", "S5","S5","S5","S5","S5","D4","P1","S4","S6","S5","S1","S2","S2","S2","S2","S2","S2","S3","S5","S4","S6","P1","D6","S5","S5","S5","S5","S5", "D1","D2","D2","D2","D2","S9","P1","S7","S9","S5","S7","S8","S8","S3","S1","S8","S8","S9","S5","S7","S9","P1","S7","D2","D2","D2","D2","D3", "D4","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","S4","S6","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","D6", "D4","P1","S1","S2","S2","S3","P1","S1","S2","S2","S2","S3","P1","S4","S6","P1","S1","S2","S2","S2","S3","P1","S1","S2","S2","S3","P1","D6", "D4","P1","S7","S8","S3","S6","P1","S7","S8","S8","S8","S9","P1","S7","S9","P1","S7","S8","S8","S8","S9","P1","S4","S1","S8","S9","P1","D6", "D4","P2","P1","P1","S4","S6","P1","P1","P1","P1","P1","P1","P1","S5","S5","P1","P1","P1","P1","P1","P1","P1","S4","S6","P1","P1","P2","D6", "Dc","S2","S3","P1","S4","S6","P1","S1","S3","P1","S1","S2","S2","S2","S2","S2","S2","S3","P1","S1","S3","P1","S4","S6","P1","S1","S2","De", "Dd","S8","S9","P1","S7","S9","P1","S4","S6","P1","S7","S8","S8","S3","S1","S8","S8","S9","P1","S4","S6","P1","S7","S9","P1","S7","S8","Df", "D4","P1","P1","P1","P1","P1","P1","S4","S6","P1","P1","P1","P1","S4","S6","P1","P1","P1","P1","S4","S6","P1","P1","P1","P1","P1","P1","D6", "D4","P1","S1","S2","S2","S2","S2","S9","S7","S2","S2","S3","P1","S4","S6","P1","S1","S2","S2","S9","S7","S2","S2","S2","S2","S3","P1","D6", "D4","P1","S7","S8","S8","S8","S8","S8","S8","S8","S8","S9","P1","S7","S9","P1","S7","S8","S8","S8","S8","S8","S8","S8","S8","S9","P1","D6", "D4","P1","P1","P1","P1","P1","P1","P1","p1","P1","p1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","P1","D6", "D7","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D8","D9" ]

assets/ghost.html

<div class='head'></div> <div class='face'> <div class='eye-left'></div> <div class='eye-right'></div> </div> <div class='under'> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 30"> <path fill='red' d="M 0,0 Q 0,15 5,20 T 20,15 40,15 60,15 80,15 100,15 L 100,0" fill='red'> <animate attributeName='d' dur='500ms' repeatCount='indefinite' calcMode="linear" values='M 0,0 Q 0,15 5,20 T 15,15 35,15 55,15 75,15 100,15 L 100,0; M 0,0 Q 0,15 5,20 T 25,15 45,15 65,15 85,15 100,15 L 100,0; M 0,0 Q 0,15 5,20 T 15,15 35,15 55,15 75,15 100,15 L 100,0;' /> </path> </svg> </div>

css/frame.css

:root{ --color : blue; --block : 16px; --border : 2px; } .frame-area{ 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 *, .frame-area *::before, .frame-area *::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 > *{ 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 > *::before, .frame-area > *::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{ 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); } .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; } .ghost > .face{ height : 30%; background-color : var(--color-body); position:relative; } .ghost > .face > .eye-left, .ghost > .face > .eye-right{ position:absolute; top:-75%; background-color:white; width : var(--size-eye); height : var(--size-eye); background-color:white; border-radius:50%; } .ghost > .face .eye-left{ left:13%; } .ghost > .face .eye-right{ right:13%; } .ghost:not([data-status='weak']) > .face > *::before{ content:''; display:block; background-color:black; width : 50%; height : 50%; margin : 25%; border-radius:50%; transition-property : margin; transition-duration : 0.3s; } .ghost:not([data-status='weak']) > .face[data-direction='right'] > *::before{ margin-left : 50%; margin-right : 0; } .ghost:not([data-status='weak']) > .face[data-direction='left'] > *::before{ margin-left : 0; margin-right : 50%; } .ghost:not([data-status='weak']) > .face[data-direction='top'] > *::before{ margin-top : 0; margin-bottom : 50%; } .ghost:not([data-status='weak']) > .face[data-direction='bottom'] > *::before{ margin-top : 50%; margin-bottom : 0; } .ghost[data-status='weak'] > .face > .eye-left, .ghost[data-status='weak'] > .face > .eye-right{ width : calc(var(--size-eye) / 2); height : calc(var(--size-eye) / 2); background-color:white; border-radius:50%; } .ghost[data-status='weak'] > .face > .eye-left{ left:25%; } .ghost[data-status='weak'] > .face > .eye-right{ right:25%; } .ghost[data-status='weak'] > .face > .mouse{ margin-top:10px; } .ghost > .under{ height : var(--size-under); } .ghost > .under path{ fill:var(--color-body); }

css/pacman.css

.pacman{ /* position:absolute; */ display:inline-block; top:100px; left:100px; width:var(--size-chara); height:var(--size-chara); } .pacman::before, .pacman::after{ content:''; display:block; width:100%; height:50%; background-color:yellow; animation-duration:0.4s; animation-timing-function: ease-in-out; animation-iteration-count: infinite; } .pacman::before{ transform-origin:center bottom; border-radius:var(--size-chara) var(--size-chara) 0 0; transform:rotate(45deg); animation-name : pacman_before; } .pacman::after{ transform-origin:center top; border-radius:0 0 var(--size-chara) var(--size-chara); transform:rotate(-45deg); animation-name : pacman_after; } @keyframes pacman_before{ 0%{ transform:rotate(45deg); } 50%{ transform:rotate(0deg); } 100%{ transform:rotate(45deg); } } @keyframes pacman_after{ 0%{ transform:rotate(-45deg); } 50%{ transform:rotate(0deg); } 100%{ transform:rotate(-45deg); } }

css/style.css

@import 'frame.css'; @import 'pacman.css'; @import 'ghost.css'; :root{ --size-chara : 32px; }

js/frame.js

export class Frame{ constructor(){ Frame.stage_datas = this.stage_datas = [] this.load_asset() } static get root(){ return document.querySelector(`.frame-area`) } 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.stage_datas[i] = this.frame_datas[i] === 'S5' ? 1 : 0 } } }

js/ghost.js

export class Ghost{ constructor(){ this.load_asset() } static get elm_ghosts(){ return document.querySelectorAll('.ghost') } 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){ 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

import { Ghost } from './ghost.js' import { Frame } from './frame.js' export const Main = {} Main.frame = new Frame() Main.ghost = new Ghost() 前回、HTMLでベタにタグで書いたステージデータを、jsonデータに変換して、 assetフォルダにある、frame.jsonに書き込んでみました。 js/frame.jsでは、frame.jsonを元に、HTMLタグに変換して設置するようにすることで、index.htmlの内容をシンプルにすることができました。 何か修正する時も、jsonフォーマットにしておいたほうが、見やすいし修正し易いと思います。 frame.jsでは、jsonの読み込みをして読み込んだデータをObject(配列)データに変換して、それをHTMLに配置する内容が1ファイルで書かれています。 ちなみに、前回の内容に、エサのドット情報を付与して、同時に表示しているので、より本物に近づいていると思います。 敵キャラは、HTML+CSS+Javascriptで構成していたので、専用のghost.jsを設置しました。 内容としては、asset/ghost.htmlを読み込んで、それぞれ4体の敵キャラ内に設置をして、タイプを振り分けます。 次に、それらのキャラの目アニメーションを起動してますが、これは本来動きに合わせて目を動かすことになるので、後に変更されるでしょう。

まとめ

これまで作ったパーツをまとめて1つのゲームに組み立てていく作業は、いつもドキドキしてしまいます。 うまく動かなかったり、想定外のコンフリクトが起きるのは当たり前なので、それらを即座に対処して、うまく出来たらそれはそれでモチベーションがアップしてしまいます。 とりあえず、今後はJavascriptのプログラミングが中心になるので、これまでは単なる関数で書いていたのを、class構成にして構築してあります。 もし、class記述がよくわからないという人は、わからない箇所を学習しながらブログを読み勧めてもらえると、プログラミング学習も進むのではないかと思います。 とりあえず、見た目は完璧と自画自賛できたので、次は実際にキャラクターをカーソルなどで動かしてみたいと思います。 お楽しみに!

知財

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

このブログを検索

ごあいさつ

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