model-wrapper.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. "use client";
  2. import { useRef, useEffect, Suspense } from "react";
  3. import { useGLTF } from "@react-three/drei";
  4. import {
  5. RigidBody,
  6. CuboidCollider,
  7. BallCollider,
  8. CapsuleCollider,
  9. } from "@react-three/rapier";
  10. import type * as THREE from "three";
  11. import { useEditorStore } from "@/lib/store";
  12. interface PhysicsProps {
  13. mass?: number | null;
  14. isStatic?: boolean | null;
  15. restitution?: number | null;
  16. friction?: number | null;
  17. colliderType?: string | null;
  18. }
  19. interface DamageProps {
  20. amount?: number | null;
  21. enabled?: boolean | null;
  22. }
  23. interface GameModelProps {
  24. position?: [number, number, number] | null;
  25. rotation?: [number, number, number] | null;
  26. scale?: [number, number, number] | null;
  27. castShadow?: boolean | null;
  28. receiveShadow?: boolean | null;
  29. url: string;
  30. physics?: PhysicsProps | null;
  31. damage?: DamageProps | null;
  32. objectId?: string | null;
  33. }
  34. function ModelInner({
  35. url,
  36. userData,
  37. }: {
  38. url: string;
  39. userData: Record<string, unknown>;
  40. }) {
  41. const groupRef = useRef<THREE.Group>(null);
  42. const { scene } = useGLTF(url);
  43. const setIsLoading = useEditorStore((s) => s.setIsLoading);
  44. const model = scene.clone();
  45. useEffect(() => {
  46. model.traverse((node) => {
  47. if ((node as THREE.Mesh).isMesh) {
  48. node.castShadow = true;
  49. node.receiveShadow = true;
  50. }
  51. });
  52. }, [model]);
  53. useEffect(() => {
  54. if (groupRef.current) {
  55. Object.assign(groupRef.current.userData, userData);
  56. }
  57. }, [userData]);
  58. useEffect(() => {
  59. const timer = setTimeout(() => setIsLoading(false), 100);
  60. return () => clearTimeout(timer);
  61. }, [setIsLoading]);
  62. return (
  63. <group ref={groupRef}>
  64. <primitive object={model} />
  65. </group>
  66. );
  67. }
  68. export function GameModel({
  69. position,
  70. rotation,
  71. scale,
  72. url,
  73. physics,
  74. damage,
  75. objectId,
  76. }: GameModelProps) {
  77. const isPlaying = useEditorStore((s) => s.isPlaying);
  78. const takeDamage = useEditorStore((s) => s.takeDamage);
  79. const setIsLoading = useEditorStore((s) => s.setIsLoading);
  80. const pos: [number, number, number] = position ?? [0, 0, 0];
  81. const rot: [number, number, number] = rotation ?? [0, 0, 0];
  82. const scl: [number, number, number] = scale ?? [1, 1, 1];
  83. const hasPhysics =
  84. isPlaying &&
  85. physics &&
  86. physics.colliderType &&
  87. physics.colliderType !== "none";
  88. const hasDamage = damage?.enabled && (damage.amount ?? 0) > 0;
  89. useEffect(() => {
  90. if (url) setIsLoading(true);
  91. }, [url, setIsLoading]);
  92. if (!url) {
  93. return (
  94. <group position={pos} rotation={rot} scale={scl}>
  95. <mesh castShadow receiveShadow>
  96. <boxGeometry args={[1, 1, 1]} />
  97. <meshStandardMaterial color="#888888" wireframe />
  98. </mesh>
  99. </group>
  100. );
  101. }
  102. const inner = (
  103. <Suspense fallback={null}>
  104. <ModelInner url={url} userData={{ id: objectId, objectId }} />
  105. </Suspense>
  106. );
  107. if (!hasPhysics) {
  108. return (
  109. <group position={pos} rotation={rot} scale={scl}>
  110. {inner}
  111. </group>
  112. );
  113. }
  114. const collider =
  115. physics!.colliderType === "ball" ? (
  116. <BallCollider args={[Math.max(scl[0], scl[1], scl[2]) * 0.5]} />
  117. ) : physics!.colliderType === "capsule" ? (
  118. <CapsuleCollider args={[scl[1] * 0.25, Math.max(scl[0], scl[2]) * 0.5]} />
  119. ) : (
  120. <CuboidCollider args={[scl[0] * 0.5, scl[1] * 0.5, scl[2] * 0.5]} />
  121. );
  122. return (
  123. <RigidBody
  124. type={physics!.isStatic ? "fixed" : "dynamic"}
  125. position={pos}
  126. rotation={rot}
  127. mass={physics!.mass ?? 1}
  128. restitution={physics!.restitution ?? 0.2}
  129. friction={physics!.friction ?? 0.5}
  130. colliders={false}
  131. onCollisionEnter={
  132. hasDamage ? () => takeDamage(damage!.amount!) : undefined
  133. }
  134. >
  135. <group scale={scl}>{inner}</group>
  136. {collider}
  137. </RigidBody>
  138. );
  139. }