音で作る2Dシューティング「WaveGround」
【更新履歴】
・2026/2/1 バージョン1.1公開。
・ダウンロードされる方はこちら。↓
《操作説明》
・方向キーで移動。
・Zキーでショットを撃つ。(押しっぱなしだと、貯め撃ち)
・Xキーでジャンプ。
・下キーで急降下して敵を踏み潰す。
・貯め撃ちすると、ショットサイズが大きくなり、
当たりやすくなる。(ダメージも増えていく。)
(※チャージ中は、自機が膨らむので、当たりやすくなる。)
・HPが減ると、ショットのダメージが減る。
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Wave Ground 1.2</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
font-family: 'Arial Black', Impact, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
color: #fff;
}
#ui-layer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
z-index: 20;
background: rgba(0, 0, 0, 0.8);
padding: 40px;
border-radius: 10px;
border: 2px solid #555;
min-width: 320px;
}
canvas {
display: none;
box-shadow: 0 0 30px rgba(0, 191, 255, 0.3);
background-color: #000;
}
.btn {
padding: 10px 30px;
font-size: 18px;
cursor: pointer;
background: #00BFFF;
color: white;
border: none;
border-radius: 5px;
margin-top: 10px;
font-weight: bold;
}
.btn:disabled { background: #555; color: #999; cursor: not-allowed; }
.key-instruction { font-size: 14px; color: #ccc; margin-top: 5px; }
</style>
</head>
<body>
<div id="ui-layer">
<h1>Wave Ground</h1>
<p>音楽ファイルを選択してゲームスタート</p>
<div class="key-instruction">
移動: ←→ | ジャンプ: X<br>
ショット: Z (長押しで巨大化チャージ)<br>
急降下攻撃: 空中で ↓
</div>
<br>
<input type="file" id="fileInput" accept="audio/*" style="color:white;">
<br><br>
<button id="startBtn" class="btn" disabled>Loading...</button>
<p id="status">ファイルを選択してください</p>
</div>
<canvas id="gameCanvas"></canvas>
<script>
// === 設定・定数 ===
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;
const WORLD_WIDTH = CANVAS_WIDTH * 5;
const BLOCK_SIZE = 20;
const COLOR = {
BG: "#000000",
BLOCK_OUTLINE: "#333",
PLAYER: {R:0, G:0, B:255},
ENEMY: {R:128, G:0, B:128},
BOSS: {R:255, G:0, B:255},
CYAN: {R:0, G:255, B:255}, // ヒット時の変化色&パーティクル
CHARGE_BG: "#000080", CHARGE_FG: "#00BFFF"
};
// 地面のカラーパレット(20ブロックごとに切り替え)
const TERRAIN_PALETTE = [
"#8B4513", // 茶
"#2E8B57", // 緑(シーグリーン)
"#4682B4", // 青(スチールブルー)
"#DAA520", // 金(ゴールデンロッド)
"#800000" // 濃い赤(マルーン)
];
// === グローバル変数 ===
let canvas, ctx;
let gameState = 'loading';
let gameResultText = '';
let cameraX = 0;
let keys = {};
let score = 0;
let frameCount = 0;
let animationFrameId;
let currentAudioLevel = 0; // 現在の音量レベル(0.0~1.0)
// エンティティ管理
let player = null;
let enemies = [];
let bullets = [];
let blocks = [];
let stars = [];
let particles = [];
let popups = []; // ダメージやCRITICAL表示用
// DOM要素
const uiLayer = document.getElementById('ui-layer');
const fileInput = document.getElementById('fileInput');
const startBtn = document.getElementById('startBtn');
const statusText = document.getElementById('status');
// キャンバス初期化
canvas = document.getElementById('gameCanvas');
ctx = canvas.getContext('2d');
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
// オーディオ系
let audioBuffer, analyser, dataArray, soundSource;
const soundManager = new class {
constructor() {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
async loadFile(file) {
if(this.ctx.state === 'suspended') await this.ctx.resume();
const buffer = await file.arrayBuffer();
return await this.ctx.decodeAudioData(buffer);
}
playBGM(buffer) {
this.stopBGM();
soundSource = this.ctx.createBufferSource();
soundSource.buffer = buffer;
soundSource.loop = true;
analyser = this.ctx.createAnalyser();
analyser.fftSize = 2048;
dataArray = new Uint8Array(analyser.frequencyBinCount);
soundSource.connect(analyser);
analyser.connect(this.ctx.destination);
soundSource.start(0);
}
stopBGM() {
if (soundSource) {
try { soundSource.stop(); } catch(e){}
soundSource = null;
}
}
playBeep(f, t, d, v=0.1) {
const o = this.ctx.createOscillator(), g = this.ctx.createGain();
o.type = t; o.frequency.value = f;
g.gain.setValueAtTime(v, this.ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + d);
o.connect(g); g.connect(this.ctx.destination);
o.start(); o.stop(this.ctx.currentTime + d);
}
playShot() { this.playBeep(1200, 'square', 0.08, 0.05); }
playChargeShot() { this.playBeep(400, 'sawtooth', 0.3, 0.2); }
playJump() { this.playBeep(440, 'triangle', 0.15); }
playDamage() { this.playBeep(150, 'sawtooth', 0.3); }
playHit() { this.playBeep(800, 'sine', 0.05); }
playCrit() { this.playBeep(1500, 'square', 0.1, 0.1); }
playStomp() { this.playBeep(100, 'square', 0.2, 0.3); }
playClear() { this.playBeep(523,'sine',0.1); setTimeout(()=>this.playBeep(783,'sine',0.4),200); }
}();
const rgbToHex = (c) => `rgb(${Math.floor(c.R)},${Math.floor(c.G)},${Math.floor(c.B)})`;
const blendColor = (target, source, weight = 0.1) => ({
R: target.R + (source.R - target.R) * weight,
G: target.G + (source.G - target.G) * weight,
B: target.B + (source.B - target.B) * weight
});
// === クラス定義 ===
class Star {
constructor() { this.reset(true); }
reset(randomX = false) {
this.x = randomX ? Math.random() * CANVAS_WIDTH : CANVAS_WIDTH;
this.y = Math.random() * CANVAS_HEIGHT;
this.speed = 2 + Math.random() * 8;
this.size = 1 + Math.random() * 2;
}
update() {
this.x -= this.speed;
if (this.x < 0) this.reset();
}
draw(ctx) {
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(this.x, this.y, this.size, this.size);
}
}
class Particle {
constructor(x, y) {
this.x = x; this.y = y;
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 5 + 2;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.life = 1.0;
this.decay = Math.random() * 0.05 + 0.02;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.2; // 重力
this.life -= this.decay;
}
draw(ctx, camX) {
if(this.life <= 0) return;
ctx.globalAlpha = this.life;
ctx.fillStyle = "#00FFFF"; // 水色
ctx.beginPath();
ctx.arc(this.x - camX, this.y, 3, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1.0;
}
}
class PopupText {
constructor(x, y, text, color="#FFF", size=20) {
this.x = x; this.y = y;
this.text = text;
this.color = color;
this.size = size;
this.life = 30;
}
update() {
this.y -= 1;
this.life--;
}
draw(ctx, camX) {
if(this.life<=0) return;
ctx.fillStyle = this.color;
ctx.font = `bold ${this.size}px Arial`;
ctx.fillText(this.text, this.x - camX, this.y);
}
}
class Block {
constructor(x, baseHeight, index) {
this.x = x;
this.baseHeight = baseHeight;
this.y = CANVAS_HEIGHT;
this.vy = 0;
// 20本ごとに色を変える
const colorIdx = Math.floor(index / 20) % TERRAIN_PALETTE.length;
this.color = TERRAIN_PALETTE[colorIdx];
}
update(targetY) {
const force = (targetY - this.y) * 0.1;
this.vy += force;
this.vy *= 0.85;
this.y += this.vy;
}
draw(ctx, camX) {
if (this.x + BLOCK_SIZE < camX || this.x > camX + CANVAS_WIDTH) return;
ctx.fillStyle = this.color;
ctx.strokeStyle = COLOR.BLOCK_OUTLINE;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.rect(this.x - camX, this.y, BLOCK_SIZE, CANVAS_HEIGHT - this.y + BLOCK_SIZE);
ctx.fill();
ctx.stroke();
}
}
class Entity {
constructor(x, y, size, rgb) {
this.x = x; this.y = y;
this.baseSize = size;
this.currentSize = size;
this.rgb = {...rgb};
this.vx = 0; this.vy = 0;
this.hp = 10;
this.dead = false; this.grounded = false;
this.groundBlock = null;
}
draw(ctx, camX) {
if (this.dead) return;
ctx.fillStyle = rgbToHex(this.rgb);
ctx.beginPath();
ctx.arc(this.x - camX, this.y - this.currentSize/2, this.currentSize, Math.PI, 0);
ctx.fillRect(this.x - camX - this.currentSize, this.y - this.currentSize/2, this.currentSize*2, this.currentSize/2);
ctx.fill();
}
update() {
this.vy += 0.6;
this.x += this.vx;
this.y += this.vy;
this.grounded = false;
this.groundBlock = null;
const blockIdx = Math.floor(this.x / BLOCK_SIZE);
if (blockIdx >= 0 && blockIdx < blocks.length) {
const block = blocks[blockIdx];
if (this.vy >= 0 && this.y >= block.y - this.currentSize/2 && this.y <= block.y + 30) {
if (block.y < CANVAS_HEIGHT + 20) {
this.y = block.y;
if(block.vy < 0) this.vy = block.vy * 0.25;
else this.vy = 0;
this.grounded = true;
this.groundBlock = block;
}
}
}
if (!this.grounded) {
const floorY = CANVAS_HEIGHT - 5;
if (this.y >= floorY) {
this.y = floorY;
this.vy = 0;
this.grounded = true;
}
}
}
hit(damage, shooterRgb) {
this.hp -= damage;
soundManager.playHit();
// 色を水色(CYAN)に近づける
this.rgb = blendColor(this.rgb, COLOR.CYAN, 0.4);
if (this.hp <= 0) {
this.dead = true;
soundManager.playDamage();
return true;
}
return false;
}
}
class Player extends Entity {
constructor() {
super(100, 200, 15, COLOR.PLAYER);
this.maxHp = 10;
this.invincible = 0;
this.scale = 1.0;
this.isStomping = false;
}
update() {
this.vx = keys['ArrowLeft'] ? -5 : (keys['ArrowRight'] ? 5 : 0);
if (keys['KeyX'] && this.grounded) {
this.vy = -14;
if (this.groundBlock) this.vy += this.groundBlock.vy * 0.2;
soundManager.playJump();
this.grounded = false;
}
if (keys['KeyZ']) {
if (this.scale < 5.0) this.scale += 0.1;
else this.scale = 5.0;
} else {
if (this.scale > 1.0) {
const dmg = this.hp * this.scale;
const shotSize = this.scale * 4;
bullets.push(new Bullet(this.x, this.y - this.currentSize/2, 10, 0, true, this.rgb, dmg, shotSize));
if (this.scale > 2.0) soundManager.playChargeShot();
else soundManager.playShot();
this.scale = 1.0;
}
}
if (keys['ArrowDown'] && !this.grounded) {
this.scale = 5.0;
this.vy += 3;
this.isStomping = true;
} else {
if (this.grounded) {
if (this.isStomping) {
soundManager.playStomp();
this.scale = 1.0;
}
this.isStomping = false;
}
}
this.currentSize = this.baseSize * this.scale;
super.update();
this.x = Math.max(0, Math.min(WORLD_WIDTH, this.x));
if (this.invincible > 0) this.invincible--;
if (this.dead && gameState === 'playing') endGame(false);
}
heal() {
if(this.hp < this.maxHp) this.hp++;
}
}
class Enemy extends Entity {
constructor(x, y, isBoss = false) {
super(x, y, isBoss ? 60 : 15, isBoss ? COLOR.BOSS : COLOR.ENEMY);
this.isBoss = isBoss;
this.shootTimer = Math.random() * 200;
}
update() {
if (this.dead) return;
super.update();
if (this.x > cameraX - 100 && this.x < cameraX + CANVAS_WIDTH + 100) {
if (!this.isBoss && this.grounded && Math.random()<0.01) this.vy = -10;
this.shootTimer--;
if (this.shootTimer <= 0) {
const dir = player.x < this.x ? -1 : 1;
bullets.push(new Bullet(this.x, this.y - 10, 6 * dir, 0, false, this.rgb, this.hp, 5));
soundManager.playShot();
this.shootTimer = this.isBoss ? 60 : 200 + Math.random()*200;
}
}
}
}
class Bullet {
constructor(x, y, vx, vy, isPlayerShot, rgb, damage, size) {
this.x = x; this.y = y; this.vx = vx; this.vy = vy;
this.isPlayerShot = isPlayerShot;
this.rgb = {...rgb};
this.damage = damage;
this.size = size;
this.active = true;
}
update() {
this.x += this.vx; this.y += this.vy;
if (this.x < cameraX - 100 || this.x > cameraX + CANVAS_WIDTH + 100) this.active = false;
}
draw(ctx, camX) {
ctx.fillStyle = rgbToHex(this.rgb);
ctx.beginPath();
ctx.arc(this.x - camX, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
if(this.size > 8) {
ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.stroke();
}
}
}
// === ゲームシステム ===
function initGame() {
player = new Player();
bullets = []; enemies = []; blocks = []; stars = [];
particles = []; popups = [];
score = 0; frameCount = 0;
keys = {};
for(let i=0; i<100; i++) stars.push(new Star());
const blockCount = Math.ceil(WORLD_WIDTH / BLOCK_SIZE);
for(let i=0; i<blockCount; i++) blocks.push(new Block(i * BLOCK_SIZE, 500, i));
for(let i=0; i<25; i++) enemies.push(new Enemy(800 + Math.random()*(WORLD_WIDTH-1000), 0));
enemies.push(new Enemy(WORLD_WIDTH - 200, 0, true)); // BOSS
gameState = 'playing';
uiLayer.style.display = 'none';
canvas.style.display = 'block';
soundManager.playBGM(audioBuffer);
if (animationFrameId) cancelAnimationFrame(animationFrameId);
gameLoop();
}
function getTargetHeight(screenX) {
if (!dataArray) return 500;
const dataIdx = Math.floor((screenX % CANVAS_WIDTH) / CANVAS_WIDTH * dataArray.length);
const val = dataArray[dataIdx] || 128;
return 550 - (val - 128) * 3.0;
}
function spawnParticles(x, y) {
for(let i=0; i<10; i++) {
particles.push(new Particle(x, y));
}
}
function update() {
if (gameState !== 'playing') return;
frameCount++;
// 音量解析
let sum = 0;
if (analyser) {
analyser.getByteTimeDomainData(dataArray);
// 平均振幅(音量)を簡易計算 128が中心
for(let i=0; i<dataArray.length; i+=10) {
sum += Math.abs(dataArray[i] - 128);
}
currentAudioLevel = sum / (dataArray.length / 10) / 128.0; // 0.0 ~ 1.0くらい
}
stars.forEach(s => s.update());
blocks.forEach(b => {
const screenX = b.x - cameraX;
let targetY = b.baseHeight;
if (screenX >= -BLOCK_SIZE && screenX <= CANVAS_WIDTH) {
targetY = getTargetHeight(screenX);
}
b.update(targetY);
});
if(player) player.update();
enemies.forEach(e => e.update());
bullets.forEach(b => b.update());
bullets = bullets.filter(b => b.active);
particles.forEach(p => p.update());
particles = particles.filter(p => p.life > 0);
popups.forEach(p => p.update());
popups = popups.filter(p => p.life > 0);
// 弾の判定
bullets.forEach(b => {
if (!b.active) return;
const targets = b.isPlayerShot ? enemies : [player];
targets.forEach(t => {
if (!b.active || t.dead || (t === player && player.invincible > 0)) return;
const dist = Math.hypot(b.x - t.x, b.y - (t.y - t.currentSize/2));
if (dist < t.currentSize + b.size) {
b.active = false;
let finalDamage = b.damage;
let isCritical = false;
// クリティカル判定(プレイヤーの弾かつ、音量が一定以上の時)
if (t !== player && currentAudioLevel > 0.15) { // 閾値調整
finalDamage *= 2;
isCritical = true;
soundManager.playCrit();
popups.push(new PopupText(t.x, t.y - 20, "CRITICAL!", "#FF0"));
}
if (t === player) finalDamage = b.damage / 10;
const dead = t.hit(finalDamage, b.rgb);
if (t === player) {
player.invincible = 60;
if (dead) endGame(false);
} else if (dead) {
// 敵撃破
let gainScore = t.isBoss ? 5000 : 500;
if (isCritical) gainScore *= 2;
score += gainScore;
// 飛沫エフェクト
spawnParticles(t.x, t.y);
// HP回復
player.heal();
popups.push(new PopupText(player.x, player.y - 40, "HP +1", "#0F0", 15));
if (t.isBoss) endGame(true);
}
}
});
});
// 体当たり & 急降下攻撃判定
enemies.forEach(e => {
if(e.dead || player.dead) return;
const dist = Math.hypot(player.x - e.x, (player.y - player.currentSize/2) - (e.y - e.currentSize/2));
const collisionDist = player.currentSize + e.currentSize;
if(dist < collisionDist) {
if (player.isStomping && player.y < e.y + e.currentSize) {
e.hit(9999, player.rgb);
spawnParticles(e.x, e.y);
score += e.isBoss ? 5000 : 500;
player.heal(); // 踏みつけでも回復
soundManager.playDamage();
if (e.isBoss && e.hp<=0) endGame(true);
}
else if (player.invincible <= 0) {
player.hit(0.1, e.rgb);
player.invincible = 60;
if(player.hp<=0) endGame(false);
}
}
});
if(player) {
cameraX = player.x - CANVAS_WIDTH * 0.4;
cameraX = Math.max(0, Math.min(WORLD_WIDTH - CANVAS_WIDTH, cameraX));
}
}
function draw() {
ctx.fillStyle = COLOR.BG;
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.save();
// 音量に応じて揺れ幅を変える
// 基本の揺れ + 音量の揺れ
const baseSway = Math.sin(frameCount * 0.005) * 5;
const volumeKick = currentAudioLevel * 20; // 音が大きいと最大20度くらい追加
// 音量の揺れは左右ランダムっぽく
const kickDir = Math.sin(frameCount * 0.1) > 0 ? 1 : -1;
const angle = (baseSway + (volumeKick * kickDir)) * (Math.PI / 180);
ctx.translate(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
ctx.rotate(angle);
ctx.translate(-CANVAS_WIDTH / 2, -CANVAS_HEIGHT / 2);
stars.forEach(s => s.draw(ctx));
blocks.forEach(b => b.draw(ctx, cameraX));
particles.forEach(p => p.draw(ctx, cameraX)); // パーティクル描画
enemies.forEach(e => e.draw(ctx, cameraX));
if(player) player.draw(ctx, cameraX);
bullets.forEach(b => b.draw(ctx, cameraX));
popups.forEach(p => p.draw(ctx, cameraX)); // ポップアップ
ctx.restore();
if (gameState === 'playing') {
drawUI();
} else if (gameState === 'gameover' || gameState === 'gameclear') {
drawEndScreen();
}
}
function drawUI() {
if(!player) return;
const barW = 200, barH = 25, barX = 20, barY = 20;
ctx.fillStyle = "red"; ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = "lime"; ctx.fillRect(barX, barY, barW * (player.hp/player.maxHp), barH);
ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.strokeRect(barX, barY, barW, barH);
ctx.fillStyle = "white"; ctx.font = "bold 16px Arial";
ctx.fillText(`HP: ${Math.ceil(Math.max(0, player.hp))}`, barX + 10, barY + 18);
ctx.textAlign = "right";
ctx.font = "bold 24px Arial";
ctx.fillText(`SCORE: ${score}`, CANVAS_WIDTH - 20, 45);
ctx.textAlign = "left";
// CRITICAL CHANCE 表示 (デバッグ用兼ねて)
if(currentAudioLevel > 0.15) {
ctx.fillStyle = "yellow";
ctx.font = "bold 14px Arial";
ctx.fillText("BEAT BONUS!", CANVAS_WIDTH - 20, 70);
}
}
function drawEndScreen() {
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.font = "bold 80px Arial Black";
ctx.fillText(gameResultText, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
ctx.font = "bold 40px Arial";
ctx.fillText(`Final Score: ${score}`, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 60);
ctx.font = "20px Arial";
ctx.fillText("Restarting in 3 seconds...", CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 120);
}
function endGame(win) {
if(gameState === 'gameover' || gameState === 'gameclear') return;
gameState = win ? 'gameclear' : 'gameover';
gameResultText = win ? "GAME CLEAR!!" : "GAME OVER";
soundManager.stopBGM();
if(win) soundManager.playClear();
setTimeout(() => {
initGame();
}, 3000);
}
function gameLoop() {
try {
update();
draw();
} catch(e) {
console.error(e);
}
animationFrameId = requestAnimationFrame(gameLoop);
}
// イベント
fileInput.addEventListener('change', async (e) => {
if (!e.target.files.length) return;
statusText.innerText = "読み込み中...";
startBtn.disabled = true;
try {
audioBuffer = await soundManager.loadFile(e.target.files[0]);
statusText.innerText = "準備完了!";
startBtn.disabled = false;
startBtn.innerText = "GAME START";
} catch (e) {
console.error(e);
statusText.innerText = "エラー: ファイルを読み込めません";
}
});
startBtn.addEventListener('click', () => { if (audioBuffer) initGame(); });
window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup', e => keys[e.code] = false);
</script>
</body>
</html>《似ているものコーナー》
・なんか見た覚えがあったんですが、
例によってAIくんに聞いてみたところ、
これが一番似ているらしいです。↓
「ビブリボン vib-ribbon」(1999)SONY
・プレイステーションに好きな音楽CDを入れると、
その曲に合わせてステージ(白いライン)が生成される。
・障害物を避けて進むタイプのゲーム。
「Audiosurf」 (2008)
・音声ファイルを読み込んで、
発音タイミングの位置にアイテムや障害物を配置し、
当たったり避けたりして進むゲーム。
・波形によってコースが起伏したりする。
「Melody's Escape」(2016)
・これも音声ファイルを読み込んでステージを作るもの。
Beat Hazard (2009)
・これも音声ファイルから作るタイプですが、
全方位シューティングということで、
敵の出現タイミングとかだと思いますが、
あまり音と連動してる感じはしないですね。
・あとは、手持ちの音声ファイルを指定できないものでいえば、
音声データとステージがリアルタイムに連動するものは
いくつかあるみたいですね。↓
「Sound Shapes」 (2012)
・足場や敵、コインなどがすべて「楽器」になっており、
ステージを進めることで曲を演奏していく。
「140」(2021)
・登場するほぼ全てのギミックが
音楽のBPM140の全音符4つ分リズムで動き、
ゲーム内の音楽と連動するように動くそうです。
・ノイズブロックに当たると、最初からやり直し。
「Geometry Dash」(2013)
技術的な「祖先」
Web Audio API のデモ (Chrome Experimentsなど)
2010年代中盤、ブラウザで高度な音声処理ができるようになった頃、今回のように「波形(スペクトラム)を可視化して、その上を走る」という技術デモがWeb上にいくつか作られました。
《このゲームの独自性》
・「音楽からステージを作る」ゲームはありますが、
多くは「事前に解析して固定のコースを作る」もので、
今回のように「リアルタイムに波形そのものが物理的な足場として
暴れまわる」というアプローチは、製品レベルのゲームでは
意外と珍しい。
・多くの音ゲーは、音を「譜面(タイミング)」として扱いますが、
このツールは音を「物理エネルギー(高さとバネ)」として扱っている。
・音が大きくなると地面が物理的にプレイヤーを跳ね飛ばす、
という「音の暴力性」をアクションに転化している点がユニーク。


コメント