주루마블 기본 알고리즘 생성중.
- 시작위치 생성 - 각 위치별 추가 이벤트시 사용될 이벤트 알고리즘 - Rapier, Three.js를 통한 주사위.
This commit is contained in:
280
app/game/dice.tsx
Normal file
280
app/game/dice.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { Canvas, useFrame, useLoader, useThree } from '@react-three/fiber';
|
||||
import {
|
||||
Physics,
|
||||
RigidBody,
|
||||
CuboidCollider,
|
||||
type RapierRigidBody,
|
||||
} from '@react-three/rapier';
|
||||
import {
|
||||
Quaternion,
|
||||
SRGBColorSpace,
|
||||
TextureLoader,
|
||||
Vector3,
|
||||
type OrthographicCamera,
|
||||
} from 'three';
|
||||
|
||||
const FLOOR_SIZE = 10;
|
||||
const FLOOR_HALF = FLOOR_SIZE / 2;
|
||||
const WALL_THICKNESS = 0.2;
|
||||
const WALL_HEIGHT = 10;
|
||||
const WALL_HALF_T = WALL_THICKNESS / 2;
|
||||
const WALL_HALF_H = WALL_HEIGHT / 2;
|
||||
|
||||
const STILL_SPEED_SQ = 0.05;
|
||||
const STILL_FRAMES = 18;
|
||||
|
||||
// boxGeometry material 순서: +X, -X, +Y, -Y, +Z, -Z
|
||||
const DICE_FACE_TEXTURES = [
|
||||
'/dice/2.png', // +X
|
||||
'/dice/5.png', // -X
|
||||
'/dice/1.png', // +Y
|
||||
'/dice/6.png', // -Y
|
||||
'/dice/3.png', // +Z
|
||||
'/dice/4.png', // -Z
|
||||
] as const;
|
||||
|
||||
// 결과 판정: 로컬 법선이 월드 +Y에 가장 가까운 면
|
||||
const DICE_FACES = [
|
||||
{ value: 1, localNormal: new Vector3(0, 1, 0) },
|
||||
{ value: 6, localNormal: new Vector3(0, -1, 0) },
|
||||
{ value: 2, localNormal: new Vector3(1, 0, 0) },
|
||||
{ value: 5, localNormal: new Vector3(-1, 0, 0) },
|
||||
{ value: 3, localNormal: new Vector3(0, 0, 1) },
|
||||
{ value: 4, localNormal: new Vector3(0, 0, -1) },
|
||||
];
|
||||
|
||||
const _quat = new Quaternion();
|
||||
const _normal = new Vector3();
|
||||
const WORLD_UP = new Vector3(0, 1, 0);
|
||||
|
||||
function readTopFace(body: RapierRigidBody): number {
|
||||
const r = body.rotation();
|
||||
_quat.set(r.x, r.y, r.z, r.w);
|
||||
|
||||
let bestValue = 1;
|
||||
let bestDot = -Infinity;
|
||||
for (const face of DICE_FACES) {
|
||||
_normal.copy(face.localNormal).applyQuaternion(_quat);
|
||||
const dot = _normal.dot(WORLD_UP);
|
||||
if (dot > bestDot) {
|
||||
bestDot = dot;
|
||||
bestValue = face.value;
|
||||
}
|
||||
}
|
||||
return bestValue;
|
||||
}
|
||||
|
||||
function FitSquareFloor() {
|
||||
const { camera, size } = useThree();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const cam = camera as OrthographicCamera;
|
||||
cam.left = -FLOOR_HALF;
|
||||
cam.right = FLOOR_HALF;
|
||||
cam.top = FLOOR_HALF;
|
||||
cam.bottom = -FLOOR_HALF;
|
||||
cam.near = 0.1;
|
||||
cam.far = 50;
|
||||
cam.updateProjectionMatrix();
|
||||
}, [camera, size.width, size.height]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function InvisibleWalls() {
|
||||
const t = WALL_HALF_T;
|
||||
const h = WALL_HALF_H;
|
||||
const f = FLOOR_HALF;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CuboidCollider args={[t, h, f]} position={[f + t, h, 0]} />
|
||||
<CuboidCollider args={[t, h, f]} position={[-(f + t), h, 0]} />
|
||||
<CuboidCollider args={[f, h, t]} position={[0, h, f + t]} />
|
||||
<CuboidCollider args={[f, h, t]} position={[0, h, -(f + t)]} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DiceMesh() {
|
||||
const textures = useLoader(TextureLoader, [...DICE_FACE_TEXTURES]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
for (const texture of textures) {
|
||||
texture.colorSpace = SRGBColorSpace;
|
||||
texture.anisotropy = 8;
|
||||
texture.needsUpdate = true;
|
||||
}
|
||||
}, [textures]);
|
||||
|
||||
return (
|
||||
<mesh castShadow>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
{textures.map((texture, index) => (
|
||||
<meshStandardMaterial
|
||||
key={texture.uuid}
|
||||
attach={`material-${index}`}
|
||||
map={texture}
|
||||
roughness={0.55}
|
||||
metalness={0.05}
|
||||
/>
|
||||
))}
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
function DiceSettleWatcher({
|
||||
diceRef,
|
||||
rolling,
|
||||
onSettled,
|
||||
}: {
|
||||
diceRef: React.RefObject<RapierRigidBody | null>;
|
||||
rolling: boolean;
|
||||
onSettled: (value: number) => void;
|
||||
}) {
|
||||
const stillFrames = useRef(0);
|
||||
const reported = useRef(false);
|
||||
|
||||
useFrame(() => {
|
||||
const body = diceRef.current;
|
||||
if (!body || !rolling) {
|
||||
stillFrames.current = 0;
|
||||
reported.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const lv = body.linvel();
|
||||
const av = body.angvel();
|
||||
const speedSq =
|
||||
lv.x * lv.x + lv.y * lv.y + lv.z * lv.z + av.x * av.x + av.y * av.y + av.z * av.z;
|
||||
|
||||
if (speedSq < STILL_SPEED_SQ || body.isSleeping()) {
|
||||
stillFrames.current += 1;
|
||||
if (stillFrames.current >= STILL_FRAMES && !reported.current) {
|
||||
reported.current = true;
|
||||
onSettled(readTopFace(body));
|
||||
}
|
||||
} else {
|
||||
stillFrames.current = 0;
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function RapierDiceExample({
|
||||
diceRef,
|
||||
rolling,
|
||||
onSettled,
|
||||
}: {
|
||||
diceRef: React.RefObject<RapierRigidBody | null>;
|
||||
rolling: boolean;
|
||||
onSettled: (value: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<Canvas
|
||||
orthographic
|
||||
camera={{ position: [0, 20, 0.01], zoom: 1 }}
|
||||
className="h-full w-full"
|
||||
gl={{ alpha: true }}
|
||||
>
|
||||
<FitSquareFloor />
|
||||
<ambientLight intensity={0.85} />
|
||||
<directionalLight position={[4, 10, 2]} intensity={0.9} />
|
||||
<Physics>
|
||||
<RigidBody type="fixed" colliders={false} position={[0, 0, 0]}>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[FLOOR_SIZE, FLOOR_SIZE]} />
|
||||
<meshBasicMaterial color="blue" />
|
||||
</mesh>
|
||||
<CuboidCollider args={[FLOOR_HALF, 0.05, FLOOR_HALF]} position={[0, -0.05, 0]} />
|
||||
<InvisibleWalls />
|
||||
</RigidBody>
|
||||
|
||||
<RigidBody
|
||||
ref={diceRef}
|
||||
type="dynamic"
|
||||
position={[0, 4, 0]}
|
||||
colliders="cuboid"
|
||||
restitution={0.2}
|
||||
friction={0.6}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<DiceMesh />
|
||||
</Suspense>
|
||||
</RigidBody>
|
||||
|
||||
<DiceSettleWatcher diceRef={diceRef} rolling={rolling} onSettled={onSettled} />
|
||||
</Physics>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
function rollDice(body: RapierRigidBody | null) {
|
||||
if (!body) return;
|
||||
|
||||
body.wakeUp();
|
||||
body.applyImpulse(
|
||||
{ x: (Math.random() - 0.5) * 8, y: 4 + Math.random() * 4, z: (Math.random() - 0.5) * 8 },
|
||||
true,
|
||||
);
|
||||
body.applyTorqueImpulse(
|
||||
{ x: (Math.random() - 0.5) * 6, y: (Math.random() - 0.5) * 6, z: (Math.random() - 0.5) * 6 },
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
export default function Dice({
|
||||
onRolled,
|
||||
disabled = false,
|
||||
}: {
|
||||
onRolled?: (value: number) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const diceRef = useRef<RapierRigidBody>(null);
|
||||
const [rolling, setRolling] = useState(false);
|
||||
const [result, setResult] = useState<number | null>(null);
|
||||
|
||||
const onSettled = useCallback(
|
||||
(value: number) => {
|
||||
setResult(value);
|
||||
setRolling(false);
|
||||
onRolled?.(value);
|
||||
},
|
||||
[onRolled],
|
||||
);
|
||||
|
||||
const busy = rolling || disabled;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full" style={{ containerType: 'size' }}>
|
||||
<div className="absolute top-0 left-0 aspect-square w-[min(100%,100cqh)]">
|
||||
<RapierDiceExample diceRef={diceRef} rolling={rolling} onSettled={onSettled} />
|
||||
<button
|
||||
type="button"
|
||||
className="absolute bottom-2 left-2 z-10 rounded bg-black/70 px-3 py-1 text-sm text-white disabled:opacity-50"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
setResult(null);
|
||||
setRolling(true);
|
||||
rollDice(diceRef.current);
|
||||
}}
|
||||
>
|
||||
Roll
|
||||
</button>
|
||||
<div className="pointer-events-none absolute right-2 bottom-2 z-10 rounded bg-black/70 px-3 py-1 text-sm text-white">
|
||||
{rolling
|
||||
? '굴리는 중…'
|
||||
: disabled
|
||||
? '이동 중…'
|
||||
: result != null
|
||||
? `결과: ${result}`
|
||||
: '결과: —'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user