logo
HOMEABOUTBLOGCONTACT
© 2025 Haruyasu Kaitori All Rights Reserved.
プレイヤー操作チュートリアル
2025/04/19
thumbnail
React Three Fiber

プレイヤーをキーボードで動かすチュートリアルです。

ステップ1: プロジェクトの準備

まずは React アプリを作成し、必要なライブラリをインストールします。

# プロジェクト作成(JavaScript版)
yarn create vite . --template react
cd my-3d-game

# 必要パッケージを追加
yarn add three @react-three/fiber @react-three/drei @react-three/rapier three-stdlib
  • three:Three.js 本体
  • @react-three/fiber:React から Three.js を扱うためのレンダラー
  • @react-three/drei:よく使うヘルパーやコントロールをまとめたユーティリティ集
  • @react-three/rapier:物理演算エンジン Rapier の React バインディング
  • three-stdlib:モデル複製など Three.js 標準外のユーティリティ

ステップ2: シーンと環境の構築

index.cssを修正します

index.css
#root {
  width: 100vw;
  height: 100vh;
}

body {
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

React コンポーネント App を編集して、Canvas とカメラ、ライト、空(Sky)を配置します。

src/App.jsx
import { Canvas } from "@react-three/fiber"
import { Sky } from "@react-three/drei"

function App() {
  return (
    <Canvas shadows camera={{ position: [0, 5, 5] }}>
      {/* 空を追加 */}
      <Sky
        distance={10000}
        sunPosition={[0, 10, 5]}
        inclination={0}
        azimuth={0.25}
        rayleigh={0.1}
      />

      {/* 環境光と影付き方向光 */}
      <ambientLight intensity={1.5} />
      <directionalLight
        castShadow
        position={[0, 5, 0]}
        intensity={2}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      >
        {/* シャドウカメラを調整 */}
        <orthographicCamera
          attach="shadow-camera"
          left={-25} right={25}
          top={25} bottom={-25}
          near={0.1} far={100}
        />
      </directionalLight>
    </Canvas>
  )
}

export default App
  • <Canvas> の shadows でシャドウを有効化
  • <Sky> で大空/太陽光を簡単に設定
  • <ambientLight> と <directionalLight> でシーン全体を照らします

ステップ3: 物理エンジンと地面・壁の追加

Rapier を使って地面とステージ外周の壁(Boundary)を作ります。

src/Stage.jsx
import { RigidBody, CuboidCollider } from "@react-three/rapier"

const wallConfigs = [
  { pos: [0, 2.5, 25.5], size: [50, 5, 1] }, // 北
  { pos: [0, 2.5, -25.5], size: [50, 5, 1] }, // 南
  { pos: [25.5, 2.5, 0], size: [1, 5, 50] }, // 東
  { pos: [-25.5, 2.5, 0], size: [1, 5, 50] }, // 西
]

export function BoundaryWalls() {
  return (
    <>
      {wallConfigs.map(({ pos, size }, i) => (
        <RigidBody key={i} type="fixed" colliders="cuboid">
          <mesh position={pos} receiveShadow>
            <boxGeometry args={size} />
            <meshStandardMaterial visible={false} />
          </mesh>
        </RigidBody>
      ))}
    </>
  )
}

export function Ground() {
  return (
    <RigidBody type="fixed" colliders={false} name="ground">
      <mesh rotation-x={-Math.PI / 2} receiveShadow>
        <planeGeometry args={[50, 50]} />
        <meshStandardMaterial color="gray" />
      </mesh>
      <CuboidCollider args={[25, 0.1, 25]} position={[0, -0.1, 0]} />
    </RigidBody>
  )
}
  • RigidBody type="fixed":動かないオブジェクトに設定
  • CuboidCollider:物理計算用の衝突判定形状
  • 壁は可視化せずぶつかるだけに(visible={false})

これらを <Physics> の中に含めることで、物理演算が働くステージが完成します。


ステップ4: キャラクターモデルの読み込みとアニメーション

GLTF 形式のキャラクター(Shepard.glb)を読み込み、idle/run アニメーションを制御します。

キャラクターのモデルはpoly.pizzaからダウンロードしています。

こちらをダウンロードして、public/models/Shepard.glbに保存してください。

https://poly.pizza/m/Hssa6NPc6W

src/Shepard.jsx
import { useGLTF, useAnimations } from "@react-three/drei"
import { SkeletonUtils } from "three-stdlib"
import { useEffect, useMemo, useRef } from "react"

export function Shepard({ animation }) {
  const group = useRef()
  const { scene, animations } = useGLTF("/models/Shepard.glb")
  const clone = useMemo(() => SkeletonUtils.clone(scene), [scene])
  const { actions, mixer } = useAnimations(animations, group)

  useEffect(() => {
    const action = actions[animation]
    action?.reset().fadeIn(0.24).play()
    mixer.timeScale = 1.4
    return () => action?.fadeOut(0.24)
  }, [animation, actions, mixer])

  useEffect(() => {
    clone.traverse((child) => {
      if (child.isMesh) child.castShadow = true
    })
  }, [clone])

  return <primitive ref={group} object={clone} />
}
useGLTF.preload("/models/Shepard.glb")
  • useGLTF:モデルとアニメーションを一度にロード
  • SkeletonUtils.clone:複数配置したいときに必要
  • useAnimations:GLTF 内のアニメーションを再生・制御

ステップ5: 入力操作とプレイヤー制御・カメラ追従

KeyboardControls を使ってキー入力を取得し、Rapier の RigidBody に速度を適用します。さらにカメラも追従させます。

src/PlayerController.jsx
import { useKeyboardControls } from "@react-three/drei"
import { RigidBody, CapsuleCollider } from "@react-three/rapier"
import { useFrame } from "@react-three/fiber"
import { useRef, useState } from "react"
import * as THREE from "three"
import { Shepard } from "./Shepard"
import { Controls } from "./App"

const MOVEMENT_SPEED = 7

const lerpAngle = (start, end, t) => {
  let s = THREE.MathUtils.euclideanModulo(start, Math.PI * 2)
  let e = THREE.MathUtils.euclideanModulo(end, Math.PI * 2)
  if (Math.abs(e - s) > Math.PI) {
    if (e > s) s += 2 * Math.PI
    else e += 2 * Math.PI
  }
  return THREE.MathUtils.euclideanModulo(s + (e - s) * t, Math.PI * 2)
}

const PlayerController = () => {
  const bodyRef = useRef()
  const charRef = useRef()
  const cameraTarget = useRef()
  const cameraPosRef = useRef()

  const [animation, setAnimation] = useState("Idle")
  const [, getControls] = useKeyboardControls()
  const movementVector = useRef(new THREE.Vector3()).current
  const worldCamPos = useRef(new THREE.Vector3())
  const worldCamLook = useRef(new THREE.Vector3())
  const camLookAt = useRef(new THREE.Vector3())

  useFrame(({ camera }) => {
    if (!bodyRef.current) return

    // 入力取得と移動ベクトル設定 (前後キーを修正)
    const keys = getControls()
    movementVector.set(
      (keys[Controls.left] ? 1 : 0) + (keys[Controls.right] ? -1 : 0),
      0,
      (keys[Controls.forward] ? 1 : 0) + (keys[Controls.back] ? -1 : 0)
    )

    // 移動速度と向き
    if (movementVector.lengthSq() > 0) {
      movementVector.normalize().multiplyScalar(MOVEMENT_SPEED)
      const angle = Math.atan2(movementVector.x, movementVector.z)
      charRef.current.rotation.y = lerpAngle(
        charRef.current.rotation.y,
        angle,
        0.2
      )
      setAnimation("Run")
    } else {
      setAnimation("Idle")
    }

    // 速度を物理エンジンに反映
    const vel = bodyRef.current.linvel()
    bodyRef.current.setLinvel(
      { x: movementVector.x, y: vel.y, z: movementVector.z },
      true
    )

    // カメラ追従処理
    cameraPosRef.current.getWorldPosition(worldCamPos.current)
    camera.position.lerp(worldCamPos.current, 0.1)
    cameraTarget.current.getWorldPosition(worldCamLook.current)
    camLookAt.current.lerp(worldCamLook.current, 0.1)
    camera.lookAt(camLookAt.current)
  })

  return (
    <RigidBody
      colliders={false}
      lockRotations
      ref={bodyRef}
      position={[0, 0.1, 0]}
      name="Player"
    >
      <group ref={cameraTarget} />
      <group ref={cameraPosRef} position={[0, 1.5, -3.5]} />
      <group ref={charRef}>
        <Shepard animation={animation} />
      </group>
      <CapsuleCollider args={[0.2, 0.2]} position={[0, 0.4, 0]} />
    </RigidBody>
  )
}

export default PlayerController
  • KeyboardControls:上下左右(WASD/矢印キー)を簡単に管理
  • linvel/setLinvel:Rapier の線形速度を読み書き
  • lerpAngle:回転を自然に補間

まとめ:最終的な組み立て

上記コンポーネントをすべて揃えたら、App.js を以下のように構成します。

src/App.jsx
import { Canvas } from "@react-three/fiber"
import { Preload, KeyboardControls, Sky } from "@react-three/drei"
import { Physics } from "@react-three/rapier"
import { useMemo } from "react"
import { Ground, BoundaryWalls } from "./Stage"
import PlayerController from "./PlayerController"

export const Controls = {
  forward: "forward",
  back: "back",
  left: "left",
  right: "right",
}

const App = () => {
  const keyMap = useMemo(
    () => [
      { name: Controls.forward, keys: ["ArrowUp", "KeyW"] },
      { name: Controls.back, keys: ["ArrowDown", "KeyS"] },
      { name: Controls.left, keys: ["ArrowLeft", "KeyA"] },
      { name: Controls.right, keys: ["ArrowRight", "KeyD"] },
    ],
    []
  )

  return (
    <KeyboardControls map={keyMap}>
      <Canvas camera={{ position: [0, 1.5, -3.5] }} shadows>
        <Sky
          distance={10000}
          sunPosition={[0, 10, 5]}
          inclination={0}
          azimuth={0.25}
          rayleigh={0.1}
        />
        <ambientLight intensity={1.5} />
        <directionalLight
          castShadow
          position={[0, 5, 0]}
          intensity={2}
          shadow-mapSize-width={2048}
          shadow-mapSize-height={2048}
        >
          <orthographicCamera
            attach="shadow-camera"
            left={-25}
            right={25}
            top={25}
            bottom={-25}
            near={0.1}
            far={100}
          />
        </directionalLight>
        <Preload all />
        <Physics>
          <Ground />
          <PlayerController />
          <BoundaryWalls />
        </Physics>
      </Canvas>
    </KeyboardControls>
  )
}

export default App

これで、

  1. ゲーム画面にキャラクターと地面・壁が表示され
  2. WASD/矢印キーで前後左右に動き
  3. 自然なアニメーションとカメラ追従が働く

という「プレイヤーを動かす」チュートリアルの完成です!

各ステップのコードを順に組み合わせながら動作を確かめてみてください。

関連記事
関連記事がありません
カテゴリ
Loading...
アーカイブ
Loading...
オススメ
Three.js Journey
Wawasensei