diff --git a/src/components/Explosion.tsx b/src/components/Explosion.tsx new file mode 100644 index 0000000..65d2218 --- /dev/null +++ b/src/components/Explosion.tsx @@ -0,0 +1,127 @@ +import { useFrame } from "@react-three/fiber"; +import { useEffect, useRef, useState } from "react"; +import * as THREE from "three"; +import type { ExplosionData } from "@/types/Explosion"; + +type Fragment = { + id: string; + mesh: THREE.Mesh; + velocity: THREE.Vector3; + rotationAxis: THREE.Vector3; + lifetime: number; +}; + +type ExplosionProps = { + explosion: ExplosionData; + onComplete?: () => void; +}; + +export function Explosion({ explosion, onComplete }: ExplosionProps) { + const groupRef = useRef(null); + const [fragments, setFragments] = useState([]); + + // 爆発初期化 + useEffect(() => { + const newFragments: Fragment[] = []; + for (let i = 0; i < explosion.fragmentCount; i++) { + const id = crypto.randomUUID(); + + const size = Math.random() * (explosion.radius * 0.2) + 0.05; + const geometry = new THREE.SphereGeometry(size, 6, 6); + const material = new THREE.MeshStandardMaterial({ + color: 0xffaa33, + emissive: 0xff5500, + }); + const mesh = new THREE.Mesh(geometry, material); + + // 初期位置は惑星中心 + mesh.position.copy(explosion.position); + + // ランダム方向に飛ぶ速度 + const velocity = new THREE.Vector3( + (Math.random() - 0.5) * 4, + (Math.random() - 0.5) * 4, + (Math.random() - 0.5) * 4, + ); + + // 回転軸 + const rotationAxis = new THREE.Vector3( + Math.random(), + Math.random(), + Math.random(), + ).normalize(); + + newFragments.push({ + id, + mesh, + velocity, + rotationAxis, + lifetime: Math.random() * 2 + 1, // 1~3秒で消える + }); + } + setFragments(newFragments); + + return () => { + for (let i = 0; i < newFragments.length; i++) { + const f = newFragments[i]; + f.mesh.parent?.remove(f.mesh); + f.mesh.geometry.dispose(); + + const mat = f.mesh.material; + if (Array.isArray(mat)) { + for (let j = 0; j < mat.length; j++) { + mat[j].dispose(); + } + } else { + mat.dispose(); + } + } + }; + }, [explosion]); + + // フレームごとの更新 + useFrame((_, delta) => { + if (fragments.length === 0) return; + + setFragments((prev) => { + const alive: Fragment[] = []; + + for (let i = 0; i < prev.length; i++) { + const f = prev[i]; + + // 位置更新 + f.mesh.position.addScaledVector(f.velocity, delta); + + // 回転 + f.mesh.rotateOnAxis(f.rotationAxis, delta * 5); + + // 減速(摩擦的) + f.velocity.multiplyScalar(0.98); + + // 減衰 + f.lifetime -= delta; + + if (f.lifetime > 0) { + alive.push(f); + } else { + f.mesh.parent?.remove(f.mesh); // Group から削除 + } + } + + // 爆発完了通知 + if (alive.length === 0 && onComplete) { + onComplete(); + } + + return alive; + }); + }); + + return ( + + {fragments.map((f) => ( + + ))} + + ); +} diff --git a/src/data/planets.ts b/src/data/planets.ts index c72ea55..5093f3b 100644 --- a/src/data/planets.ts +++ b/src/data/planets.ts @@ -2,6 +2,7 @@ import * as THREE from "three"; import type { Planet } from "@/types/planet"; export const earth: Planet = { + name: "Earth", texturePath: "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", rotationSpeedY: 2, @@ -9,6 +10,19 @@ export const earth: Planet = { width: 64, height: 64, position: new THREE.Vector3(0, 0, 0), + velocity: new THREE.Vector3(0, 0, 0), +}; + +export const testPlanet: Planet = { + name: "TestPlanet", + texturePath: + "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", + rotationSpeedY: 2, + radius: 2, + width: 64, + height: 64, + position: new THREE.Vector3(100, 0, 0), + velocity: new THREE.Vector3(-10, 0, 0), }; // Easy to add more planets later: diff --git a/src/pages/Simulation/index.tsx b/src/pages/Simulation/index.tsx index 912ab62..a760b6c 100644 --- a/src/pages/Simulation/index.tsx +++ b/src/pages/Simulation/index.tsx @@ -1,13 +1,18 @@ import { OrbitControls, Stars, useTexture } from "@react-three/drei"; import { Canvas, useFrame } from "@react-three/fiber"; -import { useRef } from "react"; +import { useRef, useState } from "react"; import type * as THREE from "three"; -import { earth } from "@/data/planets"; +import { Explosion } from "@/components/Explosion"; +import { earth, testPlanet } from "@/data/planets"; +import type { ExplosionData } from "@/types/Explosion"; import type { Planet } from "@/types/planet"; +import { isColliding } from "@/utils/isColliding"; -interface PlanetMeshProps { +const testPlanets: Planet[] = [earth, testPlanet]; + +type PlanetMeshProps = { planet: Planet; -} +}; function PlanetMesh({ planet }: PlanetMeshProps) { const meshRef = useRef(null); @@ -20,6 +25,12 @@ function PlanetMesh({ planet }: PlanetMeshProps) { if (meshRef.current) { // Rotate the planet on its Y-axis meshRef.current.rotation.y += delta * planet.rotationSpeedY; + // 位置を planet.position に同期 + meshRef.current.position.set( + planet.position.x, + planet.position.y, + planet.position.z, + ); } }); @@ -36,8 +47,59 @@ function PlanetMesh({ planet }: PlanetMeshProps) { ); } +type SimulationProps = { + planets: Planet[]; + onExplosion: (newExp: ExplosionData) => void; +}; + +export function Simulation({ planets, onExplosion }: SimulationProps) { + // 前フレームの衝突ペアを記録して、連続爆発を防ぐ + const collidedPairsRef = useRef>(new Set()); + + useFrame((_state, delta) => { + // 並進運動 + for (let i = 0; i < planets.length; i++) { + if (planets[i].velocity) { + planets[i].position.addScaledVector(planets[i].velocity, delta); + } + } + + // 衝突判定 + for (let i = 0; i < planets.length; i++) { + for (let j = i + 1; j < planets.length; j++) { + const a = planets[i]; + const b = planets[j]; + const key = `${i}-${j}`; + + if (isColliding(a, b)) { + if (!collidedPairsRef.current.has(key)) { + collidedPairsRef.current.add(key); + + // 衝突したら爆発を追加 + const newExp = { + id: crypto.randomUUID(), + radius: (a.radius + b.radius) / 2, + position: a.position.clone().lerp(b.position, 0.5), + fragmentCount: 50, + }; + onExplosion(newExp); + + console.log(`Collision detected between planet ${i} and ${j}`); + } + } else { + // 衝突していない場合は記録を削除 + collidedPairsRef.current.delete(key); + } + } + } + }); + + return null; +} export default function Page() { + const [explosions, setExplosions] = useState([]); + return ( - + {testPlanets.map((planet) => ( + + ))} + + setExplosions((prev) => [...prev, newExp]) + } + /> + {explosions.map((exp) => ( + + ))} {/* Optional background and controls */}