ゲーム内部の処理が大体出来上がってきたので、今回はステージクリアの処理を追加したいと思います。
今回の目的
- ステージクリア(エサを全部食べた時)に次のステージに進む
- クリア時に、敵キャラの動きを止めて非表示にする
- クリア時に、自キャラの動きを止める
- ステージ切り替えに必要な各種データのリセット処理
対象ファイル一覧
- index.html
- assets/ghost.json
- css/frame.css
- css/ghost.css
- css/pacman.css
- js/control.js
- js/feed.js
- js/frame.js
- js/ghost.js
- js/main.js
- js/pacman.js
軽い気持ちで作業をしてみたところ、結構大本の箇所の修正なども行う必要があったので、ほとんどのソース修正が必要になりました。
ソースコード
[全部更新] index.html
自キャラのエレメントを、動的に作成するようにしました。
<!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>
</body>
</html>
[新規追加] assets/ghost.json
敵キャラの設定データをjsonファイル化しました。
[
{
"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 }
}
]
[最後に追加] css/frame.css
ステージクリアした時の、ステージ枠の色アニメーションをcssで追加しました。
.frame-area[data-status='clear']{
animation-name : stage-clear;
animation-duration:2s;
animation-timing-function: linear;
animation-iteration-count: 1;
}
@keyframes stage-clear{
0%{
--color:blue;
}
10%{
--color:white;
}
20%{
--color:blue;
}
30%{
--color:white;
}
40%{
--color:blue;
}
50%{
--color:white;
}
60%{
--color:blue;
}
70%{
--color:white;
}
80%{
--color:blue;
}
90%{
--color:white;
}
100%{
--color:blue;
}
}
[一行削除] css/ghost.css
不要な処理を1行だけ削除しました。
赤色の行を消してください。
.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;}
/* .frame-area .ghost[data-status='1']{--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;
}
}
[一部追加] css/pacman.css
クリアした時のcss処理を追加しました。
赤字の箇所のみ更新してもらうだけで大丈夫です。
.pacman{
position:absolute;
z-index:10;
display:inline-block;
width:var(--size-chara);
height:var(--size-chara);
}
.frame-area[data-status='clear'] .pacman::before,
.frame-area[data-status='clear'] .pacman::after,
.pacman[data-status='crashed']::before,
.pacman[data-status='crashed']::after{
animation-play-state: paused;
}
.pacman::before,
.pacman::after{
content:'';
display:block;
width:100%;
height:50%;
background-color:yellow;
animation-duration:var(--anim-speed);
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);
}
.pacman::after{
transform-origin:center top;
border-radius:0 0 var(--size-chara) var(--size-chara);
transform:rotate(-45deg);
}
.pacman[data-anim="true"]::before{
animation-name : pacman_before;
}
.pacman[data-anim="true"]::after{
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);
}
}
.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);
}
.pacman[data-status="dead"]::before,
.pacman[data-status="dead"]::after{
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;
}
.pacman[data-status="dead"]::before{
animation-name : pacman-dead-before;
}
.pacman[data-status="dead"]::after{
animation-name : pacman-dead-after;
}
@keyframes pacman-dead-before{
0%{
clip-path: polygon(
50% 100%,
0% 100%,
0% 0%,
100% 0%,
100% 100%
);
}
25%{
clip-path: polygon(
50% 100%,
0% 0%,
0% 0%,
100% 0%,
100% 100%
);
}
75%{
clip-path: polygon(
50% 100%,
100% 0%,
100% 0%,
100% 0%,
100% 100%
);
}
100%{
clip-path: polygon(
50% 100%,
100% 100%,
100% 100%,
100% 100%,
100% 100%
);
}
}
@keyframes pacman-dead-after{
0%{
clip-path: polygon(
50% 0%,
0% 0%,
0% 100%,
100% 100%,
100% 0%
);
}
25%{
clip-path: polygon(
50% 0%,
0% 100%,
0% 100%,
100% 100%,
100% 0%
);
}
75%{
clip-path: polygon(
50% 0%,
100% 100%,
100% 100%,
100% 100%,
100% 0%
);
}
100%{
clip-path: polygon(
50% 0%,
100% 0%,
100% 0%,
100% 0%,
100% 0%
);
}
}
[全部更新] js/control.js
全体的に、private処理をstatic処理に切り替えました。
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 name = Control.key2name(key)
if(!name){return}
Control.direction = name
Pacman.move(Control.direction)
}
static keyup(e){
if(!Control.direction){return}
const name = Control.key2name(e.keyCode)
Control.clear()
}
static clear(){
if(Control.direction !== name){return}
delete Control.direction
}
}
[全部更新] js/feed.js
エサの残り数を計算する処理を全体的に追加しています。
ファイル内を全て入れ替えてください。
import { Frame } from './frame.js'
import { Pacman } from './pacman.js'
import { Ghost } from './ghost.js'
export class Feed{
constructor(){
// Feed.number_of_bites = this.elm_number_of_bites
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/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'
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
}
static 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
}
}
static get is_clear(){
return Frame.root.getAttribute('data-status') === 'clear' ? true : 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()
Frame.asset_json = e.target.response
Frame.start()
this.finish()
}
}).bind(this)
xhr.send()
}
static start(){
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)
}
}
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を設置
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(){
// console.log('stage cleared !!')
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((()=>{
Main.is_clear = false
Frame.crear()
Frame.root.setAttribute('data-status' , '')
Frame.start()
Feed.reset_data()
Ghost.reset_data()
Pacman.reset_data()
}),4000)
}
}
[全部更新] js/ghost.js
モジュール内に書いていた、ghostのデータをjsonデータとして、別ファイルに移動して、ajax読み込みするようにしました。
import { Main } from './main.js'
import { Frame } from './frame.js'
import { Pacman } from './pacman.js'
export class Ghost{
constructor(){
this.load_data()
}
static reset_data(){
Ghost.datas = JSON.parse(Ghost.data_json)
Ghost.put_element()
}
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
}
load_data(){
const xhr = new XMLHttpRequest()
xhr.open('get' , `assets/ghost.json` , true)
xhr.setRequestHeader('Content-Type', 'text/json');
xhr.onreadystatechange = ((e) => {
if(xhr.readyState !== XMLHttpRequest.DONE){return}
if(xhr.status === 404){return}
if (xhr.status === 200) {
Ghost.data_json = e.target.response
Ghost.datas = JSON.parse(Ghost.data_json)
Ghost.put_element()
}
})
xhr.send()
}
static 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)
}
Ghost.load_asset()
}
static 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) {
Ghost.asset = e.target.response
Ghost.set_ghost()
Ghost.set_move()
// setTimeout(this.set_move.bind(this) , 300)
}
})
xhr.send()
}
static 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 = 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
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(Ghost.moved.bind(this , elm_ghost))
}
static moved(elm_ghost , e){
if(!elm_ghost){return}
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')
Ghost.reverse_move(elm_ghost , data)
}
else{
Ghost.next_move(elm_ghost , data)
}
}
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_stop(elm_ghost)
}
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 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 { Ghost } from './ghost.js'
import { Frame } from './frame.js'
import { Control } from './control.js'
import { Pacman } from './pacman.js'
import { Feed } from './feed.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,
}
function init(){
new Frame().then(()=>{
new Ghost()
new Pacman()
new Control()
new Feed()
})
}
switch(document.readyState){
case 'complete':
case 'interactive':
init()
break
default:
window.addEventListener('DOMContentLoaded' , (()=>init()))
}
[全部更新] 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.start()
}
static start(){
Pacman.create()
Pacman.coodinates = Pacman.start_coodinates
Frame.put(Pacman.elm, Pacman.coodinates)
Pacman.elm.style.setProperty('--anim-speed' , `${Main.anim_speed}ms` , '')
}
static reset_data(){
Pacman.direction = null
Pacman.start()
}
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('.pacman')
}
static move(direction){
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){
setTimeout(Pacman.dead , 1000)
Pacman.move_stop()
}
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){
anim[0].pause()
}
}
static hidden(){
Pacman.elm.style.setProperty('display','none','')
}
static close_mouse(){
// Pacman.elm.setAttribute('data-status' , 'mouse-close')
}
}
解説とポイント
初回の設定の甘さがここに来て出てしまいましたwww。
エサの残り数をFeedモジュールで処理させていますが、それをMainモジュールでセットしていなかったのと、敵キャラ、自キャラなどがステージクリアした時に、新しいステージでちゃんと最初の設定にもどるように、データリセットする事が考慮されていなかったので、全体の処理を見直しました。
なので、ステージは、assetデータを読み込んでいて、メモリに残した状態で、きれいに作り直すようにしました。
その時に、自キャラが消えてしまっていたのは、HTMLで直にタグを書いていたことが原因だったので、これを動的に処理するようにしました。
あと、ステージクリアした時に、画面が点滅する処理は、cssに全てやらせているので、さほど難しくなかったですね。
画面キャプチャ
あとがき
デバッグする時に、毎回1ステージをクリアする必要があったので、めちゃくちゃクリアしまくりました。
でも、全体処理として、足りていなかった箇所などがよく理解できて良かったです。
ただ、まだ自機の残り数などの設定自体が組み込まれていないので、一回でも死んでしまうと、ゲームがフリーズしてしまいます。
次回はその辺を進めましょうね。
知財
パックマンは、バンダイナムコ社の登録商標です。
PAC-MAN™ & ©1980 BANDAI NAMCO Entertainment Inc.
0 件のコメント:
コメントを投稿