paint-brush
React と Three.js スタックを使用して独自の 3D シューターを作成する — パート 1@varlab
907 測定値
907 測定値

React と Three.js スタックを使用して独自の 3D シューターを作成する — パート 1

Ivan Zhukov17m2023/10/21
Read on Terminal Reader

長すぎる; 読むには

Web テクノロジーとインタラクティブ アプリケーションが活発に開発されている時代において、3D グラフィックスの関連性はますます高まっており、需要が高まっています。しかし、Web 開発の利点を失わずに 3D アプリケーションを作成するにはどうすればよいでしょうか?この記事では、Three.js のパワーと React の柔軟性を組み合わせて、ブラウザー内で独自のゲームを作成する方法を見ていきます。 この記事では、React Three Fiber ライブラリを紹介し、インタラクティブな 3D ゲームの作成方法を説明します。
featured image - React と Three.js スタックを使用して独自の 3D シューターを作成する — パート 1
Ivan Zhukov HackerNoon profile picture
0-item
1-item

現代の Web 開発では、クラシック アプリケーションと Web アプリケーションの境界が日に日に曖昧になってきています。現在では、インタラクティブな Web サイトだけでなく、ブラウザ上で本格的なゲームを作成することもできます。これを可能にするツールの 1 つは、 React Three Fiberライブラリです。これは、 Reactテクノロジーを使用してThree.jsに基づいて 3D グラフィックスを作成するための強力なツールです。

React Three Fiber スタックについて

React Three Fiber はReactの構造と原理を使用して Web 上に 3D グラフィックスを作成するThree.jsのラッパーです。このスタックにより、開発者はThree.jsのパワーとReactの利便性と柔軟性を組み合わせることができ、アプリケーションの作成プロセスがより直観的かつ体系的になりました。


React Three Fiberの中心となるのは、シーンで作成するものはすべてReactコンポーネントであるという考えです。これにより、開発者は使い慣れたパターンや方法論を適用できるようになります。

React Three Fiberの主な利点の 1 つは、 Reactエコシステムとの統合が容易であることです。このライブラリを使用すると、他のReactツールも簡単に統合できます。

Web-GameDev の関連性

Web-GameDev は近年大きな変化を遂げ、単純な 2D Flashゲームからデスクトップ アプリケーションに匹敵する複雑な 3D プロジェクトまで進化しました。この人気と機能の増大により、Web-GameDev は無視できない分野になっています。


Web ゲームの主な利点の 1 つは、そのアクセシビリティです。プレーヤーは追加のソフトウェアをダウンロードしてインストールする必要はなく、ブラウザでリンクをクリックするだけです。これにより、ゲームの配布とプロモーションが簡素化され、世界中の幅広いユーザーがゲームを利用できるようになります。


最後に、Web ゲーム開発は、開発者にとって使い慣れたテクノロジを使用してゲーム開発に挑戦できる優れた方法です。利用可能なツールとライブラリのおかげで、3D グラフィックスの経験がなくても、興味深い高品質のプロジェクトを作成することが可能です。

最新のブラウザでのゲームのパフォーマンス

最新のブラウザは長い道のりを経て、非常にシンプルな Web ブラウジング ツールから、複雑なアプリケーションやゲームを実行するための強力なプラットフォームまで進化しました。 ChromeFirefoxEdgeなどの主要なブラウザは、高いパフォーマンスを確保するために常に最適化および開発されており、複雑なアプリケーションを開発するための理想的なプラットフォームとなっています。


ブラウザベースのゲームの開発を促進した重要なツールの 1 つはWebGLです。この標準により、開発者はハードウェア グラフィック アクセラレーションを使用できるようになり、3D ゲームのパフォーマンスが大幅に向上しました。 WebGL は、他の WebAPI と連携して、ブラウザーで直接印象的な Web アプリケーションを作成するための新しい可能性を開きます。


それにもかかわらず、ブラウザー用のゲームを開発する場合、さまざまなパフォーマンスの側面を考慮することが重要です。リソースの最適化、メモリ管理、さまざまなデバイスへの適応はすべて、プロジェクトの成功に影響を与える重要なポイントです。

位置について!

ただし、言葉と理論は別のものですが、実際の経験はまったく別のものです。 Web ゲーム開発の可能性を最大限に理解して評価するには、開発プロセスに没頭するのが最善の方法です。そこで、Web ゲーム開発の成功例として、独自のゲームを作成してみます。このプロセスにより、開発の重要な側面を学び、実際の問題に直面してその解決策を見つけ、Web ゲーム開発プラットフォームがいかに強力で柔軟であるかを確認することができます。


一連の記事では、このライブラリの機能を使用して一人称シューティング ゲームを作成する方法を検討し、Web ゲーム開発のエキサイティングな世界に飛び込みます。


最終デモ


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


次に、プロジェクトから不要なものをすべて削除します


セクションコード

キャンバス表示のカスタマイズ

main.jsxファイルに、スコープとしてページに表示される div 要素を追加します。 Canvasコンポーネントを挿入し、カメラの視野を設定します。 Canvasコンポーネント内にAppコンポーネントを配置します。


メイン.jsx


Index.cssにスタイルを追加して、UI 要素を画面の高さ全体に引き伸ばし、スコープを画面の中央に円として表示しましょう。


インデックス.css


AppコンポーネントにSkyコンポーネントを追加します。これはゲーム シーンの背景として空の形で表示されます。


App.jsx


シーン内に空を表示する


セクションコード

床面

Groundコンポーネントを作成し、 Appコンポーネントに配置しましょう。


App.jsx


Groundで、平面要素を作成します。 Y 軸上で下に移動して、この平面がカメラの視野内に収まるようにします。また、X 軸上で平面を反転して水平にします。


Ground.jsx


マテリアルカラーとしてグレーを指定したにもかかわらず、プレーンは真っ黒に見えます。


現場でフラット


セクションコード

基本的な照明

デフォルトでは、シーンには照明がないため、オブジェクトをすべての側面から照らし、指向性ビームを持たない光源ambientLightを追加しましょう。パラメーターとしてグローの強度を設定します。


App.jsx


ライトアップされた平面


セクションコード

床面のテクスチャ

床の表面が均一にならないように、テクスチャを追加します。床面全体に沿って繰り返されるセルの形で床面のパターンを作成します。

アセットフォルダーにテクスチャ付きの PNG 画像を追加します。


追加されたテクスチャ


シーンにテクスチャをロードするには、 @react-three/dreiパッケージのuseTextureフックを使用しましょう。そして、フックのパラメータとして、ファイルにインポートされたテクスチャ画像を渡します。横軸の画像の繰り返しを設定します。


Ground.jsx


平面上のテクスチャ


セクションコード

カメラの動き

@react-three/dreiパッケージのPointerLockControlsコンポーネントを使用して、マウスを動かしたときにカーソルが動かないように、シーン上のカメラの位置を変更するようにカーソルを画面上に固定します。


App.jsx


カメラモーションのデモンストレーション


Groundコンポーネントを少し編集してみましょう。


Ground.jsx


セクションコード

物理学の追加

わかりやすくするために、単純な立方体をシーンに追加してみましょう。


 <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コンポーネントで立方体をラップする必要があります。


App.jsx


その後、ページがリロードされるたびに、立方体が重力の影響で落下することがすぐにわかります。


キューブフォール


しかし、今度は別のタスクがあります。床を、立方体が相互作用でき、それを越えて落下しないオブジェクトにする必要があります。


セクションコード

物理的なオブジェクトとしての床

Groundコンポーネントに戻り、 RigidBodyコンポーネントを床面上のラッパーとして追加しましょう。


Ground.jsx


落下しても、立方体は実際の物理的オブジェクトのように床に留まります。


飛行機に落ちてくる立方体


セクションコード

キャラクターを物理法則に従わせる

シーン上のキャラクターを制御するPlayerコンポーネントを作成しましょう。


キャラクターは追加された立方体と同じ物理オブジェクトであるため、シーン上の立方体だけでなく床面とも対話する必要があります。そのため、 RigidBodyコンポーネントを追加します。そしてキャラクターをカプセル状にしてみましょう。


Player.jsx


Playerコンポーネントを Physics コンポーネント内に配置します。


App.jsx


これで私たちのキャラクターがシーンに登場しました。


カプセル状のキャラクター


セクションコード

キャラクターの移動 - フックの作成

キャラクターはWASDキーを使用して制御され、スペースバーを使用してジャンプします。

独自の反応フックを使用して、キャラクターを移動するロジックを実装します。


hooks.jsファイルを作成し、そこに新しいusepersonControls関数を追加しましょう。


{"キーコード": "実行するアクション"} の形式でオブジェクトを定義しましょう。次に、キーボードのキーを押したり放したりするためのイベント ハンドラーを追加します。ハンドラーがトリガーされると、現在実行されているアクションが判断され、アクティブな状態が更新されます。最終結果として、フックは {"action in progress": "status"} という形式のオブジェクトを返します。


フック.js


セクションコード

キャラクターの移動 - フックの実装

usePersonControlsフックを実装した後、キャラクターを制御するときに使用する必要があります。 Playerコンポーネントでは、モーション ステート トラッキングを追加し、キャラクターの移動方向のベクトルを更新します。


移動方向の状態を保存する変数も定義します。


Player.jsx


キャラクターの位置を更新するには、 @react-three/fiberパッケージが提供するFrame を使用しましょう。このフックはrequestAnimationFrameと同様に機能し、関数の本体を 1 秒あたり約 60 回実行します。


Player.jsx


コードの説明:

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);左右の移動ベクトルを設定します。

8. 方向.subVectors(frontVector, SideVector).normalize().multiplyScalar(MOVE_SPEED);プレイヤーの動きの最終的なベクトルを計算するには、動きベクトルを減算し、結果を正規化し (ベクトルの長さが 1 になるように)、移動速度定数を掛けます。

9. playerRef.current.wakeUp();プレーヤー オブジェクトを「ウェイクアップ」して、変更に確実に反応するようにします。このメソッドを使用しない場合、しばらくするとオブジェクトは「スリープ」状態になり、位置の変更に反応しなくなります。

10. playerRef.current.setLinvel({ x: 方向.x, y: 速度.y, z: 方向.z });計算された移動方向に基づいてプレーヤーの新しい線速度を設定し、現在の垂直速度を維持します (ジャンプや落下に影響を与えないように)。


その結果、 WASDキーを押すと、キャラクターがシーン内を動き始めました。立方体は両方とも物理的なオブジェクトであるため、立方体と対話することもできます。


キャラクターの動き


セクションコード

キャラクターの移動 - ジャンプ

ジャンプを実装するには、 @dimforge/rapier3d-compatおよび@react-three/rapierパッケージの機能を使用しましょう。この例では、キャラクターが地面にいてジャンプキーが押されたことを確認してみましょう。今回はキャラクターの方向と加速力をY軸に設定します。


プレイヤーには質量を追加し、すべての軸で回転をブロックします。これにより、シーン上の他のオブジェクトと衝突したときに別の方向に倒れないようになります。


Player.jsx


コードの説明:

  1. const world = rapier.world; Rapier物理エンジン シーンへのアクセスを取得します。すべての物理オブジェクトが含まれており、それらの相互作用を管理します。
  1. const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 }));ここで「レイキャスティング」(レイキャスティング)が行われます。プレーヤーの現在位置から始まり、y 軸の下を指す光線が作成されます。この光線はシーンに「キャスト」され、シーン内のオブジェクトと交差するかどうかが判断されます。
  1. const grounded = ray && ray.collider && Math.abs(ray.toi) <= 1.5;プレーヤーが地面にいるかどうかが状態をチェックします。
  • ray -レイが作成されたかどうか。
  • ray.collider - レイがシーン上のオブジェクトと衝突したかどうか。
  • Math.abs(ray.toi) - レイの「露光時間」。この値が所定の値以下である場合、プレーヤーが「地上」とみなされるほど表面に十分近いことを示している可能性があります。


また、シーン内の他のオブジェクトと相互作用する物理オブジェクトを追加して、「着陸」ステータスを決定するためのレイトレーシング アルゴリズムが正しく機能するように、 Groundコンポーネントを変更する必要もあります。


Ground.jsx


シーンを見やすくするために、カメラを少し高く上げてみましょう。


メイン.jsx


キャラクタージャンプ


セクションコード

カメラをキャラクターの後ろに移動する

カメラを移動するには、プレーヤーの現在位置を取得し、フレームが更新されるたびにカメラの位置を変更します。また、カメラが向けられた軌道に沿ってキャラクターを正確に移動させるには、 applyEuler を追加する必要があります。


Player.jsx


コードの説明:

applyEulerメソッドは、指定されたオイラー角に基づいてベクトルに回転を適用します。この場合、カメラの回転が方向ベクトルに適用されます。これは、カメラの向きに相対的な動きを一致させるために使用され、プレーヤーはカメラが回転した方向に移動します。


Playerのサイズをわずかに調整して立方体に対して相対的に高くし、 CapsuleColliderのサイズを大きくして「ジャンプ」ロジックを修正してみましょう。


Player.jsx


カメラを移動する


セクションコード

キューブの生成

シーンが完全に空であるように感じさせないように、立方体の生成を追加しましょう。 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コンポーネントに追加しましょう。


App.jsx


キューブの生成


セクションコード

モデルをプロジェクトにインポートする

次に、シーンに 3D モデルを追加しましょう。キャラクターの武器モデルを追加しましょう。まずは 3D モデルを探しましょう。たとえば、これを見てみましょう。


モデルを GLTF 形式でダウンロードし、プロジェクトのルートでアーカイブを解凍します。

モデルをシーンにインポートする必要がある形式を取得するには、 gltf-pipelineアドオン パッケージをインストールする必要があります。


npm i -D gltf-pipeline


gltf-pipelineパッケージを使用して、モデルをGLTF 形式からGLB 形式に再変換します。これは、この形式ではすべてのモデル データが 1 つのファイルに配置されるためです。生成されたファイルの出力ディレクトリとして、パブリックフォルダーを指定します。


gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb


次に、このモデルのマークアップを含む反応コンポーネントを生成して、シーンに追加する必要があります。 @react-three/fiber開発者からの公式リソースを使用しましょう。


コンバーターに移動するには、変換されたWeapon.glbファイルをロードする必要があります。


ドラッグ アンド ドロップまたはエクスプローラー検索を使用して、このファイルを見つけてダウンロードします。


変換されたモデル


コンバーターでは、生成された反応コンポーネントが表示されます。そのコードを新しいファイルWeaponModel.jsxでプロジェクトに転送し、コンポーネントの名前をファイルと同じ名前に変更します。


セクションコード

武器モデルを現場に表示する

それでは、作成したモデルをシーンにインポートしてみましょう。 App.jsxファイルにWeaponModelコンポーネントを追加します。


App.jsx


輸入モデルのデモンストレーション


セクションコード

影を追加する

シーンのこの時点では、どのオブジェクトも影を落としていません。

シーンでシャドウを有効にするには、 Canvasコンポーネントにシャドウ属性を追加する必要があります。


メイン.jsx


次に、新しい光源を追加する必要があります。シーンには既にアンビエントライトが存在しますが、指向性ライト ビームがないため、オブジェクトの影を作成できません。そこで、 directionLightという新しい光源を追加して構成しましょう。 「キャスト」シャドウ モードを有効にする属性はCastShadowです。このパラメータの追加は、このオブジェクトが他のオブジェクトに影を落とすことができることを示します。


App.jsx


その後、別の属性acceptShadow をGroundコンポーネントに追加しましょう。これは、シーン内のコンポーネントがそれ自体にシャドウを受け取って表示できることを意味します。


Ground.jsx


モデルが影を落とす


同様の属性をシーン上の他のオブジェクト (キューブやプレーヤー) に追加する必要があります。キューブの場合は、影のキャストと受信の両方ができるため、 CastShadowacceptShadowを追加します。また、プレーヤーの場合は、 CastShadowのみを追加します。


PlayerCastShadowを追加しましょう。


Player.jsx


CubeCastShadowacceptShadow を追加します。


キューブ.jsx


シーン上のすべてのオブジェクトが影を落とす


セクションコード

影の追加 - 影のクリッピングを修正する

よく見ると、影が投影される表面積が非常に小さいことがわかります。そしてこの領域を超えると、影は単純に切り取られます。


影のトリミング


その理由は、デフォルトでカメラがdirectionLightから表示された影の小さな領域のみをキャプチャするためです。追加の属性shadow-camera-(top、bottom、left、right)を追加することで、 directionLightコンポーネントにこの可視領域を拡張することができます。これらの属性を追加すると、影がわずかにぼやけます。品質を向上させるために、 shadow-mapSize属性を追加します。


App.jsx


セクションコード

武器をキャラクターにバインドする

次に、一人称視点の武器表示を追加しましょう。新しい武器コンポーネントを作成します。これには、武器の動作ロジックと 3D モデル自体が含まれます。


 import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); }


このコンポーネントをキャラクターのRigidBodyと同じレベルに配置し、 useFrameフックで、カメラからの値の位置に基づいて位置と回転角度を設定します。


Player.jsx


一人称視点の武器モデルの表示


セクションコード

歩きながら武器を振るアニメーション

キャラクターの歩き方をより自然にするために、移動中に武器をわずかに小刻みに動かします。アニメーションを作成するには、インストールされているtween.jsライブラリを使用します。


Weaponコンポーネントはグループ タグでラップされるため、 useRefフックを介して参照を追加できます。


Player.jsx


アニメーションを保存するためにuseStateを追加しましょう。


Player.jsx


アニメーションを初期化する関数を作成しましょう。


Player.jsx


コードの説明:

  1. const twSwayingAnimation = new TWEEN.Tween(currentPosition) ...現在の位置から新しい位置までオブジェクトが「揺れる」アニメーションを作成します。
  1. const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ...最初のアニメーションが完了した後に開始位置に戻るオブジェクトのアニメーションを作成します。
  1. twSwayingAnimation.chain(twSwayingBackAnimation); 2 つのアニメーションを接続して、最初のアニメーションが完了すると、2 番目のアニメーションが自動的に開始されるようにします。


useEffectではアニメーション初期化関数を呼び出します。


Player.jsx


次に、動きが発生した瞬間を判断する必要があります。これは、キャラクターの方向の現在のベクトルを決定することによって行うことができます。


キャラクターの動きが発生した場合は、アニメーションを更新し、終了時に再度実行します。


Player.jsx


コードの説明:

  1. const isMoving = 方向.長さ() > 0;ここではオブジェクトの移動状態をチェックします。方向ベクトルの長さが 0 より大きい場合、オブジェクトには移動方向があることを意味します。
  1. if (isMoving && isSwayingAnimationFinished) { ... }このステートは、オブジェクトが移動中で、「揺れる」アニメーションが終了した場合に実行されます。


Appコンポーネントに、トゥイーン アニメーションを更新するuseFrameを追加しましょう。


App.jsx


TWEEN.update() は、 TWEEN.jsライブラリ内のすべてのアクティブなアニメーションを更新します。このメソッドは、すべてのアニメーションがスムーズに実行されるようにするために、アニメーション フレームごとに呼び出されます。


セクションコード:

反動アニメーション

ショットが発射される瞬間、つまりマウス ボタンが押された瞬間を定義する必要があります。この状態を保存するuseState 、武器オブジェクトへの参照を保存するuseRef 、およびマウス ボタンを押したり放したりするための 2 つのイベント ハンドラーを追加しましょう。


Weapon.jsx


Weapon.jsx


Weapon.jsx


マウスボタンをクリックしたときの反動アニメーションを実装してみましょう。この目的のためにtween.jsライブラリを使用します。


反動力とアニメーション期間の定数を定義しましょう。


Weapon.jsx


武器の小刻みなアニメーションと同様に、反動とホームポジションに戻るアニメーションの 2 つの useState 状態と、アニメーション終了ステータスの状態を追加します。


Weapon.jsx


反動アニメーションのランダム ベクトルを取得する関数、 generateRecoilOffsetgenerateNewPositionOfRecoilを作成しましょう。


Weapon.jsx


反動アニメーションを初期化する関数を作成します。また、 useEffectを追加します。ここでは、「ショット」状態を依存関係として指定します。これにより、各ショットでアニメーションが再度初期化され、新しい終了座標が生成されます。


Weapon.jsx


Weapon.jsx


そして、 useFrameに、マウスキーを「押したまま」にして起動するためのチェックを追加して、キーが放されるまで起動アニメーションが停止しないようにしましょう。


Weapon.jsx


反動アニメーション


セクションコード

非アクティブ時のアニメーション

キャラクターの「非アクティブ」のアニメーションを実現し、ゲームの「ハング」感をなくします。


これを行うには、 useStateを介していくつかの新しい状態を追加しましょう。


Player.jsx


状態の値を使用するように「ウィグル」アニメーションの初期化を修正しましょう。その考え方は、異なる状態 (歩行または停止) によってアニメーションに異なる値が使用され、そのたびにアニメーションが最初に初期化されるということです。


Player.jsx


アイドルアニメーション


結論

このパートでは、シーンの生成とキャラクターの移動を実装しました。また、武器モデル、発砲時およびアイドル時の反動アニメーションも追加しました。次のパートでは、新しい機能を追加してゲームを改良し続けます。


ここでも公開されています。