ブラウザで遊べるドライブゲームImageBooster
【操作説明】
・左右キーで自機を左右に移動。
・上キーで加速。
・下キーで減速。
・スペースキーでジャンプ。
・障害物を避けて、高得点を目指しましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Image Booster 1.0</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: 24px; }
#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 0.8s ease-out forwards; white-space: nowrap; transform: translateX(-50%); }
@keyframes moveUpFade { 0% { transform: translate(-50%, 0); opacity: 1; } 100% { transform: translate(-50%, -120px); opacity: 0; } }
#speed-lines { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, transparent 30%, rgba(255,255,255,0.3) 100%); display: none; mask-image: repeating-conic-gradient(from 0deg, #000 0deg 1deg, transparent 1deg 10deg); -webkit-mask-image: repeating-conic-gradient(from 0deg, #000 0deg 1deg, transparent 1deg 10deg); }
</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="speed-lines"></div>
<div id="hud">
<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 STAGE_TIME_LIMIT = 180;
const LANE_WIDTH = 12;
let scene, camera, renderer, clock, cameraHolder;
let gameState = "TITLE";
let score = 0, hp = MAX_HP, timeLeft = STAGE_TIME_LIMIT;
let isBoost = false; // 今回の仕様:上キーを離すまで継続
let player = { mesh: null, x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0.2, rotZ: 0, rotX: 0 };
let worldObjects = [], particles = [], stars;
let keys = {};
let nextSpawnZ = 50;
function PlaySound(filename) { new Audio(`./Sound/${filename}`).play().catch(()=>{}); }
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000005);
scene.fog = new THREE.Fog(0x000005, 50, 250);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// カメラ構造の改善 (逆さま防止)
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
cameraHolder = new THREE.Group();
cameraHolder.add(camera);
scene.add(cameraHolder);
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const sun = new THREE.DirectionalLight(0xffffff, 1.2);
sun.position.set(10, 20, 10);
scene.add(sun);
player.mesh = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.6, 2.5), new THREE.MeshPhongMaterial({ color: 0x00aaff }));
scene.add(player.mesh);
const groundTex = new THREE.TextureLoader().load('./BgImage/ground.png', (tex)=>{
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(5, 1000);
});
const ground = new THREE.Mesh(new THREE.PlaneGeometry(30, 10000), new THREE.MeshPhongMaterial({ map: groundTex, color: 0x222222 }));
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
createStarfield();
window.addEventListener('keydown', (e) => { keys[e.code] = true; if(e.code === 'Space') handleSpace(); });
window.addEventListener('keyup', (e) => keys[e.code] = false);
clock = new THREE.Clock();
changeState("TITLE");
animate();
}
function createStarfield() {
const starCoords = [];
for(let i=0; i<2000; i++) starCoords.push((Math.random()-0.5)*500, (Math.random()-0.5)*500, Math.random()*500);
const starGeo = new THREE.BufferGeometry().setAttribute('position', new THREE.Float32BufferAttribute(starCoords, 3));
stars = new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.8 }));
scene.add(stars);
}
function handleSpace() {
if (gameState === "TITLE") changeState("STAGE_START");
else if (gameState === "STAGE_START") changeState("PLAYING");
else if (gameState === "PLAYING" && player.y <= 0.01) { player.vy = 0.6; PlaySound("jump_up.mp3"); }
}
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</p>";
else if(s === "STAGE_START") { menu.innerHTML = "<h1>STAGE 1</h1><p>READY?</p>"; resetPlayer(); }
else if(s === "PLAYING") menu.innerHTML = "";
}
function resetPlayer() {
player.x = 0; player.y = 0; player.z = 0; player.vz = 0.2; player.rotZ = 0;
hp = MAX_HP; score = 0; isBoost = false;
}
function showPopText(text, color) {
const div = document.createElement('div');
div.className = 'pop-text';
div.innerText = text;
div.style.color = color;
div.style.left = "50%"; div.style.top = "40%";
document.getElementById('ui-layer').appendChild(div);
setTimeout(() => div.remove(), 800);
}
function animate() {
requestAnimationFrame(animate);
if (gameState === "PLAYING") {
updatePhysics();
updateObjects();
updateParticles();
stars.position.z = -(player.z % 500);
}
updateCamera();
renderer.render(scene, camera);
}
function updatePhysics() {
// 加速・減速ロジック
if (keys['ArrowUp']) {
player.vz += (isBoost) ? 0.06 : 0.02; // ブースト中は超加速
} else {
if (isBoost) isBoost = false; // 上キーを離すとブースト終了
player.vz *= 0.992; // 緩やかな減速
}
if (keys['ArrowDown']) {
player.vz *= 0.92; // 急ブレーキ
if(player.vz > 0.3) PlaySound("break.mp3");
}
let maxSpd = isBoost ? 4.5 : 1.5;
player.vz = Math.max(0.1, Math.min(player.vz, maxSpd));
// 左右
if (keys['ArrowLeft']) { player.vx = THREE.MathUtils.lerp(player.vx, 0.4, 0.1); player.rotZ += 0.15; }
else if (keys['ArrowRight']) { player.vx = THREE.MathUtils.lerp(player.vx, -0.4, 0.1); player.rotZ -= 0.15; }
else {
player.vx = THREE.MathUtils.lerp(player.vx, 0, 0.1);
player.rotZ = THREE.MathUtils.lerp(player.rotZ, Math.round(player.rotZ/(Math.PI*2))*(Math.PI*2), 0.1);
}
player.x = Math.max(-LANE_WIDTH, Math.min(LANE_WIDTH, player.x + player.vx));
player.y += player.vy;
if (player.y > 0) { player.vy -= 0.03; player.rotX = player.vy * 0.5; }
else { player.y = 0; player.vy = 0; player.rotX = 0; }
player.z += player.vz;
player.mesh.position.set(player.x, player.y + 0.5, player.z);
player.mesh.rotation.z = player.rotZ;
player.mesh.rotation.x = -player.rotX;
timeLeft -= 1/60;
updateUI();
}
function updateCamera() {
// ホルダーをプレイヤーの位置に
cameraHolder.position.set(player.x * 0.7, player.y + 5, player.z - 12);
// プレイヤーを注視
camera.lookAt(player.x, player.y + 1, player.z + 10);
// 左右の傾き(ホルダーを回すことで逆さまを防止)
cameraHolder.rotation.z = player.rotZ * 0.2;
}
function updateObjects() {
if (player.z + 150 > nextSpawnZ) { spawnObject(nextSpawnZ); nextSpawnZ += 12; }
worldObjects.forEach((obj, idx) => {
if(obj.flying) {
obj.mesh.position.add(obj.vel);
obj.mesh.rotation.x += 0.2; obj.vel.y -= 0.01;
} else if (player.mesh.position.distanceTo(obj.mesh.position) < 2.5) {
handleCollision(obj);
}
if (obj.mesh.position.z < player.z - 30 || obj.mesh.position.y < -10) {
scene.remove(obj.mesh); worldObjects.splice(idx, 1);
}
});
}
function spawnObject(z) {
const types = ["block", "hurdle", "score", "boost", "heal"];
const type = types[Math.floor(Math.random() * types.length)];
const colors = { block: 0x884422, hurdle: 0xff3300, score: 0xffff00, boost: 0x00ffff, heal: 0x00ff00 };
const mesh = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshPhongMaterial({ color: colors[type] }));
mesh.position.set((Math.random() - 0.5) * LANE_WIDTH * 2, 1, z);
scene.add(mesh);
worldObjects.push({ mesh, type, flying: false, vel: new THREE.Vector3() });
}
function handleCollision(obj) {
if (obj.type === "block") {
score += 10; PlaySound("block.mp3"); showPopText("+10", "#ffaa00");
createExplosion(obj.mesh.position, 0xffaa00, 15);
obj.flying = true; obj.vel.set((Math.random()-0.5)*1, 0.5, 2.0);
} else if (obj.type === "hurdle") {
if (isBoost) {
score += 50; PlaySound("block.mp3"); showPopText("+50", "#ff3300");
createExplosion(obj.mesh.position, 0xff3300, 20);
obj.flying = true; obj.vel.set(2, 0.5, 3.0); // ブースト時は激しく吹っ飛ぶ
} else {
hp--; PlaySound("hurdle.mp3"); showPopText("HP-1", "#ff69b4");
createExplosion(obj.mesh.position, 0xffffff, 10);
obj.flying = true; obj.vel.set(0.5, 0.1, 0.5);
}
} else if (obj.type === "score") {
score += 100; PlaySound("heal.mp3"); showPopText("+100", "#ffff00");
scene.remove(obj.mesh);
} else if (obj.type === "boost") {
isBoost = true; PlaySound("boost.mp3"); showPopText("SUPER BOOST!", "#00ffff");
scene.remove(obj.mesh);
} else if (obj.type === "heal") {
hp = Math.min(MAX_HP, hp + 1); showPopText("HP+1", "#00ff00");
scene.remove(obj.mesh);
}
}
function createExplosion(pos, color, count) {
for (let i = 0; i < count; i++) {
const p = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.3), new THREE.MeshBasicMaterial({ color: color }));
p.position.copy(pos);
const vel = new THREE.Vector3((Math.random()-0.5)*0.8, Math.random()*0.8, (Math.random()-0.5)*0.8);
scene.add(p);
particles.push({ mesh: p, vel: vel, life: 40 });
}
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]; p.mesh.position.add(p.vel); p.vel.y -= 0.02;
if (--p.life <= 0) { scene.remove(p.mesh); particles.splice(i, 1); }
}
}
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);
document.getElementById('speed-lines').style.display = isBoost ? 'block' : 'none';
}
init();
</script>
</body>
</html>

コメント