ブラウザで遊べるドライブゲームImageBooster
【操作説明】
・左右キー … 自機を左右に移動。
・上キー … アクセル。
・下キー … ブレーキ。
・スペースキー … ジャンプ。
・F1キー … チューブモードの一人称視点に切り替え。
・F2キー … 平面モードに切り替え。
・F3キー … チューブモードの三人称視点に切り替え。
・障害物を避けて、高得点を目指しましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Image Booster 1.0 - Chase Camera Fixed</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; }
#hud { position: absolute; top: 20px; left: 20px; text-align: left; display: none; font-size: 22px; line-height: 1.2; }
#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="ui-layer">
<div id="hud">
<div id="view-mode" style="color:#0f0">VIEW: 3RD PERSON</div>
<div id="hp-disp">HP: 10</div>
<div id="score-disp">SCORE: 0</div>
<div id="time-disp">TIME: 180.00</div>
</div>
<div id="center-text"></div>
</div>
<script type="module">
import * as THREE from 'three';
const MAX_HP = 10;
const TUBE_R = 15;
const PLANE_W = 12;
let scene, camera, renderer;
let gameState = "TITLE", viewMode = "F3";
let score = 0, hp = MAX_HP, timeLeft = 180;
let isBoost = false;
let player = { mesh: null, angle: 0, x: 0, z: 0, vz: 0.2, vAngle: 0, vx: 0 };
let worldObjects = [], groundTube, groundPlane;
let keys = {};
let nextSpawnZ = 50;
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000005);
scene.fog = new THREE.Fog(0x000005, 50, 500);
renderer = new THREE.WebGLRenderer({ antialias: true });
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.8));
const sun = new THREE.DirectionalLight(0xffffff, 1.0);
sun.position.set(0, 20, 10);
scene.add(sun);
player.mesh = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.6, 2.5), new THREE.MeshPhongMaterial({ color: 0x00aaff }));
scene.add(player.mesh);
groundTube = new THREE.Mesh(
new THREE.CylinderGeometry(TUBE_R, TUBE_R, 200000, 32, 1, true),
new THREE.MeshPhongMaterial({ color: 0x333344, side: THREE.BackSide, wireframe: true })
);
groundTube.rotation.x = Math.PI / 2;
scene.add(groundTube);
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) => {
keys[e.code] = true;
if(["F1", "F2", "F3"].includes(e.code)) {
e.preventDefault();
viewMode = e.code;
updateViewLabel();
}
if(e.code === 'Space') handleSpace();
});
window.addEventListener('keyup', (e) => keys[e.code] = false);
updateViewLabel();
changeState("TITLE");
animate();
}
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") changeState("STAGE_START");
else if (gameState === "STAGE_START") changeState("PLAYING");
}
function changeState(s) {
gameState = s;
document.getElementById('hud').style.display = (s === "PLAYING") ? "block" : "none";
const menu = document.getElementById('center-text');
if(s === "TITLE") menu.innerHTML = "<h1>Image Booster 1.0</h1><p>PRESS SPACE TO START</p>";
else if(s === "STAGE_START") { menu.innerHTML = "<h1>READY?</h1><p>SPACE: GO!</p>"; resetPlayer(); }
else if(s === "PLAYING") menu.innerHTML = "";
else if(s === "GAMEOVER") menu.innerHTML = "<h1 style='color:red;'>GAME OVER</h1><p>SCORE: "+score+"</p><p>SPACE TO RESTART</p>";
}
function resetPlayer() {
player.angle = 0; player.x = 0; player.z = 0; player.vz = 0.5;
hp = MAX_HP; score = 0; timeLeft = 180; isBoost = false;
worldObjects.forEach(obj => scene.remove(obj.mesh));
worldObjects = [];
nextSpawnZ = 50;
}
function animate() {
requestAnimationFrame(animate);
if (gameState === "PLAYING") {
updatePhysics();
updateObjects();
}
updateCamera();
renderer.render(scene, camera);
}
function updatePhysics() {
if (keys['ArrowUp']) player.vz += (isBoost ? 0.08 : 0.04);
else { isBoost = false; player.vz *= 0.995; }
if (keys['ArrowDown']) player.vz *= 0.90;
player.vz = Math.max(0.1, Math.min(player.vz, 2.5));
if (viewMode === "F2") {
if (keys['ArrowLeft']) player.vx = THREE.MathUtils.lerp(player.vx, 0.7, 0.1);
else if (keys['ArrowRight']) player.vx = THREE.MathUtils.lerp(player.vx, -0.7, 0.1);
else player.vx *= 0.9;
player.x = Math.max(-PLANE_W, Math.min(PLANE_W, player.x + player.vx));
player.mesh.position.set(player.x, 0.5, player.z);
player.mesh.rotation.set(0, 0, player.vx * 0.3);
} else {
if (keys['ArrowLeft']) player.vAngle = THREE.MathUtils.lerp(player.vAngle, 0.08, 0.1);
else if (keys['ArrowRight']) player.vAngle = THREE.MathUtils.lerp(player.vAngle, -0.08, 0.1);
else player.vAngle *= 0.9;
player.angle += player.vAngle;
player.mesh.position.set(
Math.sin(player.angle) * (TUBE_R - 0.5),
-Math.cos(player.angle) * (TUBE_R - 0.5),
player.z
);
player.mesh.rotation.set(0, 0, player.angle);
}
player.z += player.vz;
timeLeft -= 1/60;
if (hp <= 0 || timeLeft <= 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 - 15);
camera.lookAt(player.x, 1, player.z + 30);
} else {
// 【F3 三人称視点】自機の少し手前の上から見下ろす
const camDist = 18; // 自機からの後方距離
const camHeight = 5; // チューブ中心方向(上空)への高さ
// カメラ位置:自機の位置から、中心方向に浮かせ、さらに後方に引く
camera.position.x = player.mesh.position.x - Math.sin(player.angle) * camHeight;
camera.position.y = player.mesh.position.y + Math.cos(player.angle) * camHeight;
camera.position.z = player.z - camDist;
// カメラの上向きは「チューブの中心」
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 + 20);
}
}
function updateObjects() {
if (player.z + 250 > nextSpawnZ) { spawnObject(nextSpawnZ); nextSpawnZ += 5; }
for (let i = worldObjects.length - 1; i >= 0; i--) {
const obj = worldObjects[i];
if(obj.flying) {
obj.mesh.position.add(obj.vel);
} else if (player.mesh.position.distanceTo(obj.mesh.position) < 2.5) {
handleCollision(obj);
if(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 type = (Math.random() < 0.1) ? "score" : (Math.random() < 0.05) ? "heal" : "block";
if (Math.random() < 0.15) type = "hurdle";
const colors = { block: 0x884422, hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
const mesh = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshPhongMaterial({ color: colors[type] }));
if (viewMode === "F2") {
mesh.position.set((Math.random()-0.5)*PLANE_W*2, 1, z);
} else {
const a = Math.random()*Math.PI*2;
mesh.position.set(Math.sin(a)*(TUBE_R - 1.0), -Math.cos(a)*(TUBE_R - 1.0), z);
mesh.rotation.z = a;
}
scene.add(mesh);
worldObjects.push({ mesh, type, flying: false, vel: new THREE.Vector3() });
}
function handleCollision(obj) {
if (obj.type === "block") {
score += 10; showPopText("+10", "#ffaa00");
obj.flying = true; obj.vel.set((Math.random()-0.5)*2, 2, 3);
} else if (obj.type === "hurdle") {
hp--; showPopText("HP -1", "#ff0000");
obj.flying = true; obj.vel.set(0, 1, 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 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 = "50%";
document.getElementById('ui-layer').appendChild(div);
setTimeout(() => div.remove(), 1000);
}
function updateUI() {
document.getElementById('hp-disp').innerText = "HP: " + hp;
document.getElementById('score-disp').innerText = "SCORE: " + score;
document.getElementById('time-disp').innerText = "TIME: " + Math.max(0, timeLeft).toFixed(2);
}
init();
</script>
</body>
</html>

コメント