April 1, 2025

Three - 玩家控制 demo


import { useKeyboardControls } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { RigidBody } from "@react-three/rapier";
import { useEffect, useRef, useState } from "react";
import { useRapier } from "@react-three/rapier";
import * as THREE from "three";
import { useGame } from "./stores/useGame";

const groundLevel = 0.32;

export const Player = () => {
  const start = useGame((state) => state.start);
  const restart = useGame((state) => state.restart);
  const end = useGame((state) => state.end);
  const blocksCount = useGame((state) => state.blocksCount);

  const [subscribeKeys, getKeys] = useKeyboardControls();
  // const { rapier, world } = useRapier();

  const bodyRef = useRef();

  const smoothedCameraPosition = new THREE.Vector3(10, 10, 10);
  const smoothedCameraTarget = new THREE.Vector3();

  const jump = () => {
    // 偷雞, 只有球體在 y 軸 0.32以下 才可以二次跳躍
    if (bodyRef.current.translation().y <= groundLevel) {
      bodyRef.current.applyImpulse({ x: 0, y: 0.5, z: 0 });
    }
    
    // 下方為正式作法
    // const origin = bodyRef.current.translation();
    // origin.y -= 0.31;
    // const direction = { x: 0, y: -1, z: 0 };
    // const ray = new rapier.Ray(origin, direction);

    // const hit = world.castRay(ray, 10, true);
    // if (hit.timeOfImpact < 0.15)
    //   bodyRef.current.applyImpulse({ x: 0, y: 0.5, z: 0 });
  };

  const reset = () => {
    bodyRef.current.setTranslation({ x: 0, y: 1, z: 0 });
    bodyRef.current.setLinvel({ x: 0, y: 0, z: 0 });
    bodyRef.current.setAngvel({ x: 0, y: 0, z: 0 });
  };

  useFrame((state, delta) => {
    if (!bodyRef.current) return;
    const { forward, backward, leftward, rightward } = getKeys();

    const impulse = { x: 0, y: 0, z: 0 };
    const torque = { x: 0, y: 0, z: 0 };

    const impulseStrength = 0.4 * delta;
    const torqueStrength = 0.2 * delta;

    if (forward) {
      impulse.z -= impulseStrength;
      torque.x -= torqueStrength;
    }

    if (rightward) {
      impulse.x += impulseStrength;
      torque.z -= torqueStrength;
    }

    if (backward) {
      impulse.z += impulseStrength;
      torque.x += torqueStrength;
    }

    if (leftward) {
      impulse.x -= impulseStrength;
      torque.z += torqueStrength;
    }

    bodyRef.current?.applyImpulse(impulse);
    bodyRef.current?.applyTorqueImpulse(torque);

    // camera
    const bodyPosition = bodyRef.current?.translation();

    let cameraPosition = new THREE.Vector3();
    const { x, y, z } = bodyPosition;
    cameraPosition = { x, y: y + 0.65, z: z + 2.25 };

    /** 設定相機目標點位於球體上方 */
    const cameraTarget = new THREE.Vector3();
    cameraTarget.copy(bodyPosition);
    cameraTarget.y += 0.25;

    // 從 smoothedCameraPosition 平滑到 cameraPosition 
    smoothedCameraPosition.lerp(cameraPosition, 5 * delta);
    smoothedCameraTarget.lerp(cameraTarget, 5 * delta);

    state.camera.position.copy(smoothedCameraPosition);
    state.camera.lookAt(smoothedCameraTarget);
  });

  // 如果球的位置低于-1,则将其重置到(0, 1, 0)
  useFrame((state, delta) => {
    if (!bodyRef.current) return;
    const position = bodyRef.current.translation();
    if (position.y < -2) {
      restart();
      // // 重置球的位置
      // bodyRef.current.setTranslation({ x: 0, y: 1, z: 0 }, true);

      // // 重置線性速度,避免繼續掉落
      // bodyRef.current.setLinvel({ x: 0, y: 0, z: 0 }, true);

      // // 重置角速度,避免旋轉異常
      // bodyRef.current.setAngvel({ x: 0, y: 0, z: 0 }, true);
    }

    if (position.z < -(blocksCount * 4 + 2)) {
      end();
    }
  });

  useEffect(() => {
    const unsubscribeReset = useGame.subscribe(
      (state) => state.phase,
      (value) => {
        if (value === "ready") reset();
      }
    );

    const unsubscribeJump = subscribeKeys(
      (state) => state.jump,
      (value) => {
        if (value) jump();
      }
    );

    const unsubscribeAny = subscribeKeys(() => {
      start();
    });
    // ...

    return () => {
      unsubscribeReset();
      unsubscribeJump();
      unsubscribeAny();
    };
  }, []);

  return (
    <>
      <RigidBody
        ref={bodyRef}
        position={[0, 1, 0]}
        colliders="ball"
        restitution={0.2}
        friction={1}
        canSleep={false}
        linearDamping={0.5}
        angularDamping={0.5}
      >
        <mesh castShadow>
          <icosahedronGeometry args={[0.3, 1]} />
          <meshStandardMaterial flatShading color="mediumpurple" />
        </mesh>
      </RigidBody>
    </>
  );
};