さて、またしてもゲームネタを思いついたので早速作ってみました。
今度はCPU相手に玉を打ち合うゲーム、まあようするにエアホッケーみたいなものです。
見た目はこんな感じ。
プレイしたい方は続きをどうぞ!改造&自分のブログに設置したい方は記事の下の方へどうぞ。
🏓カオスピンポン
[ESC]でポーズ。
ゲーム説明
簡単に説明します。
エアホッケーゲーム、もしくはポン(PONG)のようにCPU相手に打ち返していくゲームなのですが、それらと違うのは操作するのは卓球のラケット!
操作方法
・マウスを使って左右に🏓(ラケット)を動かし、○(玉)を打ち返す。
・テーブルの中心に邪魔なアイテムが現れます。
何故か観客から缶ジュースや、バナナが投げ込まれてきます!
缶ジュースやバナナは玉が当たると跳ね返ってしまうため、相手のコート(卓球台)に打ったはずの玉が自分の方に戻ってきてしまう!
・パワーアップアイテム&カオスアイテム
同じくコートの中心に出てくるアイテムなのですが、取るとパワーアップ!取るとカオス!になるアイテムがあります。
🏓=自分のラケットが2個に増える!(たまにCPUが取ってもこっちが2個に増える事も…)
○=玉が2つに増えるのですが、ブロック崩しゲームなら嬉しいアイテム!…なのですが、このゲームの場合、玉が増えるとカオスになってしまいます。
片方の玉だけでも打ち損じると相手のポイントになってしまうため、2個も向かってきたら大変です。
・必殺技!?
コートの右側に□が2個あるのですが、この□はパワーメーターとなっています。
マウスの左クリックでパワーが2個貯まり、貯まった状態で玉に当たると通常より早い玉が相手のコートに向かって飛んでいきます!
MAXパワーで打った玉は赤点滅をします。
一見ずっと使った方が有利に思えるのですが…そこはカオスピンポン。
使うと反動で数秒間ラケットの動きが遅くなってしまうため、ずっと使ってるとふりになります。
・画面端の打ち合いが続くと…
極稀に何故か画面端を返しあってしまう時があったので追加したのですが、数回画面端で一直線に打ち合う状態が続くと勝手に玉が中心に向かって飛ぶようにしました。
・ポーズボタン
実はこのゲーム…終わりがなく永遠に打ち続けなければいけません。
途中で休憩したい!という方のためにESCキーを押すと停止が出来る機能を実装してあります。
さてそんなゲームを改造して自分のブログ記事でやりたい!という人は以下のコードをどうぞ!
なお、自分のブログにてこのゲームを公開する場合、この記事のURLでよろしいので(動画の場合も情報欄に記述していただけると嬉しいです)
ゲームコード配布場所 Gスカの適当に遊べ
URL https://skgura.blogspot.com/2026/02/chaos-pong.html
という感じで表記をお願いします。
以下HTMLコードです。
<div id="jami-true-final" style="align-items: center; background-color: #1a1a1a; border-radius: 10px; color: white; display: flex; flex-direction: column; font-family: sans-serif; padding: 20px; user-select: none;">
<div style="font-size: 24px; margin-bottom: 10px;">PLAYER: <span id="pScoreTF">0</span> | CPU: <span id="cScoreTF">0</span></div>
<div style="align-items: center; display: flex; justify-content: center; position: relative;">
<canvas id="pongCanvasTF" style="background-color: #1b5e20; border: 4px solid #fff; cursor: none;"></canvas>
<div id="pMeterTF" style="color: deepskyblue; display: flex; flex-direction: column; font-size: 30px; font-weight: bold; height: 100px; justify-content: center; margin-left: 15px;">
<div id="m1_off">□</div><div id="m1_on" style="display:none;">■</div>
<div id="m0_off">□</div><div id="m0_on" style="display:none;">■</div>
</div>
<div id="overlayTF" style="align-items: center; background: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; height: 400px; justify-content: center; left: 0px; position: absolute; top: 0px; width: 240px; z-index: 20;">
<button id="startButtonTF" style="background: rgb(255, 69, 0); border-color: initial; border-radius: 5px; border-style: none; border-width: initial; color: white; cursor: pointer; font-size: 20px; font-weight: bold; padding: 15px 30px;">GAME START!</button>
<p style="font-size: 12px; margin-top: 10px;">[ESC]でポーズ。</p>
</div>
<div id="msgTF" style="font-size: 26px; font-weight: bold; opacity: 0; pointer-events: none; position: absolute; text-align: center; text-shadow: rgb(0, 0, 0) 2px 2px; top: 50%; transform: translate(-50%, -50%); transition: opacity 0.3s; width: auto; white-space: nowrap; left: 50%; z-index: 10;"></div>
</div>
<script>
(function() {
// ==========================================
// ✨ 見た目設定:ここを好きな絵文字に変えてね!
// ==========================================
const ICONS = {
RACKET: '🏓', // ラケット
BANANA: '🍌', // バナナ
CAN: '🥫' // 空き缶
};
// ※ メーターの ■ や □ はHTML側を直接書き換えてね!
// ==========================================
const canvas = document.getElementById('pongCanvasTF');
const ctx = canvas.getContext('2d');
const msg = document.getElementById('msgTF');
const overlay = document.getElementById('overlayTF');
const startBtn = document.getElementById('startButtonTF');
canvas.width = 240; canvas.height = 400;
let paused = true;
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSfx(f, d=0.1, type='triangle') {
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
o.type = type; o.frequency.setValueAtTime(f, audioCtx.currentTime);
o.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + d);
g.gain.setValueAtTime(0.05, audioCtx.currentTime); g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + d);
o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime + d);
}
// --- ゲーム状態の変数 ---
let balls = [];
const pW = 50;
const player = { x: 95, score: 0, speedMult: 1, charge: 0, double: false };
const cpu = { x: 95, score: 0, speedMult: 1, charge: 0, double: false };
let junks = []; // バナナ、空き缶
let powers = []; // ラケット、マルチボール
let isDown = false; let mX = 120;
// 【ポーズ機能】
function togglePause() {
paused = !paused;
overlay.style.display = paused ? 'flex' : 'none';
if (paused) startBtn.textContent = "RESUME";
}
window.addEventListener('keydown', (e) => { if (e.key === "Escape") togglePause(); });
startBtn.addEventListener('click', () => {
if (audioCtx.state === 'suspended') audioCtx.resume();
if (balls.length === 0) { player.score = 0; cpu.score = 0; nextBall("READY?"); }
togglePause();
});
// マウス操作
canvas.addEventListener('mousedown', () => { isDown = true; });
canvas.addEventListener('mouseup', () => { isDown = false; });
canvas.addEventListener('mousemove', (e) => { const r = canvas.getBoundingClientRect(); mX = e.clientX - r.left; });
function showMsg(t) { msg.textContent = t; msg.style.opacity = 1; setTimeout(() => msg.style.opacity = 0, 1000); }
function nextBall(m) {
showMsg(m);
// 最初はランダムに打ち出します
const startDir = Math.random() > 0.5 ? 4 : -4;
balls = [{ x: 120, y: 200, dx: (Math.random()>0.5?3:-3), dy: startDir, radius: 7, isSmash: false, wallCount: 0 }];
junks = []; powers = [];
}
// --- メインアップデート(物理計算) ---
function update() {
if (paused) return;
// ラケット移動制限を緩和!端っこピッタリ
let pCurW = player.double ? pW * 2 : pW;
player.x += (mX - pCurW/2 - player.x) * player.speedMult;
if (player.x < -pCurW/4) player.x = -pCurW/4;
if (player.x + pCurW > canvas.width + pCurW/4) player.x = canvas.width - pCurW + pCurW/4;
// チャージ処理:長押しで力を溜めてください
if (isDown && player.charge < 2) player.charge += 0.035;
else if (!isDown) player.charge = 0;
// メーターの表示切り替え(文字化け対策)
const ch = Math.floor(player.charge);
document.getElementById('m0_on').style.display = (ch > 0) ? 'block' : 'none';
document.getElementById('m0_off').style.display = (ch > 0) ? 'none' : 'block';
document.getElementById('m1_on').style.display = (ch > 1) ? 'block' : 'none';
document.getElementById('m1_off').style.display = (ch > 1) ? 'none' : 'block';
// アイテム生成:たまに空から降ってきます
if (Math.random() < 0.01) junks.push({x: Math.random()*200+20, y: Math.random()*150+120, type: Math.random()>0.5?ICONS.BANANA:ICONS.CAN, t:300});
// マルチボールは図形描画フラグを使用
if (Math.random() < 0.005) powers.push({x: Math.random()*200+20, y: 200, isBall: Math.random()>0.5, t:400});
balls.forEach((b, idx) => {
b.x += b.dx; b.y += b.dy;
// 壁反射と【強制バウンド】機能:端っこラリーを許しません
if (b.x + b.radius > canvas.width || b.x - b.radius < 0) {
b.dx *= -1;
b.wallCount = (b.wallCount || 0) + 1;
if (b.wallCount >= 5) {
b.dx = (120 - b.x) * 0.06; // 中央(120)へ向かって飛べーっ!
b.wallCount = 0;
showMsg("強制バウンド");
}
}
// アイテム当たり判定(バナナ・缶)
junks.forEach((j, ji) => {
if (Math.abs(b.x - j.x) < 18 && Math.abs(b.y - j.y) < 18) {
playSfx(400, 0.15, 'square'); b.dx = (Math.random()-0.5)*10; b.dy *= -1;
junks.splice(ji, 1);
}
});
// アイテム当たり判定(強化アイテム):200pxを境界に権利を分配!
powers.forEach((p, pi) => {
if (Math.abs(b.y - p.y) < 18 && Math.abs(b.x - p.x) < 18) {
powers.splice(pi, 1);
if (!p.isBall) {
if (b.y > 200) { player.double = true; showMsg("PLAYER DOUBLE!"); setTimeout(()=>player.double=false, 8000); }
else { cpu.double = true; showMsg("CPU DOUBLE!"); setTimeout(()=>cpu.double=false, 8000); }
} else { balls.push({x:b.x, y:b.y, dx:-b.dx, dy:-b.dy, radius:7, isSmash:false, wallCount:0}); showMsg("MULTI!"); }
}
});
// CPUの動き
let cCurW = cpu.double ? pW * 2 : pW;
let cpuT = balls[0].x - cCurW/2;
cpu.x += (cpuT - cpu.x) * 0.12 * cpu.speedMult;
// 打ち返し判定(プレイヤー側)
if (b.y + b.radius > canvas.height - 45 && b.dy > 0 && b.x > player.x && b.x < player.x + pCurW) {
let s = player.charge >= 2; playSfx(s?950:600);
b.dx = ((b.x - (player.x + pCurW/2)) / (pCurW/2)) * 7;
b.dy = s?-15:-6; b.isSmash = s;
if (s) { player.speedMult = 0.05; setTimeout(()=>player.speedMult=1, 3000); }
player.charge = 0; b.y = canvas.height - 45 - b.radius; b.wallCount = 0;
}
// 打ち返し判定(CPU側)
if (b.y - b.radius < 30 && b.dy < 0 && b.x > cpu.x && b.x < cpu.x + cCurW) {
let s = Math.random()<0.12; playSfx(s?850:500);
b.dx = ((b.x - (cpu.x + cCurW/2)) / (cCurW/2)) * 7; b.dy = s?12:5; b.isSmash = s;
if (s) { cpu.speedMult = 0.1; setTimeout(()=>cpu.speedMult=1, 3000); }
b.y = 30 + b.radius; b.wallCount = 0;
}
if (b.y < 0) { player.score++; nextBall("POINT!"); }
else if (b.y > canvas.height) { cpu.score++; nextBall("LOST..."); }
});
// アイテムの寿命を減らす
junks.forEach(j => j.t--); junks = junks.filter(j => j.t > 0);
powers.forEach(p => p.t--); powers = powers.filter(p => p.t > 0);
document.getElementById('pScoreTF').textContent = player.score;
document.getElementById('cScoreTF').textContent = cpu.score;
}
// --- メイン描画(ここが点滅撲滅の砦!) ---
function draw() {
// ①透明度を1.0(不透明)に完全固定してリセット!
ctx.globalAlpha = 1.0;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#1b5e20'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.fillRect(0, 195, canvas.width, 10);
// ②アイテムの描画:絶対不透明を維持したまま一気に描きます!
junks.forEach(j => { ctx.font = '22px serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = "white"; ctx.fillText(j.type, j.x, j.y); });
// パワーアップアイテムの描画(マルチボールは図形で文字化け回避)
powers.forEach(p => {
if(!p.isBall) {
ctx.font = '22px serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = "white"; ctx.fillText(ICONS.RACKET, p.x, p.y);
} else {
ctx.strokeStyle = "white"; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(p.x, p.y, 10, 0, Math.PI*2); ctx.stroke();
}
});
// ③ボールの描画
balls.forEach(b => {
ctx.fillStyle = (b.isSmash && Math.floor(Date.now()/50)%2) ? '#ff4500' : 'white';
ctx.beginPath(); ctx.arc(b.x, b.y, b.radius, 0, Math.PI*2); ctx.fill();
});
// ④ラケットの描画:save/restoreで「ここだけ」半透明を適用!
ctx.font = '24px serif';
ctx.save();
ctx.globalAlpha = cpu.speedMult < 1 ? 0.4 : 1.0;
let cCurW = cpu.double ? pW * 2 : pW;
ctx.fillText(cpu.double ? ICONS.RACKET + ICONS.RACKET : ICONS.RACKET, cpu.x + cCurW/2, 35);
ctx.restore();
ctx.save();
ctx.globalAlpha = player.speedMult < 1 ? 0.3 : 1.0;
let pCurW = player.double ? pW * 2 : pW;
ctx.fillText(player.double ? ICONS.RACKET + ICONS.RACKET : ICONS.RACKET, player.x + pCurW/2, canvas.height - 20);
ctx.restore();
update(); requestAnimationFrame(draw);
}
draw();
})();
</script></div>
0 件のコメント:
コメントを投稿
I'm sorry, Japanese text only.
荒らし目的と思われるコメントは気づき次第対処します。