現代の Web 開発では、クラシック アプリケーションと Web アプリケーションの境界が日に日に曖昧になってきています。現在では、インタラクティブな Web サイトだけでなく、ブラウザ上で本格的なゲームを作成することもできます。これを可能にするツールの 1 つは、 ライブラリです。これは、 テクノロジーを使用して に基づいて 3D グラフィックスを作成するための強力なツールです。 React Three Fiber React Three.js React Three Fiber スタックについて 、 の構造と原理を使用して Web 上に 3D グラフィックスを作成する のラッパーです。このスタックにより、開発者は のパワーと の利便性と柔軟性を組み合わせることができ、アプリケーションの作成プロセスがより直観的かつ体系的になりました。 React Three Fiber は React Three.js Three.js React の中心となるのは、シーンで作成するものはすべて コンポーネントであるという考えです。これにより、開発者は使い慣れたパターンや方法論を適用できるようになります。 React Three Fiber React の主な利点の 1 つは、 エコシステムとの統合が容易であることです。このライブラリを使用すると、他の ツールも簡単に統合できます。 React Three Fiber React React Web-GameDev の関連性 近年大きな変化を遂げ、単純な 2D ゲームからデスクトップ アプリケーションに匹敵する複雑な 3D プロジェクトまで進化しました。この人気と機能の増大により、Web-GameDev は無視できない分野になっています。 Web-GameDev は Flash Web ゲームの主な利点の 1 つは、そのアクセシビリティです。プレーヤーは追加のソフトウェアをダウンロードしてインストールする必要はなく、ブラウザでリンクをクリックするだけです。これにより、ゲームの配布とプロモーションが簡素化され、世界中の幅広いユーザーがゲームを利用できるようになります。 最後に、Web ゲーム開発は、開発者にとって使い慣れたテクノロジを使用してゲーム開発に挑戦できる優れた方法です。利用可能なツールとライブラリのおかげで、3D グラフィックスの経験がなくても、興味深い高品質のプロジェクトを作成することが可能です。 最新のブラウザでのゲームのパフォーマンス 最新のブラウザは長い道のりを経て、非常にシンプルな Web ブラウジング ツールから、複雑なアプリケーションやゲームを実行するための強力なプラットフォームまで進化しました。 、 、 の主要なブラウザは、高いパフォーマンスを確保するために常に最適化および開発されており、複雑なアプリケーションを開発するための理想的なプラットフォームとなっています。 Chrome Firefox Edge など ブラウザベースのゲームの開発を促進した重要なツールの 1 つは です。この標準により、開発者はハードウェア グラフィック アクセラレーションを使用できるようになり、3D ゲームのパフォーマンスが大幅に向上しました。 、他の WebAPI と連携して、ブラウザーで直接印象的な Web アプリケーションを作成するための新しい可能性を開きます。 WebGL WebGL は それにもかかわらず、ブラウザー用のゲームを開発する場合、さまざまなパフォーマンスの側面を考慮することが重要です。リソースの最適化、メモリ管理、さまざまなデバイスへの適応はすべて、プロジェクトの成功に影響を与える重要なポイントです。 位置について! ただし、言葉と理論は別のものですが、実際の経験はまったく別のものです。 Web ゲーム開発の可能性を最大限に理解して評価するには、開発プロセスに没頭するのが最善の方法です。そこで、Web ゲーム開発の成功例として、独自のゲームを作成してみます。このプロセスにより、開発の重要な側面を学び、実際の問題に直面してその解決策を見つけ、Web ゲーム開発プラットフォームがいかに強力で柔軟であるかを確認することができます。 一連の記事では、このライブラリの機能を使用して一人称シューティング ゲームを作成する方法を検討し、Web ゲーム開発のエキサイティングな世界に飛び込みます。 最終デモ https://codesandbox.io/p/github/JI0PATA/fps-game?embedable=true 上のリポジトリ GitHub さあ、始めましょう! プロジェクトのセットアップとパッケージのインストール まず、 プロジェクト テンプレートが必要です。それでは、インストールしてみましょう。 React npm create vite@latest ライブラリを選択します。 React を選択します。 [JavaScript] 追加の npm パッケージをインストールします。 npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js 次に、プロジェクトから不要なものをすべて 。 削除します セクションコード キャンバス表示のカスタマイズ ファイルに、スコープとしてページに表示される div 要素を追加します。 コンポーネントを挿入し、カメラの視野を設定します。 コンポーネント内に コンポーネントを配置します。 main.jsx Canvas Canvas App にスタイルを追加して、UI 要素を画面の高さ全体に引き伸ばし、スコープを画面の中央に円として表示しましょう。 Index.css コンポーネントに コンポーネントを追加します。これはゲーム シーンの背景として空の形で表示されます。 App Sky セクションコード 床面 コンポーネントを作成し、 コンポーネントに配置しましょう。 Ground App で、平面要素を作成します。 Y 軸上で下に移動して、この平面がカメラの視野内に収まるようにします。また、X 軸上で平面を反転して水平にします。 Ground マテリアルカラーとしてグレーを指定したにもかかわらず、プレーンは真っ黒に見えます。 セクションコード 基本的な照明 デフォルトでは、シーンには照明がないため、オブジェクトをすべての側面から照らし、指向性ビームを持たない光源 を追加しましょう。パラメーターとしてグローの強度を設定します。 ambientLight セクションコード 床面のテクスチャ 床の表面が均一にならないように、テクスチャを追加します。床面全体に沿って繰り返されるセルの形で床面のパターンを作成します。 フォルダーにテクスチャ付きの PNG 画像を追加します。 アセット シーンにテクスチャをロードするには、 パッケージの フックを使用しましょう。そして、フックのパラメータとして、ファイルにインポートされたテクスチャ画像を渡します。横軸の画像の繰り返しを設定します。 @react-three/drei useTexture セクションコード カメラの動き パッケージの コンポーネントを使用して、マウスを動かしたときにカーソルが動かないように、シーン上のカメラの位置を変更するようにカーソルを画面上に固定します。 @react-three/drei PointerLockControls コンポーネントを少し編集してみましょう。 Ground セクションコード 物理学の追加 わかりやすくするために、単純な立方体をシーンに追加してみましょう。 <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> 今、彼はただ宇宙にぶら下がっているだけだ。 パッケージの コンポーネントを使用して、シーンに「物理学」を追加します。パラメータとして、軸に沿った重力を設定する重力フィールドを構成します。 @react-three/rapier Physics <Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics> ただし、立方体は物理コンポーネント内にありますが、何も起こりません。立方体を実際の物理オブジェクトのように動作させるには、 パッケージの コンポーネントで立方体をラップする必要があります。 @react-three/rapier RigidBody その後、ページがリロードされるたびに、立方体が重力の影響で落下することがすぐにわかります。 しかし、今度は別のタスクがあります。床を、立方体が相互作用でき、それを越えて落下しないオブジェクトにする必要があります。 セクションコード 物理的なオブジェクトとしての床 コンポーネントに戻り、 コンポーネントを床面上のラッパーとして追加しましょう。 Ground RigidBody 落下しても、立方体は実際の物理的オブジェクトのように床に留まります。 セクションコード キャラクターを物理法則に従わせる シーン上のキャラクターを制御する コンポーネントを作成しましょう。 Player キャラクターは追加された立方体と同じ物理オブジェクトであるため、シーン上の立方体だけでなく床面とも対話する必要があります。そのため、 コンポーネントを追加します。そしてキャラクターをカプセル状にしてみましょう。 RigidBody コンポーネントを Physics コンポーネント内に配置します。 Player これで私たちのキャラクターがシーンに登場しました。 セクションコード キャラクターの移動 - フックの作成 キャラクターは キーを使用して制御され、 を使用してジャンプします。 WASD スペースバー 独自の反応フックを使用して、キャラクターを移動するロジックを実装します。 ファイルを作成し、そこに新しい 関数を追加しましょう。 hooks.js usepersonControls {"キーコード": "実行するアクション"} の形式でオブジェクトを定義しましょう。次に、キーボードのキーを押したり放したりするためのイベント ハンドラーを追加します。ハンドラーがトリガーされると、現在実行されているアクションが判断され、アクティブな状態が更新されます。最終結果として、フックは {"action in progress": "status"} という形式のオブジェクトを返します。 セクションコード キャラクターの移動 - フックの実装 フックを実装した後、キャラクターを制御するときに使用する必要があります。 コンポーネントでは、モーション ステート トラッキングを追加し、キャラクターの移動方向のベクトルを更新します。 usePersonControls Player 移動方向の状態を保存する変数も定義します。 キャラクターの位置を更新するには、 パッケージが提供する しましょう。このフックは と同様に機能し、関数の本体を 1 秒あたり約 60 回実行します。 @react-three/fiber Frame を使用 requestAnimationFrame コードの説明: プレーヤーオブジェクトのリンクを作成します。このリンクにより、シーン上のプレーヤー オブジェクトとの直接対話が可能になります。 1. const playerRef = useRef(); フックが使用されると、プレーヤーが現在どのコントロール ボタンを押しているかを示すブール値を持つオブジェクトが返されます。 2. const {前方、後方、左方、右方、ジャンプ} = usePersonControls(); フックはアニメーションの各フレームで呼び出されます。このフック内で、プレーヤーの位置と線速度が更新されます。 3. useFrame((状態) => { ... }); プレイヤーオブジェクトの存在を確認します。プレーヤー オブジェクトがない場合、関数はエラーを避けるために実行を停止します。 4. if (!playerRef.current) return; プレーヤーの現在の線速度を取得します。 5. const 速度 = playerRef.current.linvel(); 押されたボタンに基づいて、前方/後方の動きベクトルを設定します。 6.frontVector.set(0, 0, 後方 - 前方); 左右の移動ベクトルを設定します。 7.sideVector.set(左 - 右, 0, 0); プレイヤーの動きの最終的なベクトルを計算するには、動きベクトルを減算し、結果を正規化し (ベクトルの長さが 1 になるように)、移動速度定数を掛けます。 8. 方向.subVectors(frontVector, SideVector).normalize().multiplyScalar(MOVE_SPEED); プレーヤー オブジェクトを「ウェイクアップ」して、変更に確実に反応するようにします。このメソッドを使用しない場合、しばらくするとオブジェクトは「スリープ」状態になり、位置の変更に反応しなくなります。 9. playerRef.current.wakeUp(); 計算された移動方向に基づいてプレーヤーの新しい線速度を設定し、現在の垂直速度を維持します (ジャンプや落下に影響を与えないように)。 10. playerRef.current.setLinvel({ x: 方向.x, y: 速度.y, z: 方向.z }); その結果、 キーを押すと、キャラクターがシーン内を動き始めました。立方体は両方とも物理的なオブジェクトであるため、立方体と対話することもできます。 WASD セクションコード キャラクターの移動 - ジャンプ ジャンプを実装するには、 および パッケージの機能を使用しましょう。この例では、キャラクターが地面にいてジャンプキーが押されたことを確認してみましょう。今回はキャラクターの方向と加速力をY軸に設定します。 @dimforge/rapier3d-compat @react-three/rapier には質量を追加し、すべての軸で回転をブロックします。これにより、シーン上の他のオブジェクトと衝突したときに別の方向に倒れないようになります。 プレイヤー コードの説明: 物理エンジン シーンへのアクセスを取得します。すべての物理オブジェクトが含まれており、それらの相互作用を管理します。 const world = rapier.world; Rapier ここで「レイキャスティング」(レイキャスティング)が行われます。プレーヤーの現在位置から始まり、y 軸の下を指す光線が作成されます。この光線はシーンに「キャスト」され、シーン内のオブジェクトと交差するかどうかが判断されます。 const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); プレーヤーが地面にいるかどうかが状態をチェックします。 const grounded = ray && ray.collider && Math.abs(ray.toi) <= 1.5; - 作成されたかどうか。 ray レイが - レイがシーン上のオブジェクトと衝突したかどうか。 ray.collider - レイの「露光時間」。この値が所定の値以下である場合、プレーヤーが「地上」とみなされるほど表面に十分近いことを示している可能性があります。 Math.abs(ray.toi) また、シーン内の他のオブジェクトと相互作用する物理オブジェクトを追加して、「着陸」ステータスを決定するためのレイトレーシング アルゴリズムが正しく機能するように、 コンポーネントを変更する必要もあります。 Ground シーンを見やすくするために、カメラを少し高く上げてみましょう。 セクションコード 最初のコミット 2 番目のコミット カメラをキャラクターの後ろに移動する カメラを移動するには、プレーヤーの現在位置を取得し、フレームが更新されるたびにカメラの位置を変更します。また、カメラが向けられた軌道に沿ってキャラクターを正確に移動させるには、 追加する必要があります。 applyEuler を コードの説明: メソッドは、指定されたオイラー角に基づいてベクトルに回転を適用します。この場合、カメラの回転が ベクトルに適用されます。これは、カメラの向きに相対的な動きを一致させるために使用され、プレーヤーはカメラが回転した方向に移動します。 applyEuler 方向 のサイズをわずかに調整して立方体に対して相対的に高くし、 のサイズを大きくして「ジャンプ」ロジックを修正してみましょう。 Player CapsuleCollider セクションコード 最初のコミット 2 番目のコミット キューブの生成 シーンが完全に空であるように感じさせないように、立方体の生成を追加しましょう。 json ファイルに各立方体の座標をリストし、シーン上に表示します。これを行うには、ファイル を作成し、その中に座標の配列をリストします。 cubes.json [ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ] ファイルで、ループ内でキューブを生成する コンポーネントを作成します。そして コンポーネントは直接生成されたオブジェクトとなります。 Cube.jsx Cubes Cube import {RigidBody} from "@react-three/rapier"; import cubes from "./cubes.json"; export const Cubes = () => { return cubes.map((coords, index) => <Cube key={index} position={coords} />); } const Cube = (props) => { return ( <RigidBody {...props}> <mesh castShadow receiveShadow> <meshStandardMaterial color="white" /> <boxGeometry /> </mesh> </RigidBody> ); } 先ほどの単一のキューブを削除して、作成した コンポーネントを コンポーネントに追加しましょう。 Cubes App セクションコード モデルをプロジェクトにインポートする 次に、シーンに 3D モデルを追加しましょう。キャラクターの武器モデルを追加しましょう。まずは 3D モデルを探しましょう。たとえば、 見てみましょう。 これを モデルを GLTF 形式でダウンロードし、プロジェクトのルートでアーカイブを解凍します。 モデルをシーンにインポートする必要がある形式を取得するには、 アドオン パッケージをインストールする必要があります。 gltf-pipeline npm i -D gltf-pipeline パッケージを使用して、モデルを から に再変換します。これは、この形式ではすべてのモデル データが 1 つのファイルに配置されるためです。生成されたファイルの出力ディレクトリとして、 フォルダーを指定します。 gltf-pipeline GLTF 形式 GLB 形式 パブリック gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb 次に、このモデルのマークアップを含む反応コンポーネントを生成して、シーンに追加する必要があります。 開発者からの を使用しましょう。 @react-three/fiber 公式リソース コンバーターに移動するには、変換された ファイルをロードする必要があります。 Weapon.glb ドラッグ アンド ドロップまたはエクスプローラー検索を使用して、このファイルを見つけてダウンロードします。 コンバーターでは、生成された反応コンポーネントが表示されます。そのコードを新しいファイル でプロジェクトに転送し、コンポーネントの名前をファイルと同じ名前に変更します。 WeaponModel.jsx セクションコード 武器モデルを現場に表示する それでは、作成したモデルをシーンにインポートしてみましょう。 ファイルに コンポーネントを追加します。 App.jsx WeaponModel セクションコード 影を追加する シーンのこの時点では、どのオブジェクトも影を落としていません。 シーンで 有効にするには、 コンポーネントに 属性を追加する必要があります。 シャドウを Canvas シャドウ 次に、新しい光源を追加する必要があります。シーンには既に が存在しますが、指向性ライト ビームがないため、オブジェクトの影を作成できません。そこで、 という新しい光源を追加して構成しましょう。 「 」シャドウ モードを有効にする属性は です。このパラメータの追加は、このオブジェクトが他のオブジェクトに影を落とすことができることを示します。 アンビエントライト directionLight キャスト CastShadow その後、別の属性 コンポーネントに追加しましょう。これは、シーン内のコンポーネントがそれ自体にシャドウを受け取って表示できることを意味します。 acceptShadow を Ground 同様の属性をシーン上の他のオブジェクト (キューブやプレーヤー) に追加する必要があります。キューブの場合は、影のキャストと受信の両方ができるため、 と を追加します。また、プレーヤーの場合は、 のみを追加します。 CastShadow acceptShadow CastShadow に を追加しましょう。 Player CastShadow に と 追加します。 Cube CastShadow acceptShadow を セクションコード 影の追加 - 影のクリッピングを修正する よく見ると、影が投影される表面積が非常に小さいことがわかります。そしてこの領域を超えると、影は単純に切り取られます。 その理由は、デフォルトでカメラが から表示された影の小さな領域のみをキャプチャするためです。追加の属性 追加することで、 コンポーネントにこの可視領域を拡張することができます。これらの属性を追加すると、影がわずかにぼやけます。品質を向上させるために、 属性を追加します。 directionLight shadow-camera-(top、bottom、left、right)を directionLight shadow-mapSize セクションコード 武器をキャラクターにバインドする 次に、一人称視点の武器表示を追加しましょう。新しい コンポーネントを作成します。これには、武器の動作ロジックと 3D モデル自体が含まれます。 武器 import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); } このコンポーネントをキャラクターの と同じレベルに配置し、 フックで、カメラからの値の位置に基づいて位置と回転角度を設定します。 RigidBody useFrame セクションコード 歩きながら武器を振るアニメーション キャラクターの歩き方をより自然にするために、移動中に武器をわずかに小刻みに動かします。アニメーションを作成するには、インストールされている ライブラリを使用します。 tween.js コンポーネントはグループ タグでラップされるため、 フックを介して参照を追加できます。 Weapon useRef アニメーションを保存するために を追加しましょう。 useState アニメーションを初期化する関数を作成しましょう。 コードの説明: 現在の位置から新しい位置までオブジェクトが「揺れる」アニメーションを作成します。 const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... 最初のアニメーションが完了した後に開始位置に戻るオブジェクトのアニメーションを作成します。 const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... 2 つのアニメーションを接続して、最初のアニメーションが完了すると、2 番目のアニメーションが自動的に開始されるようにします。 twSwayingAnimation.chain(twSwayingBackAnimation); ではアニメーション初期化関数を呼び出します。 useEffect 次に、動きが発生した瞬間を判断する必要があります。これは、キャラクターの方向の現在のベクトルを決定することによって行うことができます。 キャラクターの動きが発生した場合は、アニメーションを更新し、終了時に再度実行します。 コードの説明: ここではオブジェクトの移動状態をチェックします。方向ベクトルの長さが 0 より大きい場合、オブジェクトには移動方向があることを意味します。 const isMoving = 方向.長さ() > 0; このステートは、オブジェクトが移動中で、「揺れる」アニメーションが終了した場合に実行されます。 if (isMoving && isSwayingAnimationFinished) { ... } コンポーネントに、トゥイーン アニメーションを更新する を追加しましょう。 App useFrame ライブラリ内のすべてのアクティブなアニメーションを更新します。このメソッドは、すべてのアニメーションがスムーズに実行されるようにするために、アニメーション フレームごとに呼び出されます。 TWEEN.update() は、 TWEEN.js セクションコード: 最初のコミット 2 番目のコミット 反動アニメーション ショットが発射される瞬間、つまりマウス ボタンが押された瞬間を定義する必要があります。この状態を保存する 、武器オブジェクトへの参照を保存する 、およびマウス ボタンを押したり放したりするための 2 つのイベント ハンドラーを追加しましょう。 useState useRef マウスボタンをクリックしたときの反動アニメーションを実装してみましょう。この目的のために ライブラリを使用します。 tween.js 反動力とアニメーション期間の定数を定義しましょう。 武器の小刻みなアニメーションと同様に、反動とホームポジションに戻るアニメーションの 2 つの useState 状態と、アニメーション終了ステータスの状態を追加します。 反動アニメーションのランダム ベクトルを取得する関数、 と を作成しましょう。 generateRecoilOffset generateNewPositionOfRecoil 反動アニメーションを初期化する関数を作成します。また、 を追加します。ここでは、「ショット」状態を依存関係として指定します。これにより、各ショットでアニメーションが再度初期化され、新しい終了座標が生成されます。 useEffect そして、 に、マウスキーを「押したまま」にして起動するためのチェックを追加して、キーが放されるまで起動アニメーションが停止しないようにしましょう。 useFrame セクションコード 非アクティブ時のアニメーション キャラクターの「非アクティブ」のアニメーションを実現し、ゲームの「ハング」感をなくします。 これを行うには、 を介していくつかの新しい状態を追加しましょう。 useState 状態の値を使用するように「ウィグル」アニメーションの初期化を修正しましょう。その考え方は、異なる状態 (歩行または停止) によってアニメーションに異なる値が使用され、そのたびにアニメーションが最初に初期化されるということです。 結論 このパートでは、シーンの生成とキャラクターの移動を実装しました。また、武器モデル、発砲時およびアイドル時の反動アニメーションも追加しました。次のパートでは、新しい機能を追加してゲームを改良し続けます。 でも公開されています。 ここ