ブラウザで遊べるドライブゲームImageBooster
・テキストファイルを新規作成して、(index.html)
以下のソースコードをコピペして保存し、ファイルをクリックすると、
webブラウザが起動してゲームが遊べます。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Image Booster 2.1 - Glass Road</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: 20px; font-size: 20px; color: #ff0; }
/* 集中線エフェクト(加速時のみ表示) */
#speed-lines {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: repeating-conic-gradient(transparent 0deg, transparent 2deg, rgba(255, 255, 255, 0.1) 2.1deg, transparent 2.5deg);
opacity: 0;
transition: opacity 0.2s 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.1s ease-out; }
#hp-bg { background: #ff0000; }
#hp-fill { background: #adff2f; }
#speed-bg { background: #000080; }
#speed-fill { background: #00ffff; }
#time-bg { background: #b8860b; }
#time-fill { background: #ffff00; }
#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; }
.pop-text { position: absolute; font-weight: bold; font-size: 38px; animation: moveUpFade 1s forwards; transform: translate(-50%, -50%); z-index: 999; }
@keyframes moveUpFade { 0% { transform: translate(-50%, 0); opacity: 1; } 100% { transform: translate(-50%, -150px); 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="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';
const MAX_HP = 10, TUBE_R = 15, PLANE_W = 12, MAX_TIME = 60, MAX_SPEED = 2.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;
// surge: 加速時の前後スライド量, bank: 旋回時の傾き
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
};
let worldObjects = [], debris = [], stars, groundTube, groundPlane;
let keys = {};
let nextSpawnZ = 50;
let tubeMaterial; // スクロール用に保持
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000005);
scene.fog = new THREE.Fog(0x000005, 50, 400);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const sun = new THREE.DirectionalLight(0xffffff, 1.0);
sun.position.set(0, 20, 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);
// ガラスの床(チューブ)
// グリッドテクスチャをCanvasで動的生成
const canvas = document.createElement('canvas');
canvas.width = 512; canvas.height = 512;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#000000'; ctx.fillRect(0,0,512,512);
ctx.strokeStyle = '#00ffff'; ctx.lineWidth = 4;
// グリッドを描画
ctx.beginPath();
for(let i=0; i<=512; i+=64) {
ctx.moveTo(i, 0); ctx.lineTo(i, 512);
ctx.moveTo(0, i); ctx.lineTo(512, i);
}
ctx.stroke();
const gridTex = new THREE.CanvasTexture(canvas);
gridTex.wrapS = THREE.RepeatWrapping;
gridTex.wrapT = THREE.RepeatWrapping;
gridTex.repeat.set(12, 40); // チューブ全周と奥行きの繰り返し数
// 半透明マテリアル
tubeMaterial = new THREE.MeshPhongMaterial({
color: 0x444455,
map: gridTex,
side: THREE.BackSide,
transparent: true,
opacity: 0.6,
shininess: 100
});
groundTube = new THREE.Mesh(
new THREE.CylinderGeometry(TUBE_R, TUBE_R, 1000, 32, 1, true), // 長さは短くして追従させる手もあるが、今回は固定長でテクスチャスクロール
tubeMaterial
);
groundTube.rotation.x = Math.PI / 2;
// チューブ自体をカメラについてこさせるために、Z方向のスケールを大きく、または位置更新
// ここではシンプルに超巨大円柱にしておく(前回の仕様踏襲)
groundTube.geometry = new THREE.CylinderGeometry(TUBE_R, TUBE_R, 200000, 32, 1, true);
scene.add(groundTube);
// 平面モード用(F2)
groundPlane = new THREE.Mesh(new THREE.PlaneGeometry(100, 200000), new THREE.MeshPhongMaterial({ color: 0x111111 }));
groundPlane.rotation.x = -Math.PI / 2;
groundPlane.visible = false;
scene.add(groundPlane);
window.addEventListener('keydown', (e) => {
if(["F1", "F2", "F3"].includes(e.key) || ["F1", "F2", "F3"].includes(e.code)) {
e.preventDefault();
viewMode = e.code;
updateViewLabel();
}
keys[e.code] = true;
if(e.code === 'Space') {
e.preventDefault();
handleSpace();
}
});
window.addEventListener('keyup', (e) => keys[e.code] = false);
updateViewLabel();
changeState("TITLE");
animate();
}
function createStars() {
const starGeo = new THREE.BufferGeometry();
const starCount = 2000;
const posArray = new Float32Array(starCount * 3);
for(let i=0; i<starCount * 3; i++) {
posArray[i] = (Math.random() - 0.5) * 600;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
const starMat = new THREE.PointsMaterial({color: 0xffffff, size: 0.8, transparent: true});
stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
}
function updateStars() {
if(stars) {
stars.position.z = player.z;
stars.rotation.z += 0.001;
}
// 床のテクスチャスクロール(疾走感)
if(tubeMaterial) {
// UVのY座標をずらすことでスクロール表現
tubeMaterial.map.offset.y = -player.z * 0.02;
}
}
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") {
score = 0;
currentStage = 1;
changeState("STAGE_START");
}
else if (gameState === "STAGE_CLEAR") changeState("STAGE_START");
else if (gameState === "STAGE_START") changeState("PLAYING");
else if (gameState === "PLAYING" && player.altitude <= 0.01) {
// ジャンプ力アップ(障害物飛び越え用)
player.jumpV = 0.75;
}
}
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 2.1</h1><p>PRESS SPACE TO START</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;
if(gameState === "TITLE" || gameState === "GAMEOVER") { 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();
}
updateCamera();
updateStars();
renderer.render(scene, camera);
}
function updatePhysics() {
// 加速・減速処理
if (keys['ArrowUp']) {
player.vz += 0.04;
// 加速中は集中線ON
document.getElementById('speed-lines').style.opacity = 0.6;
// 前方へスライド(surge)
player.surge = THREE.MathUtils.lerp(player.surge, 2.0, 0.05);
} else {
player.vz *= 0.995;
// 減速時は集中線OFF
document.getElementById('speed-lines').style.opacity = 0;
if (keys['ArrowDown']) {
player.vz *= 0.90;
// ブレーキ時は後退
player.surge = THREE.MathUtils.lerp(player.surge, -1.5, 0.05);
} else {
// 何も押してない時は定位置へ戻る
player.surge = THREE.MathUtils.lerp(player.surge, 0, 0.05);
}
}
player.vz = Math.max(0.1, Math.min(player.vz, MAX_SPEED));
// ジャンプ重力
player.altitude += player.jumpV;
if (player.altitude > 0) player.jumpV -= 0.025; // 重力少し強め
else { player.altitude = 0; player.jumpV = 0; }
// 左右移動とバンク角
let targetBank = 0;
if (viewMode === "F2") {
if (keys['ArrowLeft']) { player.vx = THREE.MathUtils.lerp(player.vx, 0.7, 0.1); targetBank = 0.5; }
else if (keys['ArrowRight']) { player.vx = THREE.MathUtils.lerp(player.vx, -0.7, 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.08, 0.1); targetBank = 0.8; }
else if (keys['ArrowRight']) { player.vAngle = THREE.MathUtils.lerp(player.vAngle, -0.08, 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);
// 回転:チューブに沿った角度(angle) + 左右移動時の傾き(bank)
// チューブの内側に張り付くので、Z回転は angle が基本。そこに bank を足す。
// 左入力(vAngle>0) -> 左翼を下げるには、Z軸回転を増やす方向 (時計回りが-Zなら...Three.jsは反時計が+)
// 試行錯誤調整: angle + bank
player.mesh.rotation.set(0, 0, player.angle + player.bank);
}
player.z += player.vz;
timeLeft -= 1/60;
if (timeLeft <= 0) changeState("STAGE_CLEAR");
if (hp <= 0) changeState("GAMEOVER");
updateUI();
}
function updateCamera() {
player.mesh.visible = (viewMode !== "F1");
// カメラは player.z (論理位置) を追うが、meshは player.z + player.surge にある。
// これにより加速時に機体がカメラから離れ、減速時に近づく演出になる。
if (viewMode === "F1") {
camera.position.copy(player.mesh.position);
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, 6, player.z - 15);
camera.lookAt(player.x, 1, player.z + 30);
} else {
const camDist = 18, camHeight = 5;
// カメラ位置計算用のダミー座標(機体のバンクやサージの影響を受けない中心点)
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;
camera.position.z = player.z - camDist;
camera.up.set(-coreX, -coreY, 0).normalize();
camera.lookAt(coreX, coreY, player.z + 20);
}
}
function updateObjects() {
// 密度2倍:出現間隔を半分にする
let spawnDist = Math.max(10, 25 - (currentStage * 2.5));
if (player.z + 250 > nextSpawnZ) {
spawnObject(nextSpawnZ);
nextSpawnZ += Math.random() * 5 + (spawnDist / 2); // 間隔も狭く
}
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.0 : 2.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 - 50) {
scene.remove(obj.mesh);
worldObjects.splice(i, 1);
}
}
}
function spawnObject(z) {
let hurdleRate = Math.min(0.4, 0.1 + (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: 0x884422, hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
let geo;
if(type === 'hurdle') geo = new THREE.BoxGeometry(4, 4, 4);
else if(type === 'score' || type === 'heal') geo = new THREE.BoxGeometry(1, 1, 1);
else geo = new THREE.BoxGeometry(2, 2, 2);
const mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ color: colors[type] }));
// 一度に登場するブロック数を倍にする(2個同時沸き確率を追加)
// ただし単純にループでspawnObjectを呼ぶと再帰するので、
// ここでは「1回のspawnObject呼び出しで、追加でもう1個配置する」処理を入れるか、
// updateObjectsの呼び出し頻度で調整済みとする。
// 今回は updateObjects で spawnDist を半分にしたので、実質密度は倍になっている。
if (viewMode === "F2") mesh.position.set((Math.random()-0.5)*PLANE_W*2, (type==='hurdle'?2:1), z);
else {
const a = Math.random()*Math.PI*2;
const offset = (type==='hurdle') ? 2 : (type==='block' ? 1 : 0.5);
mesh.position.set(Math.sin(a)*(TUBE_R - offset), -Math.cos(a)*(TUBE_R - offset), z);
mesh.rotation.z = a;
}
scene.add(mesh);
worldObjects.push({ mesh, type, flying: false, vel: new THREE.Vector3() });
}
function handleCollision(obj, index) {
const isMaxSpeed = player.vz >= (MAX_SPEED - 0.2);
if (obj.type === "block") {
if (isMaxSpeed) {
createExplosion(obj.mesh.position, obj.mesh.material.color, 1);
scene.remove(obj.mesh);
worldObjects.splice(index, 1);
score += 20; showPopText("BREAK! +20", "#ffaa00");
} else {
obj.flying = true;
obj.vel.set((Math.random()-0.5)*2, 2, 3);
score += 10; showPopText("+10", "#ffaa00");
}
} else if (obj.type === "hurdle") {
// 障害物の新ルール
if (isMaxSpeed) {
// 最高速ならHP-1(スコアなし)
hp -= 1;
showPopText("OUCH! HP -1", "#ff5500");
// エフェクトとして弾く
obj.flying = true;
obj.vel.set((Math.random()-0.5)*2, 2, 3);
player.vz *= 0.8; // 少し減速
} else {
// 通常ならHP-2
hp -= 2;
showPopText("CRASH! HP -2", "#ff0000");
obj.flying = true;
obj.vel.set(0, 1, 1);
player.vz *= 0.5; // 大きく減速
}
}
else if (obj.type === "score") { score += 100; showPopText("+100", "#ffff00"); }
else if (obj.type === "heal") { hp = Math.min(MAX_HP, hp + 1); showPopText("HP +1", "#00ff00"); }
}
function createExplosion(pos, color, scaleFactor) {
const count = 8 * scaleFactor;
const size = 0.5 * scaleFactor;
for(let i=0; i<count; i++) {
const geo = new THREE.BoxGeometry(size, size, size);
const mat = new THREE.MeshPhongMaterial({ color: color });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
mesh.position.x += (Math.random()-0.5)*2;
mesh.position.y += (Math.random()-0.5)*2;
scene.add(mesh);
const vel = new THREE.Vector3((Math.random()-0.5)*1, Math.random()*1, 2.0+Math.random()*2);
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.mesh.rotation.z += 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(), 1000);
}
function updateUI() {
document.getElementById('hp-txt').innerText = "HP: " + hp;
document.getElementById('hp-fill').style.width = Math.max(0, (hp / MAX_HP * 100)) + "%";
document.getElementById('speed-txt').innerText = "SPEED: " + (player.vz * 40).toFixed(0);
document.getElementById('speed-fill').style.width = (player.vz / MAX_SPEED * 100) + "%";
if(player.vz >= MAX_SPEED - 0.1) document.getElementById('speed-fill').style.backgroundColor = "#fff";
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


コメント