ブラウザで遊べるドライブゲーム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: 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: 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="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';
const MAX_HP = 10, TUBE_R = 15, PLANE_W = 12, MAX_TIME = 60;
// 速度と加速を倍増
const MAX_SPEED = 5.0;
const ACCEL = 0.1;
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
};
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, 600); // 霧の範囲を広げて高速走行に対応
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, 3000);
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);
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;
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, 100);
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, 400000, 32, 1, true),
tubeMaterial
);
groundTube.rotation.x = Math.PI / 2;
scene.add(groundTube);
groundPlane = new THREE.Mesh(new THREE.PlaneGeometry(100, 400000), 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.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 = 3000;
const posArray = new Float32Array(starCount * 3);
for(let i=0; i<starCount * 3; i++) {
posArray[i] = (Math.random() - 0.5) * 1000;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
const starMat = new THREE.PointsMaterial({color: 0xffffff, size: 1.0, 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) {
tubeMaterial.map.offset.y = -player.z * 0.01;
}
}
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") {
if(gameState !== "TITLE") {
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.8;
}
}
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;
// HPのリセット条件を整理
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 += ACCEL; // 加速力アップ
document.getElementById('speed-lines').style.opacity = (player.vz / MAX_SPEED);
player.surge = THREE.MathUtils.lerp(player.surge, 3.0, 0.05);
} else {
player.vz *= 0.995;
document.getElementById('speed-lines').style.opacity = 0;
if (keys['ArrowDown']) {
player.vz *= 0.90;
player.surge = THREE.MathUtils.lerp(player.surge, -2.0, 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.03;
else { player.altitude = 0; player.jumpV = 0; }
let targetBank = 0;
if (viewMode === "F2") {
if (keys['ArrowLeft']) { player.vx = THREE.MathUtils.lerp(player.vx, 1.2, 0.1); targetBank = 0.5; }
else if (keys['ArrowRight']) { player.vx = THREE.MathUtils.lerp(player.vx, -1.2, 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, 0.1); targetBank = 0.8; }
else if (keys['ArrowRight']) { player.vAngle = THREE.MathUtils.lerp(player.vAngle, -0.12, 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;
// タイムが0になった瞬間にクリア判定
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");
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 - 20);
camera.lookAt(player.x, 1, player.z + 40);
} else {
const camDist = 20, camHeight = 6;
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 + 30);
}
}
function updateObjects() {
let spawnDist = Math.max(8, 20 - (currentStage * 2.0));
if (player.z + 400 > nextSpawnZ) {
spawnObject(nextSpawnZ);
nextSpawnZ += Math.random() * 5 + (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.0)) {
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: 0x884422, hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
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] }));
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.2 : 0.7);
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.5);
if (obj.type === "block") {
if (isMaxSpeed) {
createExplosion(obj.mesh.position, obj.mesh.material.color, 1.2);
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, 5);
score += 10; showPopText("+10", "#ffaa00");
}
} else if (obj.type === "hurdle") {
if (isMaxSpeed) {
hp -= 1; showPopText("OUCH! HP -1", "#ff5500");
obj.flying = true;
obj.vel.set((Math.random()-0.5)*3, 3, 6);
player.vz *= 0.85;
} 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") { 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 = 10 * scaleFactor;
for(let i=0; i<count; i++) {
const size = 0.5 * 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)*2, Math.random()*2, 3.0+Math.random()*4);
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.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('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)) + "%";
document.getElementById('speed-txt').innerText = "SPEED: " + (player.vz * 40).toFixed(0);
document.getElementById('speed-fill').style.width = (player.vz / MAX_SPEED * 100) + "%";
document.getElementById('speed-fill').style.backgroundColor = (player.vz >= MAX_SPEED - 0.5) ? "#fff" : "#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


コメント