diff --git a/src/components/Explosion.tsx b/src/components/Explosion.tsx new file mode 100644 index 0000000..01636fd --- /dev/null +++ b/src/components/Explosion.tsx @@ -0,0 +1,102 @@ +import { useFrame } from "@react-three/fiber"; +import { useEffect, useRef, useState } from "react"; +import * as THREE from "three"; +import type { Planet } from "@/types/planet"; + +type Fragment = { + mesh: THREE.Mesh; + velocity: THREE.Vector3; + rotationAxis: THREE.Vector3; + lifetime: number; +}; + +type ExplosionProps = { + planet: Planet; + fragmentCount?: number; + onComplete?: () => void; +}; + +export const Explosion: React.FC = ({ + planet, + fragmentCount = 50, + onComplete, +}: ExplosionProps) => { + const groupRef = useRef(null); + const [fragments, setFragments] = useState([]); + + // 爆発初期化 + useEffect(() => { + const newFragments: Fragment[] = []; + for (let i = 0; i < fragmentCount; i++) { + const size = Math.random() * (planet.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(planet.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({ + mesh, + velocity, + rotationAxis, + lifetime: Math.random() * 2 + 1, // 1~3秒で消える + }); + } + setFragments(newFragments); + }, [planet, fragmentCount]); + + // フレームごとの更新 + useFrame((_, delta) => { + if (fragments.length === 0) return; + + setFragments((prev) => { + const alive: Fragment[] = []; + prev.forEach((f) => { + // 位置更新 + f.mesh.position.add(f.velocity.clone().multiplyScalar(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, i) => ( + + ))} + + ); +}; 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..b733116 100644 --- a/src/pages/Simulation/index.tsx +++ b/src/pages/Simulation/index.tsx @@ -1,9 +1,13 @@ 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 { Planet } from "@/types/planet"; +import { isColliding } from "@/utils/isColliding"; + +const testPlanets: Planet[] = [earth, testPlanet]; interface PlanetMeshProps { planet: Planet; @@ -20,6 +24,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 +46,53 @@ function PlanetMesh({ planet }: PlanetMeshProps) { ); } +interface SimulationProps { + planets: Planet[]; + setExplosions: React.Dispatch>; +} + +export function Simulation({ planets, setExplosions }: 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); + + // 衝突したら爆発を追加 + setExplosions((prev) => [...prev, a, b]); + + 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) => ( + + ))} + + {explosions.map((exp, idx) => ( + + ))} {/* Optional background and controls */}