
HTMLのみで簡単に作れるブラウザ3Dゲーム
🧠「Three.js」という3Dライブラリ
このゲームは、「Three.js」という3Dライブラリを使って、ブラウザだけで作られています。専門的なソフトをインストールしなくても、このHTMLファイル一つで動くのが特徴です。
「3D空間で動くカラフルな的を、マウスでクリックして撃ち落とす」という、とてもシンプルなシューティングゲームです。

🎯 基本ルール(超シンプル)
制限時間は30秒:右上のタイマーがゼロになるまでプレイします。
的をクリックで破壊:3D空間に浮かぶ色とりどりの球体(的)を、マウスの十字線で狙ってクリックすると、弾が当たって的が消えます。
スコアを競う:的を1つ破壊するごとに、左上のスコアが1増えます。時間内にどれだけ多くの的を壊せるかが勝負です。
🎮 操作方法
射撃:的をクリックするだけ(照準は画面中央に固定)。
視点移動:マウスをドラッグすると、カメラが回転し、周囲の的を探せます。
再プレイ:ゲームオーバー時に表示される「もう一度」ボタンをクリック。

✨ ゲームの見どころ
的が動く:的はゆっくりとしたランダムな方向に動き、壁で跳ね返るので、狙いを定めるのに少しコツがいります。
派手なエフェクト:的を撃つと、その色に合わせた小さな火花(パーティクル)が飛び散ります。
カラフルなビジュアル:的はランダムな色で生成され、宇宙空間のような美しい背景に浮かんでいます。
奥行きのある3D空間:マウスドラッグで視点を変えられるので、的の位置を立体的に把握する必要があります。
デスクトップにindex.htmlで保存して、クリックしてブラウザで開くだけで遊べるゲームとなります。何も知識がなくても、パソコンあれば1分で試すことができます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3Dシューティング - ベクトル空間でエイム練習</title>
<style>
body { margin: 0; overflow: hidden; font-family: 'Segoe UI', sans-serif; }
#ui {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
#score {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0,0,0,0.7);
color: #ffaa00;
padding: 12px 24px;
border-radius: 30px;
font-size: 24px;
font-weight: bold;
border: 1px solid rgba(255,170,0,0.3);
backdrop-filter: blur(5px);
pointer-events: none;
}
#score span {
color: white;
font-weight: normal;
}
#timer {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0,0,0,0.7);
color: #66ffaa;
padding: 12px 24px;
border-radius: 30px;
font-size: 24px;
font-weight: bold;
border: 1px solid rgba(102,255,170,0.3);
backdrop-filter: blur(5px);
pointer-events: none;
}
#timer span {
color: white;
font-weight: normal;
}
#message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 48px;
font-weight: bold;
text-shadow: 0 0 30px rgba(0,0,0,0.9);
background: rgba(0,0,0,0.6);
padding: 30px 60px;
border-radius: 20px;
border: 2px solid rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
text-align: center;
pointer-events: none;
opacity: 0;
transition: opacity 0.5s;
}
#message.show {
opacity: 1;
}
#message .sub {
font-size: 20px;
color: #aaa;
margin-top: 10px;
}
#crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
pointer-events: none;
opacity: 0.8;
}
#crosshair::before,
#crosshair::after {
content: '';
position: absolute;
background: rgba(255,255,255,0.7);
}
#crosshair::before {
width: 2px;
height: 100%;
left: 50%;
transform: translateX(-50%);
}
#crosshair::after {
width: 100%;
height: 2px;
top: 50%;
transform: translateY(-50%);
}
#crosshair .dot {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 4px;
height: 4px;
background: #ff4444;
border-radius: 50%;
}
#restart-btn {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
background: #ffaa00;
color: #000;
border: none;
padding: 12px 40px;
font-size: 18px;
font-weight: bold;
border-radius: 30px;
cursor: pointer;
pointer-events: all;
box-shadow: 0 4px 20px rgba(255,170,0,0.3);
transition: transform 0.2s, box-shadow 0.2s;
opacity: 0;
transition: opacity 0.5s, transform 0.2s;
}
#restart-btn.show {
opacity: 1;
}
#restart-btn:hover {
transform: translateX(-50%) scale(1.05);
box-shadow: 0 6px 30px rgba(255,170,0,0.5);
}
#hint {
position: absolute;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
color: rgba(255,255,255,0.4);
font-size: 14px;
background: rgba(0,0,0,0.3);
padding: 6px 20px;
border-radius: 20px;
pointer-events: none;
white-space: nowrap;
}
</style>
</head>
<body>
<div id="ui">
<div id="score">🎯 <span id="score-value">0</span></div>
<div id="timer">⏱️ <span id="timer-value">30</span>s</div>
<div id="crosshair"><div class="dot"></div></div>
<div id="message">
<div id="msg-title">GAME OVER</div>
<div class="sub" id="msg-sub">スコア: 0</div>
</div>
<button id="restart-btn">🔄 もう一度</button>
<div id="hint">🖱️ クリックでターゲットを撃つ · ドラッグで回転</div>
</div>
<!-- Three.js -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.128.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// --- ゲーム状態 ---
const state = {
score: 0,
time: 30,
isPlaying: false,
targets: [],
spawnInterval: null,
timerInterval: null,
};
// --- DOM要素 ---
const scoreEl = document.getElementById('score-value');
const timerEl = document.getElementById('timer-value');
const messageEl = document.getElementById('message');
const msgTitle = document.getElementById('msg-title');
const msgSub = document.getElementById('msg-sub');
const restartBtn = document.getElementById('restart-btn');
// --- シーン設定 ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, 8);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.body.prepend(renderer.domElement);
// --- コントロール ---
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = false;
controls.minDistance = 3;
controls.maxDistance = 20;
controls.target.set(0, 0, 0);
controls.update();
// --- 照明 ---
const ambient = new THREE.AmbientLight(0x404060);
scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffeedd, 1);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
const fillLight = new THREE.DirectionalLight(0x4488ff, 0.3);
fillLight.position.set(-5, 0, 5);
scene.add(fillLight);
// --- 床と星 ---
const grid = new THREE.GridHelper(12, 20, 0x4488ff, 0x224488);
grid.position.y = -1.5;
scene.add(grid);
// 星
const starGeo = new THREE.BufferGeometry();
const starCount = 2000;
const starPos = new Float32Array(starCount * 3);
for (let i = 0; i < starCount * 3; i++) {
starPos[i] = (Math.random() - 0.5) * 200;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({ color: 0x88aaff, size: 0.08, transparent: true });
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
// --- 中心にマーカー ---
const center = new THREE.Mesh(
new THREE.SphereGeometry(0.1, 8, 8),
new THREE.MeshBasicMaterial({ color: 0xffaa44 })
);
scene.add(center);
// --- レイキャスター (クリック検出) ---
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// --- 敵(ターゲット)生成関数 ---
function spawnTarget() {
if (!state.isPlaying) return;
// ランダムな位置(範囲: -4〜4, yは0〜3)
const x = (Math.random() - 0.5) * 8;
const y = Math.random() * 3 + 0.5;
const z = (Math.random() - 0.5) * 8;
// 色: ランダムな派手な色
const hue = Math.random();
const color = new THREE.Color().setHSL(hue, 0.8, 0.6);
const size = 0.3 + Math.random() * 0.3;
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(size, 12, 12),
new THREE.MeshStandardMaterial({
color: color,
emissive: color,
emissiveIntensity: 0.2,
metalness: 0.2,
roughness: 0.4,
})
);
sphere.position.set(x, y, z);
sphere.userData.isTarget = true;
sphere.userData.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.02
);
sphere.castShadow = true;
// リング(オーラ)
const ring = new THREE.Mesh(
new THREE.RingGeometry(size * 1.4, size * 1.8, 16),
new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
})
);
ring.position.copy(sphere.position);
ring.lookAt(camera.position);
sphere.userData.ring = ring;
scene.add(ring);
scene.add(sphere);
state.targets.push(sphere);
}
// --- ゲーム開始 ---
function startGame() {
// リセット
state.score = 0;
state.time = 30;
state.isPlaying = true;
state.targets = [];
// 既存のターゲットを削除
scene.children.forEach(child => {
if (child.userData && child.userData.isTarget) {
scene.remove(child);
if (child.userData.ring) {
scene.remove(child.userData.ring);
}
}
});
// UI更新
scoreEl.textContent = '0';
timerEl.textContent = '30';
messageEl.classList.remove('show');
restartBtn.classList.remove('show');
document.getElementById('hint').style.opacity = '1';
// 最初に5個配置
for (let i = 0; i < 5; i++) {
spawnTarget();
}
// 定期生成 (0.8秒ごと)
if (state.spawnInterval) clearInterval(state.spawnInterval);
state.spawnInterval = setInterval(() => {
if (state.isPlaying) {
spawnTarget();
// 最大15個までに制限
if (state.targets.length > 15) {
const old = state.targets.shift();
scene.remove(old);
if (old.userData && old.userData.ring) {
scene.remove(old.userData.ring);
}
}
}
}, 800);
// タイマー (1秒ごと)
if (state.timerInterval) clearInterval(state.timerInterval);
state.timerInterval = setInterval(() => {
state.time--;
timerEl.textContent = state.time;
if (state.time <= 0) {
endGame();
}
}, 1000);
}
// --- ゲーム終了 ---
function endGame() {
state.isPlaying = false;
if (state.spawnInterval) clearInterval(state.spawnInterval);
if (state.timerInterval) clearInterval(state.timerInterval);
msgTitle.textContent = '💥 GAME OVER';
msgSub.textContent = `スコア: ${state.score} 体破壊!`;
messageEl.classList.add('show');
restartBtn.classList.add('show');
document.getElementById('hint').style.opacity = '0';
}
// --- クリック(射撃)処理 ---
renderer.domElement.addEventListener('click', (event) => {
if (!state.isPlaying) return;
// マウス座標を正規化
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// シーン内のターゲット球体を取得
const meshes = [];
scene.children.forEach(child => {
if (child.isMesh && child.userData && child.userData.isTarget) {
meshes.push(child);
}
});
const intersects = raycaster.intersectObjects(meshes);
if (intersects.length > 0) {
const hit = intersects[0].object;
if (hit.userData.isTarget) {
// 破壊!
const idx = state.targets.indexOf(hit);
if (idx !== -1) {
state.targets.splice(idx, 1);
}
// リングも削除
if (hit.userData.ring) {
scene.remove(hit.userData.ring);
}
scene.remove(hit);
// スコア加算
state.score++;
scoreEl.textContent = state.score;
// 小さな爆発エフェクト(パーティクル)
createExplosion(hit.position.clone(), hit.material.color.clone());
}
}
});
// --- 爆発エフェクト ---
function createExplosion(position, color) {
const count = 20;
const particles = [];
for (let i = 0; i < count; i++) {
const geo = new THREE.SphereGeometry(0.04, 4, 4);
const mat = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 1,
});
const p = new THREE.Mesh(geo, mat);
p.position.copy(position);
const dir = new THREE.Vector3(
(Math.random() - 0.5) * 0.15,
(Math.random() - 0.5) * 0.15,
(Math.random() - 0.5) * 0.15
);
p.userData.vel = dir;
p.userData.life = 1.0;
scene.add(p);
particles.push(p);
}
// アニメーションループで消えるように
const explodeInterval = setInterval(() => {
let alive = false;
particles.forEach(p => {
p.position.add(p.userData.vel);
p.userData.vel.multiplyScalar(0.98);
p.userData.life -= 0.03;
p.material.opacity = p.userData.life;
p.scale.setScalar(p.userData.life);
if (p.userData.life > 0) alive = true;
});
if (!alive) {
clearInterval(explodeInterval);
particles.forEach(p => scene.remove(p));
}
}, 16);
}
// --- アニメーション ---
function animate() {
requestAnimationFrame(animate);
// ターゲットをゆっくり動かす
state.targets.forEach(t => {
t.position.x += t.userData.velocity.x;
t.position.y += t.userData.velocity.y;
t.position.z += t.userData.velocity.z;
// 壁で跳ね返り
if (Math.abs(t.position.x) > 4.5) t.userData.velocity.x *= -1;
if (t.position.y > 3.5 || t.position.y < 0.2) t.userData.velocity.y *= -1;
if (Math.abs(t.position.z) > 4.5) t.userData.velocity.z *= -1;
// リングも追従
if (t.userData.ring) {
t.userData.ring.position.copy(t.position);
t.userData.ring.lookAt(camera.position);
}
});
stars.rotation.y += 0.0002;
controls.update();
renderer.render(scene, camera);
}
animate();
// --- リサイズ ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- 再起動ボタン ---
restartBtn.addEventListener('click', () => {
startGame();
});
// --- ゲームスタート! ---
startGame();
console.log('🎯 3Dシューティングゲーム開始!');
console.log('🖱️ ターゲットをクリックして撃ち落とせ!');
</script>
</body>
</html>