model-wrapper.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- @types/three version mismatch
  47. model.traverse((node: any) => {
  48. if (node.isMesh) {
  49. node.castShadow = true;
  50. node.receiveShadow = true;
  51. }
  52. });
  53. }, [model]);
  54. useEffect(() => {
  55. if (groupRef.current) {
  56. Object.assign(groupRef.current.userData, userData);
  57. }
  58. }, [userData]);
  59. useEffect(() => {
  60. const timer = setTimeout(() => setIsLoading(false), 100);
  61. return () => clearTimeout(timer);
  62. }, [setIsLoading]);
  63. return (
  64. <group ref={groupRef}>
  65. <primitive object={model} />
  66. </group>
  67. );
  68. }
  69. export function GameModel({
  70. position,
  71. rotation,
  72. scale,
  73. url,
  74. physics,
  75. damage,
  76. objectId,
  77. }: GameModelProps) {
  78. const isPlaying = useEditorStore((s) => s.isPlaying);
  79. const takeDamage = useEditorStore((s) => s.takeDamage);
  80. const setIsLoading = useEditorStore((s) => s.setIsLoading);
  81. const pos: [number, number, number] = position ?? [0, 0, 0];
  82. const rot: [number, number, number] = rotation ?? [0, 0, 0];
  83. const scl: [number, number, number] = scale ?? [1, 1, 1];
  84. const hasPhysics =
  85. isPlaying &&
  86. physics &&
  87. physics.colliderType &&
  88. physics.colliderType !== "none";
  89. const hasDamage = damage?.enabled && (damage.amount ?? 0) > 0;
  90. useEffect(() => {
  91. if (url) setIsLoading(true);
  92. }, [url, setIsLoading]);
  93. if (!url) {
  94. return (
  95. <group position={pos} rotation={rot} scale={scl}>
  96. <mesh castShadow receiveShadow>
  97. <boxGeometry args={[1, 1, 1]} />
  98. <meshStandardMaterial color="#888888" wireframe />
  99. </mesh>
  100. </group>
  101. );
  102. }
  103. const inner = (
  104. <Suspense fallback={null}>
  105. <ModelInner url={url} userData={{ id: objectId, objectId }} />
  106. </Suspense>
  107. );
  108. if (!hasPhysics) {
  109. return (
  110. <group position={pos} rotation={rot} scale={scl}>
  111. {inner}
  112. </group>
  113. );
  114. }
  115. const collider =
  116. physics!.colliderType === "ball" ? (
  117. <BallCollider args={[Math.max(scl[0], scl[1], scl[2]) * 0.5]} />
  118. ) : physics!.colliderType === "capsule" ? (
  119. <CapsuleCollider args={[scl[1] * 0.25, Math.max(scl[0], scl[2]) * 0.5]} />
  120. ) : (
  121. <CuboidCollider args={[scl[0] * 0.5, scl[1] * 0.5, scl[2] * 0.5]} />
  122. );
  123. return (
  124. <RigidBody
  125. type={physics!.isStatic ? "fixed" : "dynamic"}
  126. position={pos}
  127. rotation={rot}
  128. mass={physics!.mass ?? 1}
  129. restitution={physics!.restitution ?? 0.2}
  130. friction={physics!.friction ?? 0.5}
  131. colliders={false}
  132. onCollisionEnter={
  133. hasDamage ? () => takeDamage(damage!.amount!) : undefined
  134. }
  135. >
  136. <group scale={scl}>{inner}</group>
  137. {collider}
  138. </RigidBody>
  139. );
  140. }