ブラウザで遊べるドライブゲームImageBooster
【更新機歴】
・2025/12/22 バージョン3.0公開。
・テキストファイルを新規作成して、(index.html)
以下のソースコードをコピペして保存し、ファイルをクリックすると、
webブラウザが起動してゲームが遊べます。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Image Booster 3.0 - High Velocity</title>
<style>
body { margin: 0; overflow: hidden; background: #000; font-family: 'Arial Black', sans-serif; }
#ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; color: white; text-shadow: 2px 2px 4px #000; z-index: 10; }
#view-mode { position: absolute; top: 20px; right: 20px; font-size: 20px; color: #0f0; }
#stage-info { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); font-size: 20px; color: #ff0; }
#score-info { position: absolute; top: 20px; left: 20px; font-size: 24px; color: #fff; }
/* スピード線:ブースト時の演出用 */
#speed-lines {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(circle, transparent 30%, rgba(255, 255, 255, 0.1) 80%),
repeating-conic-gradient(transparent 0deg, transparent 2deg, rgba(0, 255, 255, 0.3) 2.1deg, transparent 2.5deg);
opacity: 0;
transition: opacity 0.1s ease-out;
pointer-events: none;
z-index: 5;
mix-blend-mode: screen;
}
#hud-bottom { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); width: 85%; display: none; flex-direction: row; justify-content: space-around; }
.gauge-container { width: 30%; text-align: center; }
.gauge-label { font-size: 18px; margin-bottom: 5px; }
.bar-bg { width: 100%; height: 12px; border: 2px solid #fff; border-radius: 6px; overflow: hidden; }
.bar-fill { height: 100%; width: 100%; transition: width 0.05s ease-out; }
#hp-bg { background: #500; }
#hp-fill { background: #ff3333; }
#speed-bg { background: #000050; }
#speed-fill { background: #00ffff; }
#time-bg { background: #554400; }
#time-fill { background: #ffcc00; }
#center-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; text-align: center; }
h1 { font-size: 60px; margin: 0; color: #0ff; }
p { font-size: 24px; margin: 10px 0; color: #fff; }
.pop-text { position: absolute; font-weight: bold; font-size: 40px; animation: moveUpFade 0.8s forwards; transform: translate(-50%, -50%); z-index: 999; }
@keyframes moveUpFade { 0% { transform: translate(-50%, 0) scale(1); opacity: 1; } 100% { transform: translate(-50%, -100px) scale(1.5); opacity: 0; } }
</style>
<script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } } </script>
</head>
<body>
<div id="speed-lines"></div>
<div id="ui-layer">
<div id="score-info">SCORE: 0</div>
<div id="stage-info">STAGE: 1 / 8</div>
<div id="view-mode">VIEW: 3RD PERSON</div>
<div id="hud-bottom">
<div class="gauge-container">
<div class="gauge-label" id="hp-txt">HP: 10</div>
<div class="bar-bg" id="hp-bg"><div id="hp-fill" class="bar-fill"></div></div>
</div>
<div class="gauge-container">
<div class="gauge-label" id="speed-txt">SPEED: 0</div>
<div class="bar-bg" id="speed-bg"><div id="speed-fill" class="bar-fill"></div></div>
</div>
<div class="gauge-container">
<div class="gauge-label" id="time-txt">TIME: 60</div>
<div class="bar-bg" id="time-bg"><div id="time-fill" class="bar-fill"></div></div>
</div>
</div>
<div id="center-text"></div>
</div>
<script type="module">
import * as THREE from 'three';
// --- Sound Manager (Web Audio API) ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let noiseBuffer = null;
// 走行音用ノード
let windSource = null;
let windGain = null;
let windFilter = null;
let jetOsc = null;
let jetGain = null;
function createNoiseBuffer() {
const bufferSize = audioCtx.sampleRate * 2.0;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
return buffer;
}
const sound = {
init: () => {
if (!noiseBuffer) noiseBuffer = createNoiseBuffer();
},
startEngine: () => {
if (windSource) return;
// 風切り音 (ホワイトノイズ + バンドパス)
windSource = audioCtx.createBufferSource();
windSource.buffer = noiseBuffer;
windSource.loop = true;
windFilter = audioCtx.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.Q.value = 1.0;
windFilter.frequency.value = 400;
windGain = audioCtx.createGain();
windGain.gain.value = 0;
windSource.connect(windFilter);
windFilter.connect(windGain);
windGain.connect(audioCtx.destination);
windSource.start();
// ジェット音 (ダッシュ時の高音キーン)
jetOsc = audioCtx.createOscillator();
jetOsc.type = 'sawtooth';
jetOsc.frequency.value = 800;
jetGain = audioCtx.createGain();
jetGain.gain.value = 0;
jetOsc.connect(jetGain);
jetGain.connect(audioCtx.destination);
jetOsc.start();
},
updateEngine: (speedRatio, isBoosting) => {
if (!windSource) return;
const now = audioCtx.currentTime;
// 風切り音: 速度に応じて中心周波数と音量を上げる
// 静かな音にしたいので音量は控えめに
const targetFreq = 400 + (speedRatio * 800);
const targetVol = Math.min(0.15, speedRatio * 0.1);
windFilter.frequency.setTargetAtTime(targetFreq, now, 0.1);
windGain.gain.setTargetAtTime(targetVol, now, 0.1);
// ジェット音: ブースト時のみ聞こえるように
const jetVol = isBoosting ? 0.05 : 0.0;
const jetFreq = 1000 + (speedRatio * 2000);
jetGain.gain.setTargetAtTime(jetVol, now, 0.2);
jetOsc.frequency.setTargetAtTime(jetFreq, now, 0.1);
},
play: (type) => {
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
if (type === 'crash') {
// 派手な破壊音
const src = audioCtx.createBufferSource();
src.buffer = noiseBuffer;
const f = audioCtx.createBiquadFilter();
f.type = 'lowpass';
f.frequency.value = 1000;
f.frequency.exponentialRampToValueAtTime(100, now + 0.5);
const g = audioCtx.createGain();
g.gain.setValueAtTime(0.8, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
src.connect(f); f.connect(g); g.connect(audioCtx.destination);
src.start(now); src.stop(now + 0.5);
} else if (type === 'heal') {
// ピュイン♪ (Sineスイープ)
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(2000, now + 0.4); // 急上昇
g.gain.setValueAtTime(0.2, now);
g.gain.linearRampToValueAtTime(0, now + 0.4);
osc.start(now); osc.stop(now + 0.4);
} else if (type === 'jump') {
// ジャンプ音
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination);
osc.type = 'triangle';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(300, now + 0.2);
g.gain.setValueAtTime(0.3, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now); osc.stop(now + 0.3);
} else if (type === 'land') {
// 着地音 (ダンッ!)
const src = audioCtx.createBufferSource();
src.buffer = noiseBuffer;
const f = audioCtx.createBiquadFilter();
f.type = 'lowpass';
f.frequency.setValueAtTime(200, now); // 低音のみ
const g = audioCtx.createGain();
g.gain.setValueAtTime(0.6, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.15); // 短く
src.connect(f); f.connect(g); g.connect(audioCtx.destination);
src.start(now); src.stop(now + 0.15);
} else if (type === 'coin') {
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination);
osc.type = 'square';
osc.frequency.setValueAtTime(1200, now);
osc.frequency.setValueAtTime(1800, now + 0.05);
g.gain.setValueAtTime(0.1, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(now); osc.stop(now + 0.1);
} else if (type === 'ui') {
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination);
osc.frequency.setValueAtTime(880, now);
g.gain.setValueAtTime(0.1, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(now); osc.stop(now + 0.1);
}
}
};
const MAX_HP = 10, TUBE_R = 15, PLANE_W = 12, MAX_TIME = 60;
const NORMAL_MAX_SPEED = 5.0;
const BOOST_MAX_SPEED = 20.0; // 倍速化(前回9.0 -> 20.0)
const NORMAL_ACCEL = 0.08;
const BOOST_ACCEL = 0.5;
const TOTAL_STAGES = 8;
let scene, camera, renderer;
let gameState = "TITLE", viewMode = "F3";
let score = 0, hp = MAX_HP, timeLeft = MAX_TIME;
let currentStage = 1;
let player = {
mesh: null, angle: 0, x: 0, z: 0,
vz: 0.2, vAngle: 0, vx: 0,
altitude: 0, jumpV: 0,
surge: 0, bank: 0,
isBoosting: false,
wasAirborne: false // 着地判定用
};
let worldObjects = [], debris = [], stars, groundTube, groundPlane;
let keys = {};
let nextSpawnZ = 50;
let tubeMaterial;
function init() {
sound.init();
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000005);
scene.fog = new THREE.Fog(0x000005, 50, 800); // 視界拡張
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight, 0.1, 4000);
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
const sun = new THREE.DirectionalLight(0xffffff, 1.2);
sun.position.set(0, 50, 10);
scene.add(sun);
createStars();
// プレイヤー
player.mesh = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.6, 2.5), new THREE.MeshPhongMaterial({ color: 0x00aaff }));
scene.add(player.mesh);
// --- 市松模様(チェッカーフラグ)テクスチャ ---
// Gray & LightGray
const canvas = document.createElement('canvas');
canvas.width = 512; canvas.height = 512;
const ctx = canvas.getContext('2d');
const color1 = '#444444'; // Gray
const color2 = '#888888'; // LightGray
ctx.fillStyle = color1; ctx.fillRect(0, 0, 512, 512); // Base
ctx.fillStyle = color2;
// 2x2チェッカーを描く
ctx.fillRect(256, 0, 256, 256);
ctx.fillRect(0, 256, 256, 256);
// 視認性を上げるための枠線
ctx.strokeStyle = '#222'; ctx.lineWidth = 2;
ctx.strokeRect(0,0,512,512);
const gridTex = new THREE.CanvasTexture(canvas);
gridTex.wrapS = THREE.RepeatWrapping;
gridTex.wrapT = THREE.RepeatWrapping;
gridTex.magFilter = THREE.LinearFilter;
gridTex.minFilter = THREE.LinearMipMapLinearFilter;
// 重要:タイリング比率の調整
// 円周: 2 * PI * 15 ≈ 94.2
// 奥行き: 無限だが、UVリピート数で正方形に見せる
// 円周方向に16タイル並べるとする -> 1タイルの幅 ≈ 5.89
// 400000 / 5.89 ≈ 67911 回のリピートが必要
gridTex.repeat.set(16, 60000);
tubeMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff,
map: gridTex,
side: THREE.BackSide,
shininess: 30
});
// トンネル
groundTube = new THREE.Mesh(
new THREE.CylinderGeometry(TUBE_R, TUBE_R, 400000, 32, 1, true),
tubeMaterial
);
groundTube.rotation.x = Math.PI / 2;
scene.add(groundTube);
// 平面床
// 平面幅200に対して、タイルのサイズを合わせる
const planeMat = tubeMaterial.clone();
planeMat.side = THREE.FrontSide;
planeMat.map = gridTex.clone();
planeMat.map.needsUpdate = true;
// 幅200 / 5.89 ≈ 34リピート
planeMat.map.repeat.set(34, 60000);
groundPlane = new THREE.Mesh(new THREE.PlaneGeometry(200, 400000), planeMat);
groundPlane.rotation.x = -Math.PI / 2;
groundPlane.visible = false;
scene.add(groundPlane);
window.addEventListener('keydown', (e) => {
if(["F1", "F2", "F3"].includes(e.code)) {
e.preventDefault();
viewMode = e.code;
updateViewLabel();
}
keys[e.code] = true;
if(e.code === 'Space') {
e.preventDefault();
handleSpace();
}
if(e.code === 'KeyX') {
handleJump();
}
});
window.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
updateViewLabel();
changeState("TITLE");
animate();
}
function createStars() {
const starGeo = new THREE.BufferGeometry();
const starCount = 4000;
const posArray = new Float32Array(starCount * 3);
for(let i=0; i<starCount * 3; i++) {
posArray[i] = (Math.random() - 0.5) * 1500;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
const starMat = new THREE.PointsMaterial({color: 0xffffff, size: 1.2, transparent: true});
stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
}
function updateStars() {
if(stars) {
stars.position.z = player.z;
stars.rotation.z += 0.0005;
}
// テクスチャスクロール
const offsetSpeed = -player.z * 0.00017; // リピート数が多いのでスクロール速度係数は小さくする
if(tubeMaterial) {
tubeMaterial.map.offset.y = offsetSpeed;
}
if(groundPlane && groundPlane.visible) {
groundPlane.material.map.offset.y = offsetSpeed;
}
}
function updateViewLabel() {
const labels = { "F1": "1ST PERSON", "F2": "PLANE MODE", "F3": "3RD PERSON" };
document.getElementById('view-mode').innerText = "VIEW: " + labels[viewMode];
groundTube.visible = (viewMode !== "F2");
groundPlane.visible = (viewMode === "F2");
}
function handleSpace() {
if (gameState === "TITLE" || gameState === "GAMEOVER" || gameState === "ALL_CLEAR") {
sound.play('ui');
sound.startEngine();
if(gameState !== "TITLE") {
score = 0;
currentStage = 1;
}
changeState("STAGE_START");
}
else if (gameState === "STAGE_CLEAR") {
sound.play('ui');
changeState("STAGE_START");
}
else if (gameState === "STAGE_START") {
sound.play('ui');
changeState("PLAYING");
}
}
function handleJump() {
if (gameState === "PLAYING" && player.altitude <= 0.01) {
sound.play('jump');
player.jumpV = 0.8;
player.wasAirborne = true;
}
}
function changeState(s) {
gameState = s;
document.getElementById('hud-bottom').style.display = (s === "PLAYING") ? "flex" : "none";
const menu = document.getElementById('center-text');
document.getElementById('stage-info').innerText = `STAGE: ${currentStage} / ${TOTAL_STAGES}`;
document.getElementById('speed-lines').style.opacity = 0;
if(s === "TITLE") menu.innerHTML = "<h1>Image Booster 3.0</h1><p>PRESS SPACE TO START</p><p style='font-size:18px; color:#aaa;'>[UP]: ACCEL / [DOWN]: BRAKE / [Z]: BOOST / [X]: JUMP</p>";
else if(s === "STAGE_START") {
menu.innerHTML = "<h1>STAGE "+currentStage+"</h1><p>SPACE: GO!</p>";
resetPlayerPos();
}
else if(s === "PLAYING") menu.innerHTML = "";
else if(s === "STAGE_CLEAR") {
if (currentStage >= TOTAL_STAGES) {
changeState("ALL_CLEAR");
} else {
menu.innerHTML = "<h1 style='color:#0f0;'>STAGE CLEAR!</h1><p>SCORE: "+score+"</p><p>SPACE FOR NEXT STAGE</p>";
currentStage++;
}
}
else if(s === "ALL_CLEAR") {
menu.innerHTML = "<h1 style='color:#0ff;'>ALL CLEARED!</h1><p>FINAL SCORE: "+score+"</p><p>THANK YOU FOR PLAYING</p><p>SPACE TO TITLE</p>";
}
else if(s === "GAMEOVER") menu.innerHTML = "<h1 style='color:red;'>GAME OVER</h1><p>SCORE: "+score+"</p><p>SPACE TO RESTART</p>";
}
function resetPlayerPos() {
player.angle = 0; player.x = 0; player.z = 0; player.vz = 0.5; player.altitude = 0; player.jumpV = 0;
player.surge = 0; player.bank = 0;
hp = MAX_HP;
timeLeft = MAX_TIME;
worldObjects.forEach(obj => scene.remove(obj.mesh));
debris.forEach(d => scene.remove(d));
worldObjects = [];
debris = [];
nextSpawnZ = 50;
}
function animate() {
requestAnimationFrame(animate);
if (gameState === "PLAYING") {
updatePhysics();
updateObjects();
updateDebris();
}
// エンジン音更新
if(gameState === "PLAYING") {
const ratio = player.vz / BOOST_MAX_SPEED;
sound.updateEngine(ratio, player.isBoosting);
} else {
sound.updateEngine(0, false);
}
updateCamera();
updateStars();
renderer.render(scene, camera);
}
function updatePhysics() {
let targetMax = NORMAL_MAX_SPEED;
let currentAccel = 0;
player.isBoosting = false;
if (keys['KeyZ']) {
// ブースト:速度倍増
player.isBoosting = true;
targetMax = BOOST_MAX_SPEED;
currentAccel = BOOST_ACCEL;
// 加速演出
player.surge = THREE.MathUtils.lerp(player.surge, 6.0, 0.05);
}
else if (keys['ArrowUp']) {
targetMax = NORMAL_MAX_SPEED;
currentAccel = NORMAL_ACCEL;
player.surge = THREE.MathUtils.lerp(player.surge, 2.0, 0.05);
}
else {
currentAccel = 0;
player.surge = THREE.MathUtils.lerp(player.surge, 0, 0.05);
}
// ブレーキ
if (keys['ArrowDown']) {
player.vz *= 0.92;
player.surge = THREE.MathUtils.lerp(player.surge, -2.0, 0.1);
}
if (currentAccel > 0) {
if (player.vz < targetMax) {
player.vz += currentAccel;
} else {
player.vz *= 0.98; // 超過分減衰
}
} else {
player.vz *= 0.98;
}
player.vz = Math.max(0.1, player.vz);
// スピード線演出
const lineOpacity = Math.max(0, (player.vz - 8) / 10.0);
document.getElementById('speed-lines').style.opacity = lineOpacity;
// ジャンプ&着地
player.altitude += player.jumpV;
if (player.altitude > 0) {
player.jumpV -= 0.03;
} else {
if (player.wasAirborne) {
sound.play('land'); // 着地音
player.wasAirborne = false;
}
player.altitude = 0;
player.jumpV = 0;
}
// 旋回(速度が速いほど旋回は鈍くする)
let steerFactor = 1.0;
if (player.vz > 10.0) steerFactor = 0.6;
let targetBank = 0;
if (viewMode === "F2") {
if (keys['ArrowLeft']) { player.vx = THREE.MathUtils.lerp(player.vx, 1.2 * steerFactor, 0.1); targetBank = 0.5; }
else if (keys['ArrowRight']) { player.vx = THREE.MathUtils.lerp(player.vx, -1.2 * steerFactor, 0.1); targetBank = -0.5; }
else player.vx *= 0.9;
player.x = Math.max(-PLANE_W, Math.min(PLANE_W, player.x + player.vx));
player.bank = THREE.MathUtils.lerp(player.bank, targetBank, 0.1);
player.mesh.position.set(player.x, 0.5 + player.altitude, player.z + player.surge);
player.mesh.rotation.set(0, 0, (player.vx * 0.3) + player.bank);
} else {
if (keys['ArrowLeft']) { player.vAngle = THREE.MathUtils.lerp(player.vAngle, 0.12 * steerFactor, 0.1); targetBank = 0.8; }
else if (keys['ArrowRight']) { player.vAngle = THREE.MathUtils.lerp(player.vAngle, -0.12 * steerFactor, 0.1); targetBank = -0.8; }
else player.vAngle *= 0.9;
player.angle += player.vAngle;
player.bank = THREE.MathUtils.lerp(player.bank, targetBank, 0.1);
const r = TUBE_R - 0.5 - player.altitude;
player.mesh.position.set(Math.sin(player.angle)*r, -Math.cos(player.angle)*r, player.z + player.surge);
player.mesh.rotation.set(0, 0, player.angle + player.bank);
}
player.z += player.vz;
timeLeft -= 1/60;
if (timeLeft <= 0) {
timeLeft = 0;
changeState("STAGE_CLEAR");
}
if (hp <= 0) {
hp = 0;
changeState("GAMEOVER");
}
updateUI();
}
function updateCamera() {
player.mesh.visible = (viewMode !== "F1");
const shake = player.isBoosting ? (Math.random()-0.5)*0.3 : 0;
if (viewMode === "F1") {
camera.position.copy(player.mesh.position);
camera.position.y += shake;
camera.up.set(-player.mesh.position.x, -player.mesh.position.y, 0).normalize();
camera.lookAt(player.mesh.position.x, player.mesh.position.y, player.z + 100);
} else if (viewMode === "F2") {
camera.up.set(0, 1, 0);
camera.position.set(player.x, 8 + shake, player.z - 25);
camera.lookAt(player.x, 0, player.z + 50);
} else {
// スピードでカメラが引く量も増やす
const camDist = 20 + (player.vz * 1.5);
const camHeight = 6 + (player.vz * 0.2);
const coreX = Math.sin(player.angle) * (TUBE_R - 0.5);
const coreY = -Math.cos(player.angle) * (TUBE_R - 0.5);
camera.position.x = coreX - Math.sin(player.angle) * camHeight;
camera.position.y = coreY + Math.cos(player.angle) * camHeight + shake;
camera.position.z = player.z - camDist;
camera.up.set(-coreX, -coreY, 0).normalize();
camera.lookAt(coreX, coreY, player.z + 40 + (player.vz * 4));
}
}
function updateObjects() {
// スピードが速いときは出現間隔を広げる
let spawnDist = Math.max(10, 30 - (currentStage * 2.0)) + (player.vz * 2.0);
if (player.z + 600 > nextSpawnZ) {
spawnObject(nextSpawnZ);
nextSpawnZ += Math.random() * 10 + spawnDist;
}
for (let i = worldObjects.length - 1; i >= 0; i--) {
const obj = worldObjects[i];
if(obj.flying) {
obj.mesh.position.add(obj.vel);
obj.mesh.rotation.x += 0.1;
obj.mesh.rotation.y += 0.1;
} else if (player.mesh.position.distanceTo(obj.mesh.position) < (obj.type === 'hurdle' ? 4.5 : 3.5)) {
handleCollision(obj, i);
if(!obj.flying && obj.type !== "block" && obj.type !== "hurdle") {
scene.remove(obj.mesh);
worldObjects.splice(i, 1);
continue;
}
}
if (obj.mesh.position.z < player.z - 100) {
scene.remove(obj.mesh);
worldObjects.splice(i, 1);
}
}
}
function spawnObject(z) {
let hurdleRate = Math.min(0.5, 0.15 + (currentStage * 0.05));
let type = (Math.random() < 0.1) ? "score" : (Math.random() < 0.05) ? "heal" : "block";
if (Math.random() < hurdleRate) type = "hurdle";
const colors = { block: 0xaa5533, hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
let stackCount = 1;
if (type === 'block') stackCount = Math.floor(Math.random() * 5) + 1;
let planeX = (Math.random()-0.5)*PLANE_W*2;
let tubeAngle = Math.random()*Math.PI*2;
for (let k = 0; k < stackCount; k++) {
let geo = (type === 'hurdle') ? new THREE.BoxGeometry(4,4,4) : ( (type==='score'||type==='heal') ? new THREE.BoxGeometry(1.5,1.5,1.5) : new THREE.BoxGeometry(2.5,2.5,2.5) );
const mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ color: colors[type] }));
// ブースト速度だと一瞬で通り過ぎるので、ブロック間の隙間を広げる
let zOffset = z + (k * 4.0);
if (viewMode === "F2") {
let yPos = (type==='hurdle'?2:1.25);
mesh.position.set(planeX, yPos, zOffset);
} else {
let baseR = TUBE_R;
if (type === 'hurdle') baseR -= 2;
else if (type === 'block') baseR -= 1.25;
else baseR -= 0.7;
mesh.position.set(Math.sin(tubeAngle)*baseR, -Math.cos(tubeAngle)*baseR, zOffset);
mesh.rotation.z = tubeAngle;
}
scene.add(mesh);
worldObjects.push({ mesh, type, flying: false, vel: new THREE.Vector3() });
}
}
function handleCollision(obj, index) {
const isHighSpeed = player.vz >= (NORMAL_MAX_SPEED + 2.0);
if (obj.type === "block") {
if (isHighSpeed) {
sound.play('crash');
createExplosion(obj.mesh.position, obj.mesh.material.color, 1.5);
scene.remove(obj.mesh);
worldObjects.splice(index, 1);
score += 50; showPopText("SMASH! +50", "#ffaa00");
} else {
sound.play('crash');
obj.flying = true;
obj.vel.set((Math.random()-0.5)*2, 2, 5);
score += 10; showPopText("+10", "#ffaa00");
player.vz *= 0.8;
}
} else if (obj.type === "hurdle") {
sound.play('crash');
if (player.vz > 15.0) { // 超高速時はペナルティ軽減
hp -= 1; showPopText("IMPACT! HP -1", "#ff5500");
obj.flying = true;
obj.vel.set((Math.random()-0.5)*5, 5, 10);
player.vz *= 0.7;
} else {
hp -= 2; showPopText("CRASH! HP -2", "#ff0000");
obj.flying = true;
obj.vel.set(0, 2, 2);
player.vz *= 0.5;
}
}
else if (obj.type === "score") {
sound.play('coin');
score += 100; showPopText("+100", "#ffff00");
}
else if (obj.type === "heal") {
sound.play('heal');
hp = Math.min(MAX_HP, hp + 1); showPopText("RECOVER!", "#00ff00");
}
}
function createExplosion(pos, color, scaleFactor) {
const count = 12 * scaleFactor;
for(let i=0; i<count; i++) {
const size = 0.6 * scaleFactor;
const mesh = new THREE.Mesh(new THREE.BoxGeometry(size, size, size), new THREE.MeshPhongMaterial({ color }));
mesh.position.copy(pos);
scene.add(mesh);
// 爆散速度アップ
const vel = new THREE.Vector3((Math.random()-0.5)*5, Math.random()*5, 5.0+Math.random()*10);
debris.push({ mesh, vel, life: 60 });
}
}
function updateDebris() {
for(let i = debris.length - 1; i >= 0; i--) {
const d = debris[i];
d.mesh.position.add(d.vel);
d.mesh.rotation.x += 0.2;
d.life--;
if(d.life <= 0) {
scene.remove(d.mesh);
debris.splice(i, 1);
}
}
}
function showPopText(text, color) {
const div = document.createElement('div');
div.className = 'pop-text';
div.style.color = color;
div.innerText = text;
div.style.left = "50%"; div.style.top = "40%";
document.getElementById('ui-layer').appendChild(div);
setTimeout(() => div.remove(), 800);
}
function updateUI() {
document.getElementById('score-info').innerText = "SCORE: " + score;
document.getElementById('hp-txt').innerText = "HP: " + hp;
document.getElementById('hp-fill').style.width = Math.max(0, (hp / MAX_HP * 100)) + "%";
const spd = (player.vz * 40).toFixed(0);
document.getElementById('speed-txt').innerText = "SPEED: " + spd;
// スピードゲージ
const speedRatio = player.vz / BOOST_MAX_SPEED;
document.getElementById('speed-fill').style.width = Math.min(100, speedRatio * 100) + "%";
if (player.isBoosting) document.getElementById('speed-fill').style.backgroundColor = "#ff00ff";
else document.getElementById('speed-fill').style.backgroundColor = "#00ffff";
document.getElementById('time-txt').innerText = "TIME: " + Math.max(0, timeLeft).toFixed(0);
document.getElementById('time-fill').style.width = (timeLeft / MAX_TIME * 100) + "%";
}
init();
</script>
</body>
</html>【操作説明】
・左右キー … 自機を左右に移動。
・上キー … アクセル。
・下キー … ブレーキ。
・スペースキー … ジャンプ。
・F1キー … チューブモードの一人称視点に切り替え。
・F2キー … 平面モードに切り替え。
・F3キー … チューブモードの三人称視点に切り替え。
・障害物を避けて、高得点を目指しましょう。
【おまけ】
・チューブ系のゲームを検索してみたところ、
やはりいくつかあったので紹介しておきます。
1981 Atari Tempest Arcade
・これはレースゲームというか、シューティングみたいな感じですね。
1983 ジャイラス コナミ
・これもシューティングゲームで、やや滑らかに回る感じ。
Bullfrog - Tube - 1995
・かなりレースゲームっぽい感じになってきました。
Ballistics (2001) - PC
これは障害物を避けていくパターンで、一応、レースゲームなのかな?
Tube Slider (2003) GameCube
・これは任天堂のレースゲームで、
「F-ZERO」の後継シリーズとして開発されていたとのこと。
Space Giraffe(2007)Llamasoft
・これはまたシューティングゲームで、テンペストっぽい感じ。
「Race The Sun」Flippfly (2013)
・これはチューブ系ではないものの、平面モードはわりと近いかも。
「Thumper」Drool(2016)
・これはリズム系のゲームで、
タイミングを合わせると障害物を回避できる。
「Tunnel Rush」(2024?)
・これも障害物を避けまくるゲーム。
とまあ、似たような雰囲気のゲームは
これまでにもあったみたいなんですが、
ぶっ壊しに主軸をおいたゲームでは無かったみたいなので、
まあまあセーフだったりするのかも知れない。(^^;
3Dのこういう真正面を向いたゲームは、
合わせるのが難しいので、
気軽に遊べるのがいいのではないだろうか。uu


コメント