カメラで対戦する「MotionBoxing」
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>モーションボクシング</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0d0d0d;
color: white;
font-family: 'Arial', sans-serif;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
}
#ui {
position: absolute;
top: 10px;
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 30px;
z-index: 10;
pointer-events: none;
}
.stat-box {
background: rgba(0,0,0,0.6);
border: 2px solid #e94560;
border-radius: 10px;
padding: 8px 20px;
text-align: center;
}
.stat-box .label { font-size: 0.75rem; color: #aaa; }
.stat-box .value { font-size: 1.6rem; font-weight: bold; color: #ffd700; }
#health-bar-container {
position: absolute;
top: 90px;
width: 80%;
left: 10%;
z-index: 10;
pointer-events: none;
}
.health-label { font-size: 0.75rem; color: #aaa; margin-bottom: 3px; }
.health-bar-bg {
background: #333;
border-radius: 10px;
height: 18px;
overflow: hidden;
border: 1px solid #555;
}
.health-bar-fill {
height: 100%;
border-radius: 10px;
transition: width 0.3s;
}
#enemy-health-fill { background: linear-gradient(90deg, #e94560, #ff6b6b); width: 100%; }
#player-health-fill { background: linear-gradient(90deg, #2ecc71, #27ae60); width: 100%; }
#canvas-container {
position: relative;
width: 100vw;
height: 100vh;
}
#video { display: none; }
#gameCanvas {
width: 100vw;
height: 100vh;
display: block;
}
#overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.75);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 20;
}
#overlay h1 { font-size: 3rem; color: #ffd700; text-shadow: 0 0 20px #ffd700; }
#overlay p { margin: 10px 0; color: #ccc; font-size: 1rem; text-align: center; max-width: 400px; }
#overlay button {
margin-top: 20px;
padding: 14px 40px;
font-size: 1.2rem;
background: #e94560;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
}
#overlay button:hover { background: #c73652; }
#hit-effect {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 15;
opacity: 0;
background: radial-gradient(circle, transparent 50%, rgba(233,69,96,0.4) 100%);
transition: opacity 0.1s;
}
#combo-display {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
font-size: 2.5rem;
font-weight: bold;
color: #ffd700;
text-shadow: 0 0 15px #ffd700;
z-index: 10;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
</style>
</head>
<body>
<div id="ui">
<div class="stat-box">
<div class="label">スコア</div>
<div class="value" id="score">0</div>
</div>
<div class="stat-box">
<div class="label">ラウンド</div>
<div class="value" id="round">1</div>
</div>
<div class="stat-box">
<div class="label">コンボ</div>
<div class="value" id="combo">x0</div>
</div>
</div>
<div id="health-bar-container">
<div class="health-label">敵 HP</div>
<div class="health-bar-bg"><div class="health-bar-fill" id="enemy-health-fill"></div></div>
<div style="height:8px"></div>
<div class="health-label">自分 HP</div>
<div class="health-bar-bg"><div class="health-bar-fill" id="player-health-fill"></div></div>
</div>
<div id="canvas-container">
<video id="video" autoplay playsinline></video>
<canvas id="gameCanvas"></canvas>
<div id="hit-effect"></div>
<div id="combo-display"></div>
</div>
<div id="overlay" id="overlay">
<h1>🥊 モーションボクシング</h1>
<p>カメラに向かって拳を素早く前に突き出すとパンチになります。<br>
右手 → 右パンチ 左手 → 左パンチ<br>
腕を上げてブロック、体を横に動かしてかわそう!</p>
<button onclick="startGame()">ゲームスタート</button>
</div>
<!-- MediaPipe & TF -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/pose/pose.js" crossorigin="anonymous"></script>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// ─── ゲーム状態 ───────────────────────────────────────────────
let gameRunning = false;
let score = 0;
let round = 1;
let combo = 0;
let comboTimer = null;
let playerHP = 100;
let enemyHP = 100;
let maxEnemyHP = 100;
// ─── ポーズデータ ─────────────────────────────────────────────
let landmarks = null;
let prevLandmarks = null;
let poseHistory = [];
// ─── パンチ検出 ───────────────────────────────────────────────
let leftPunchCooldown = false;
let rightPunchCooldown = false;
let leftPunchAnim = 0;
let rightPunchAnim = 0;
// ─── 敵キャラ ─────────────────────────────────────────────────
let enemy = {
x: 0, y: 0,
size: 120,
hp: 100,
maxHP: 100,
state: 'idle', // idle, attacking, hurt, blocking
stateTimer: 0,
attackTimer: 0,
attackInterval: 90,
dx: 0,
flashTimer: 0,
eyeAnim: 0,
};
// ─── エフェクト ───────────────────────────────────────────────
let particles = [];
let hitNumbers = [];
let screenShake = 0;
// ─── MediaPipe Pose ───────────────────────────────────────────
const pose = new Pose({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`
});
pose.setOptions({
modelComplexity: 1,
smoothLandmarks: true,
minDetectionConfidence: 0.6,
minTrackingConfidence: 0.6
});
pose.onResults(onPoseResults);
let camera = null;
function startGame() {
document.getElementById('overlay').style.display = 'none';
gameRunning = true;
enemy.x = canvas.width * 0.5;
enemy.y = canvas.height * 0.42;
camera = new Camera(video, {
onFrame: async () => {
if (gameRunning) await pose.send({ image: video });
},
width: 640,
height: 480
});
camera.start();
requestAnimationFrame(gameLoop);
}
// ─── ポーズ結果処理 ───────────────────────────────────────────
function onPoseResults(results) {
if (!results.poseLandmarks) return;
prevLandmarks = landmarks;
landmarks = results.poseLandmarks;
if (prevLandmarks) {
detectPunch();
detectBlock();
detectDodge();
}
}
const W = () => canvas.width;
const H = () => canvas.height;
// ランドマーク → キャンバス座標
function lm(idx) {
if (!landmarks || !landmarks[idx]) return null;
const l = landmarks[idx];
return { x: (1 - l.x) * W(), y: l.y * H(), v: l.visibility };
}
function prevLm(idx) {
if (!prevLandmarks || !prevLandmarks[idx]) return null;
const l = prevLandmarks[idx];
return { x: (1 - l.x) * W(), y: l.y * H(), v: l.visibility };
}
// ─── パンチ検出 ───────────────────────────────────────────────
let playerBlocking = false;
let playerDodge = 0; // -1 left, 0 center, 1 right
function detectPunch() {
// 右手首(16), 右肩(12), 左手首(15), 左肩(11)
const rWrist = lm(16), rShoulder = lm(12);
const lWrist = lm(15), lShoulder = lm(11);
const prWrist = prevLm(16), plWrist = prevLm(15);
// 右パンチ:手首が肩よりかなり前に出て、速く動いた
if (rWrist && rShoulder && prWrist && rWrist.v > 0.5) {
const velX = Math.abs(rWrist.x - prWrist.x);
const velY = Math.abs(rWrist.y - prWrist.y);
const speed = Math.sqrt(velX*velX + velY*velY);
// 手首がほぼ肩の高さより上、十分な速さ
if (speed > W() * 0.04 && rWrist.y < rShoulder.y + H()*0.1 && !rightPunchCooldown) {
triggerPunch('right', speed);
}
}
// 左パンチ
if (lWrist && lShoulder && plWrist && lWrist.v > 0.5) {
const velX = Math.abs(lWrist.x - plWrist.x);
const velY = Math.abs(lWrist.y - plWrist.y);
const speed = Math.sqrt(velX*velX + velY*velY);
if (speed > W() * 0.04 && lWrist.y < lShoulder.y + H()*0.1 && !leftPunchCooldown) {
triggerPunch('left', speed);
}
}
}
function triggerPunch(side, speed) {
if (!gameRunning) return;
const isRight = side === 'right';
// クールダウン
if (isRight) { rightPunchCooldown = true; setTimeout(() => rightPunchCooldown = false, 600); }
else { leftPunchCooldown = true; setTimeout(() => leftPunchCooldown = false, 600); }
// 敵がブロック中かチェック
if (enemy.state === 'blocking') {
spawnParticles(enemy.x, enemy.y, '#4a90d9', 8);
showHitNumber(enemy.x, enemy.y - 60, 'BLOCK!', '#4a90d9');
combo = 0;
updateComboUI();
return;
}
// ダメージ計算
const base = isRight ? 12 : 10;
const dmg = Math.min(30, Math.floor(base + (speed / W()) * 40));
combo++;
score += dmg * combo;
enemy.hp = Math.max(0, enemy.hp - dmg);
enemy.state = 'hurt';
enemy.stateTimer = 20;
enemy.flashTimer = 8;
// アニメ
if (isRight) { rightPunchAnim = 15; } else { leftPunchAnim = 15; }
// パーティクル
spawnParticles(enemy.x + (Math.random()-0.5)*enemy.size, enemy.y + (Math.random()-0.5)*enemy.size*0.5, '#ffd700', 12);
showHitNumber(enemy.x, enemy.y - 80, `${dmg}`, combo > 2 ? '#ff4444' : '#ffd700');
if (combo >= 3) {
const cd = document.getElementById('combo-display');
cd.textContent = `${combo} HIT COMBO!!`;
cd.style.opacity = '1';
if (comboTimer) clearTimeout(comboTimer);
comboTimer = setTimeout(() => { cd.style.opacity = '0'; }, 1200);
}
screenShake = 8;
updateComboUI();
updateScoreUI();
updateHealthUI();
if (enemy.hp <= 0) nextRound();
}
// ─── ブロック検出 (両手を顔の前に上げる) ─────────────────────
function detectBlock() {
const rWrist = lm(16), lWrist = lm(15);
const nose = lm(0);
if (!rWrist || !lWrist || !nose) return;
const rUp = rWrist.y < nose.y + H()*0.05;
const lUp = lWrist.y < nose.y + H()*0.05;
playerBlocking = rUp && lUp;
}
// ─── かわし検出 (肩の傾き) ────────────────────────────────────
function detectDodge() {
const rShoulder = lm(12), lShoulder = lm(11);
if (!rShoulder || !lShoulder) return;
const diff = rShoulder.y - lShoulder.y;
if (diff > H() * 0.05) playerDodge = 1;
else if (diff < -H() * 0.05) playerDodge = -1;
else playerDodge = 0;
}
// ─── パーティクル ─────────────────────────────────────────────
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 3 + Math.random() * 6;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 3,
alpha: 1,
size: 4 + Math.random() * 6,
color
});
}
}
function showHitNumber(x, y, text, color) {
hitNumbers.push({ x, y, text, color, alpha: 1, vy: -3 });
}
// ─── 敵AI ─────────────────────────────────────────────────────
function updateEnemy() {
if (!gameRunning) return;
// 状態タイマー
if (enemy.stateTimer > 0) { enemy.stateTimer--; if (enemy.stateTimer === 0) enemy.state = 'idle'; }
if (enemy.flashTimer > 0) enemy.flashTimer--;
// 攻撃タイマー
enemy.attackTimer++;
const interval = Math.max(40, enemy.attackInterval - round * 5);
if (enemy.attackTimer >= interval && enemy.state === 'idle') {
enemy.attackTimer = 0;
enemyAttack();
}
// 敵の揺れ
enemy.dx = Math.sin(Date.now() * 0.002) * 12;
}
function enemyAttack() {
enemy.state = 'attacking';
enemy.stateTimer = 40;
// かわし・ブロック判定
const dodged = Math.abs(playerDodge) > 0 && Math.random() < 0.55;
const blocked = playerBlocking && Math.random() < 0.7;
setTimeout(() => {
if (!gameRunning) return;
if (dodged) {
showHitNumber(canvas.width/2, canvas.height*0.5, 'DODGE!', '#2ecc71');
spawnParticles(canvas.width/2, canvas.height*0.5, '#2ecc71', 10);
return;
}
if (blocked) {
showHitNumber(canvas.width/2, canvas.height*0.5, 'BLOCK!', '#4a90d9');
spawnParticles(canvas.width/2, canvas.height*0.5, '#4a90d9', 10);
return;
}
const dmg = 8 + round * 2 + Math.floor(Math.random() * 8);
playerHP = Math.max(0, playerHP - dmg);
combo = 0;
updateComboUI();
screenShake = 12;
document.getElementById('hit-effect').style.opacity = '1';
setTimeout(() => document.getElementById('hit-effect').style.opacity = '0', 150);
updateHealthUI();
if (playerHP <= 0) gameOver();
}, 500);
}
// ─── 次のラウンド ─────────────────────────────────────────────
function nextRound() {
round++;
maxEnemyHP = 100 + round * 20;
enemy.hp = maxEnemyHP;
enemy.maxHP = maxEnemyHP;
enemy.attackInterval = Math.max(40, 90 - round * 8);
enemy.size = Math.min(180, 120 + round * 8);
playerHP = Math.min(100, playerHP + 25);
document.getElementById('round').textContent = round;
updateHealthUI();
}
// ─── ゲームオーバー ───────────────────────────────────────────
function gameOver() {
gameRunning = false;
const ov = document.getElementById('overlay');
ov.innerHTML = `
<h1>💀 KO !</h1>
<p style="font-size:1.4rem;color:#ffd700">スコア: ${score}</p>
<p>ラウンド ${round} まで戦いました</p>
<button onclick="location.reload()">もう一度</button>
`;
ov.style.display = 'flex';
}
// ─── UI更新 ───────────────────────────────────────────────────
function updateScoreUI() {
document.getElementById('score').textContent = score;
}
function updateComboUI() {
document.getElementById('combo').textContent = `x${combo}`;
}
function updateHealthUI() {
document.getElementById('enemy-health-fill').style.width = (enemy.hp / enemy.maxHP * 100) + '%';
document.getElementById('player-health-fill').style.width = playerHP + '%';
}
// ─── 描画 ─────────────────────────────────────────────────────
function drawBackground() {
// リング風背景
ctx.fillStyle = '#1a0a00';
ctx.fillRect(0, 0, W(), H());
// リング床
ctx.save();
const grad = ctx.createRadialGradient(W()/2, H()*0.7, 50, W()/2, H()*0.7, W()*0.6);
grad.addColorStop(0, '#3d1a00');
grad.addColorStop(1, '#0d0500');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W(), H());
ctx.restore();
// スポットライト
ctx.save();
const light = ctx.createRadialGradient(W()/2, 0, 0, W()/2, 0, H()*0.8);
light.addColorStop(0, 'rgba(255,220,100,0.08)');
light.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = light;
ctx.fillRect(0, 0, W(), H());
ctx.restore();
}
function drawEnemy() {
const ex = enemy.x + enemy.dx;
const ey = enemy.y;
const s = enemy.size;
ctx.save();
// ダメージフラッシュ
if (enemy.flashTimer > 0 && enemy.flashTimer % 2 === 0) {
ctx.globalAlpha = 0.4;
}
// 攻撃状態で前傾
let lean = 0;
if (enemy.state === 'attacking') lean = 15;
if (enemy.state === 'hurt') lean = -10;
ctx.translate(ex, ey);
ctx.rotate((lean * Math.PI) / 180);
ctx.translate(-ex, -ey);
// 胴体
const bodyGrad = ctx.createLinearGradient(ex - s*0.35, ey - s*0.1, ex + s*0.35, ey + s*0.6);
bodyGrad.addColorStop(0, '#c0392b');
bodyGrad.addColorStop(1, '#7b241c');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.ellipse(ex, ey + s*0.3, s*0.32, s*0.45, 0, 0, Math.PI*2);
ctx.fill();
// 頭
const headGrad = ctx.createRadialGradient(ex - s*0.08, ey - s*0.52, s*0.05, ex, ey - s*0.45, s*0.38);
headGrad.addColorStop(0, '#f4c090');
headGrad.addColorStop(1, '#c8814a');
ctx.fillStyle = headGrad;
ctx.beginPath();
ctx.ellipse(ex, ey - s*0.42, s*0.3, s*0.36, 0, 0, Math.PI*2);
ctx.fill();
// 目
enemy.eyeAnim = Math.sin(Date.now() * 0.005) * 2;
const eyeY = ey - s*0.48 + enemy.eyeAnim;
[ex - s*0.1, ex + s*0.1].forEach(ex2 => {
ctx.fillStyle = 'white';
ctx.beginPath(); ctx.ellipse(ex2, eyeY, s*0.07, s*0.09, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = enemy.state === 'attacking' ? '#ff0000' : '#1a1a1a';
ctx.beginPath(); ctx.ellipse(ex2, eyeY+2, s*0.04, s*0.05, 0, 0, Math.PI*2); ctx.fill();
});
// 口
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.beginPath();
if (enemy.state === 'hurt') {
ctx.arc(ex, ey - s*0.28, s*0.08, 0, Math.PI); // 驚いた口
} else if (enemy.state === 'attacking') {
ctx.arc(ex, ey - s*0.32, s*0.1, Math.PI, 0); // 怒り口
} else {
ctx.moveTo(ex - s*0.1, ey - s*0.3);
ctx.lineTo(ex + s*0.1, ey - s*0.3);
}
ctx.stroke();
// グローブ(左右)
const gloveColors = ['#c0392b', '#922b21'];
const gloveGrad = ctx.createRadialGradient(ex - s*0.45, ey + s*0.05, 5, ex - s*0.45, ey + s*0.05, s*0.2);
gloveGrad.addColorStop(0, '#e74c3c');
gloveGrad.addColorStop(1, '#922b21');
// 左グローブ
let lx = ex - s*0.52, ly = ey + s*0.05;
if (enemy.state === 'attacking') { lx += s*0.2; ly -= s*0.1; }
ctx.fillStyle = gloveGrad;
ctx.beginPath(); ctx.ellipse(lx, ly, s*0.2, s*0.17, -0.3, 0, Math.PI*2); ctx.fill();
// 右グローブ
const rGloveGrad = ctx.createRadialGradient(ex + s*0.45, ey + s*0.05, 5, ex + s*0.45, ey + s*0.05, s*0.2);
rGloveGrad.addColorStop(0, '#e74c3c');
rGloveGrad.addColorStop(1, '#922b21');
let rx = ex + s*0.52, ry = ey + s*0.05;
if (enemy.state === 'attacking') { rx -= s*0.2; ry -= s*0.1; }
ctx.fillStyle = rGloveGrad;
ctx.beginPath(); ctx.ellipse(rx, ry, s*0.2, s*0.17, 0.3, 0, Math.PI*2); ctx.fill();
// ブロック状態
if (enemy.state === 'blocking') {
ctx.strokeStyle = 'rgba(100,180,255,0.7)';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.ellipse(ex, ey - s*0.1, s*0.55, s*0.65, 0, 0, Math.PI*2);
ctx.stroke();
}
ctx.restore();
}
function drawPoseSkeleton() {
if (!landmarks) return;
// スケルトン接続
const connections = [
[11,12],[11,13],[13,15],[12,14],[14,16],
[11,23],[12,24],[23,24],[23,25],[25,27],[24,26],[26,28]
];
ctx.save();
ctx.strokeStyle = 'rgba(100, 220, 255, 0.6)';
ctx.lineWidth = 3;
for (const [a, b] of connections) {
const pa = lm(a), pb = lm(b);
if (!pa || !pb || pa.v < 0.4 || pb.v < 0.4) continue;
ctx.beginPath();
ctx.moveTo(pa.x, pa.y);
ctx.lineTo(pb.x, pb.y);
ctx.stroke();
}
// 関節点
const keyPoints = [11,12,13,14,15,16,23,24,25,26,27,28];
for (const idx of keyPoints) {
const p = lm(idx);
if (!p || p.v < 0.4) continue;
ctx.fillStyle = idx === 15 || idx === 16 ? '#ffd700' : 'rgba(100, 220, 255, 0.9)';
ctx.beginPath();
ctx.arc(p.x, p.y, idx === 15 || idx === 16 ? 10 : 6, 0, Math.PI*2);
ctx.fill();
}
// パンチエフェクト
if (rightPunchAnim > 0) {
const p = lm(16);
if (p) {
ctx.fillStyle = `rgba(255, 200, 0, ${rightPunchAnim/15})`;
ctx.beginPath();
ctx.arc(p.x, p.y, 20 + (15 - rightPunchAnim)*2, 0, Math.PI*2);
ctx.fill();
}
rightPunchAnim--;
}
if (leftPunchAnim > 0) {
const p = lm(15);
if (p) {
ctx.fillStyle = `rgba(255, 200, 0, ${leftPunchAnim/15})`;
ctx.beginPath();
ctx.arc(p.x, p.y, 20 + (15 - leftPunchAnim)*2, 0, Math.PI*2);
ctx.fill();
}
leftPunchAnim--;
}
// ブロック表示
if (playerBlocking) {
ctx.strokeStyle = 'rgba(100, 180, 255, 0.8)';
ctx.lineWidth = 3;
ctx.setLineDash([6, 3]);
const nose = lm(0);
if (nose) {
ctx.beginPath();
ctx.arc(nose.x, nose.y, 50, 0, Math.PI*2);
ctx.stroke();
}
ctx.setLineDash([]);
}
ctx.restore();
}
function drawParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx; p.y += p.vy; p.vy += 0.3;
p.alpha -= 0.03;
if (p.alpha <= 0) { particles.splice(i, 1); continue; }
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
}
function drawHitNumbers() {
for (let i = hitNumbers.length - 1; i >= 0; i--) {
const h = hitNumbers[i];
h.y += h.vy;
h.alpha -= 0.025;
if (h.alpha <= 0) { hitNumbers.splice(i, 1); continue; }
ctx.save();
ctx.globalAlpha = h.alpha;
ctx.fillStyle = h.color;
ctx.font = `bold ${h.text.length > 3 ? 22 : 36}px Arial`;
ctx.textAlign = 'center';
ctx.shadowColor = 'black';
ctx.shadowBlur = 6;
ctx.fillText(h.text, h.x, h.y);
ctx.restore();
}
}
function drawHUD() {
// かわし中
if (Math.abs(playerDodge) > 0) {
ctx.save();
ctx.fillStyle = 'rgba(46,204,113,0.18)';
ctx.fillRect(0, 0, W(), H());
ctx.fillStyle = '#2ecc71';
ctx.font = 'bold 28px Arial';
ctx.textAlign = 'center';
ctx.fillText('DODGE ▶', W()/2, H() - 50);
ctx.restore();
}
}
// ─── メインループ ─────────────────────────────────────────────
function gameLoop() {
if (!gameRunning) return;
// スクリーンシェイク
let sx = 0, sy = 0;
if (screenShake > 0) {
sx = (Math.random() - 0.5) * screenShake;
sy = (Math.random() - 0.5) * screenShake;
screenShake *= 0.75;
if (screenShake < 0.5) screenShake = 0;
}
ctx.save();
ctx.translate(sx, sy);
drawBackground();
updateEnemy();
drawEnemy();
drawPoseSkeleton();
drawParticles();
drawHitNumbers();
drawHUD();
ctx.restore();
requestAnimationFrame(gameLoop);
}
</script>
</body>
</html>

コメント