パワーエサを食べた後の処理をつけようと思ったんですが、その前にパワーエサを食べていない時に敵キャラに捕まってしまう、いわゆるDead処理を作っておきたいと思います。
この処理を先に作っておいて、自キャラと敵キャラが重なった時に、パワーエサを食べて敵キャラがWeekモード(青色)になっているかどうかの判定をして、自キャラのダメージか、敵キャラのダメージかを処理する事になります。
今回の目的
- 敵キャラに捕まるモード
- 自キャラの消滅アニメーション
対象ファイル一覧
- css/ghost.css
- css/pacman.css
- js/ghost.js
- s/main.js
- js/pacman.js
ソースコード
[全部更新] css/ghost.css
ステージのroot部分で、data-powerという属性をセットしていた仕様を、敵キャラ1体ずつの処理に切り替えました。
これにより、敵キャラを個別にWeakモードから、通常モードに自由に切り替えられるようになりました。
.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;}
.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);
}
[部分追加] css/pacman.css
自キャラが敵キャラに捕まってしまった時の消滅アニメーションを追加しました。
clip-pathを使ってcssだけでアニメーションを表現しています。
これかなり難しかった〜〜〜!
.pacman{
position:absolute;
z-index:10;
display:inline-block;
width:var(--size-chara);
height:var(--size-chara);
}
.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/ghost.js
基本的に当たり判定(コリジョン)は敵キャラの座標を基準に行うようにしたので、このモジュール内で判定を行っています。
自キャラと敵キャラ1体が衝突した時に、全体の動きを止めるというのが、なかなかしんどかったのですが、jsのanimate機能と、cssのモーションストップの2種類を非同期で行うように書いてクリアできました。
import { Main } from './main.js'
import { Frame } from './frame.js'
import { Pacman } from './pacman.js'
export class Ghost{
constructor(){
this.put_element()
}
static datas = [
{ 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 },
},
]
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
}
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)
}
this.load_asset()
}
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.set_ghost()
setTimeout(this.set_move.bind(this) , 1000)
}
}).bind(this)
xhr.send()
}
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 = this.asset
// break;//デバッグ用(敵1体のみ表示の場合)
}
}
set_move(){
const elm_ghosts = Ghost.elm_ghosts
for(const elm_ghost of elm_ghosts){
this.move(elm_ghost)
}
}
move(elm_ghost){
if(!elm_ghost){return}
const data = Ghost.get_data(elm_ghost)
const coodinate = Ghost.get_coodinate(elm_ghost)
const directions = Ghost.get_enable_directions(coodinate , data.direction) || Ghost.get_enable_directions(coodinate)
const direction = Ghost.get_direction(directions)
Ghost.set_direction(elm_ghost , direction)
const next_pos = Frame.next_pos(direction , coodinate)
this.moving(elm_ghost , next_pos)
}
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)){
Ghost.crashed()
Pacman.crashed(elm_ghost)
}
data.next_pos = next_pos
elm_ghost.animate(
[
{
left : `${before_pos.x}px`,
top : `${before_pos.y}px`,
},
{
left : `${after_pos.x}px`,
top : `${after_pos.y}px`,
}
],
{
duration: Frame.is_weak ? Main.ghost_weak_speed : Main.ghost_normal_speed
}
)
Promise.all(elm_ghost.getAnimations().map(e => e.finished))
.then(this.moved.bind(this , elm_ghost))
}
moved(elm_ghost){
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)){
Ghost.crashed()
Pacman.crashed(elm_ghost)
}
else if(elm_ghost.hasAttribute('data-reverse')){
elm_ghost.removeAttribute('data-reverse')
this.reverse_move(elm_ghost , data)
}
else{
this.next_move(elm_ghost , data)
}
}
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)
this.moving(elm_ghost , next_pos)
}
next_move(elm_ghost , data){
const directions = Ghost.get_enable_directions(data.coodinate , data.direction)
const direction = Ghost.get_direction(directions)
Ghost.set_direction(elm_ghost , direction)
const next_pos = Frame.next_pos(data.direction , data.coodinate)
if(Frame.is_collision(next_pos)){
this.move(elm_ghost)
}
else{
this.moving(elm_ghost , next_pos)
}
}
static get_direction(directions){
if(!directions || !directions.length){return null}
const direction_num = Math.floor(Math.random() * directions.length)
return directions[direction_num] || null
}
// 移動可能な方向の一覧を取得する
static get_enable_directions(pos , direction){
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')
&& 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')
&& 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')
&& 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')
&& 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){
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'){continue}
elm.removeAttribute('data-status')
}
}
static crashed(){
Main.is_crash = true
for(const elm of Ghost.elm_ghosts){
Main.is_dead = true
const svg = elm.querySelector('.under svg')
svg.pauseAnimations()
const anim = elm.getAnimations()
if(!anim || !anim.length){continue}
anim[0].pause()
}
}
static hidden_all(){
for(const elm of Ghost.elm_ghosts){
elm.style.setProperty('display' , 'none' , '');
}
}
}
[一部更新] 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 = {
anim_speed : 200,
ghost_normal_speed : 200,
ghost_weak_speed : 400,
is_crash : false,
is_dead : false,
}
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 { 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.coodinates = this.start_coodinates
Frame.put(this.elm, Pacman.coodinates)
this.elm.style.setProperty('--anim-speed' , `${Main.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(direction){
if(Pacman.direction){
return
}
Pacman.direction = direction
this.elm.setAttribute('data-anim' , "true")
this.moving()
}
static moving(){
if(Main.is_dead){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 = Pacman.next_pos(Pacman.direction , Pacman.coodinates)
}
if(Frame.is_collision(Pacman.next_pos)){
this.elm.setAttribute('data-anim' , "")
delete Pacman.direction
return
}
this.elm.setAttribute('data-direction' , Pacman.direction)
this.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(this.elm.getAnimations().map(e => e.finished)).then(()=>{
Pacman.moved()
})
}
static moved(){
Pacman.coodinates = Pacman.next_pos
Frame.put(this.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.direction = Control.direction
}
}
Pacman.moving()
}
static is_collision(pos){
if(!pos || !Pacman.coodinates || !Pacman.next_pos){return}
if(pos.x === Pacman.coodinates.x && pos.y === Pacman.coodinates.y){
return true
}
else if(pos.x === Pacman.next_pos.x && pos.y === Pacman.next_pos.y){
return true
}
else{
return false
}
}
static crashed(){
setTimeout(Pacman.dead , 1000)
const anim = Pacman.elm.getAnimations()
if(anim && anim.length){
anim[0].pause()
}
}
static dead(){
Ghost.hidden_all()
Pacman.elm.setAttribute('data-direction' , 'up')
Pacman.elm.setAttribute('data-status' , 'dead')
}
}
画面キャプチャ
解説
今回行ったプログラミングの最大のポイントは、自キャラの消滅アニメーションです。
半円を2つ重ねてパクパクさせるキャラクターで構成されているんですが、敵キャラとぶつかった時に、パクパクモーションの逆の動きで、消えるアニメーションを実装する必要がありました。
さて、どうしたものか・・・
悩んだ挙げ句、次のパターンを思いつきました。
1. キャラクタをsvgに切り替えて、svgアニメーションで実装する。
2. このアニメーションの時だけ、gifアニメで表示する。
3. cssのclip-path機能で実装する。
部分的なsvgは、敵キャラでも使っているので、これまでcssアニメだけで行っていた自キャラの処理を、切り替えることも可能だったのですが、gifアニメパターンは、仕様的にあり得ないという事で却下しました。
やっぱり処理として最も望ましいのは、3番のcssだけでの処理を実装するパターンです。
色々実験した結果、clip-path:polygon(...)として半円を消していくアニメーションを付けることで、想定通りの見た目を表現できました。
単体で見ると次のようになっています。
clip-pathデモ
知財
パックマンは、バンダイナムコ社の登録商標です。
PAC-MAN™ & ©1980 BANDAI NAMCO Entertainment Inc.
0 件のコメント:
コメントを投稿