Development Guide
このページでは、サンプルアプリの全体の構成から個別の機能の開発方針までを説明します。
ディレクトリ構造
プロジェクト全体のディレクトリやコンポーネントの構成は以下のようになります。
- コンポーネントは、components の下に作成し、basics(単体部品)と pages(画面を表す)で構成します。
- 共通フックは、hooks の下に作成します。個別のコンポーネント 専用のフックはコンポーネントと同じ場所に作成します。
├─components ... コンポーネントを格納する
│ ├─basics ... 再利用可能な基本となるコンポーネント群
│ │ ├─Avatar ... アバター
│ │ ├─AvatarSelect ... アバター選択
│ │ ├─IconMenu ... アイコンメニュー
│ │ ├─ImageSphere ... 360度画像の球体表示
│ │ ├─Multiplay ... マルチプレイ用コンポーネント
│ │ ├─Player ... プレイヤー(自分)
│ │ ├─RemotePlayer ... リモートプレイヤー
│ │ ├─RemotePlayerGroup ... リモートプレイヤーグループ
│ │ ├─ThirdPersonCamera ... 3人称カメラ
│ │ ├─VideoSphere ... 360度動画再生コンポーネント
│ │ ├─VirtualJoyStick ... 仮想JoyStick
│ │ └─VoiceChatPanel ... ボイスチャット用コンポーネント
│ └─pages ... ページ単位(basicの組み合わせ)
│ ├─AvatarSelect ... アバター選択
│ ├─ErrorPage ... エラーページ
│ ├─InSpot ... スポット訪問画面
│ └─SpotSelect ... スポット選択画面
├─generated ... バックエンドのAPIの自動生成コード
├─hooks ... カスタムReactフック
└─libs ... ユーティリティ群
状態管理
コンポーネントの状態を管理するのに、通常の React の Context と State 以外に、Zustand という状態管理ライブラリを使って複数のコンポーネントにまたがる状態を管理しています。 Zustand では、状態と状態変更関数をセットで管理でき、内部の状態をカプセル化することができます。Zustand の使い方の詳細は、ここを参照ください。
次のように使い分けています。
- コンポーネント内の制御に使う状態(ミュート状態など):State
- 機能をまたがる状態(アバター選択状態など):Zustand
- バックエンドのデータ状態(スポット情報など):TanStack Query
- React の Context は使いません。代わりに Zustand を使っています
ステレオタイプと命名規則
ステレオタイプとは、ファイルの典型的な種類を表します。
- コンポーネント:画面表示する部品を指します。
- フック:React のカスタムフック関数を指します。
- ストア:コンポーネント間を超えて管理する Zustand の状態を指します。
ファイル名のステレオタイプごとの命名規則は次の表となります。
ステレオタイプ | ファイル名の命名規則 | 例 |
---|---|---|
コンポーネント | 大文字から始まるキャメルケース+.tsx | SpotCard.tsx |
フック | use +大文字から始まるキャメルケース+.ts | usePlayerInput.ts |
ストア | use +大文字から始まるキャメルケース+Store.ts | useAvatarSelectStore.ts |
CSS | CSS modules を使うため、*.module.css となります | SpotCard.module.css |
コンポーネントの責務配置
コンポーネントは、以下の 3 種類に分類されます。
- 通常の React で描画する 2 次元の HTML 要素になるコンポーネント
- R3F を使って描画する 3 次元のコンポーネント
- 3 次元と 2 次元の親子構成を持つ複合コンポーネント
1 は通常の React のコンポーネントと同じですので普通の React と同じように開発します。
2 は、R3F の<Canvas>要素の下にコンポーネントを配置します。
通常では、 2 の下に 1 は配置できませんが、react-three/drei の<Html>
コンポーネントを使って混在させることができます。それが、3 です。
通常の 2 次元コンポーネントのシンプルな例は、このページの「SpotSelect とバックエンド API」で紹介しています。
R3F を使って描画する 3 次元のコンポーネントのシンプルな例は、このページの「ImageSphere による 360 度画像の表示」で紹介しています。
コンポーネントは極力 1 と 2 が分かれるように構成されるべきですが、R3F 要素と HTML 要素の混在が必要な部分では 3 を使います。
画面遷移
このサンプルアプリケーションでは、ルーティングのライブラリは利用していません。
アバターのプロフィール入力状態やスポットの選択状態に応じてコンポーネントを出し分けて対応しています。
規模の大きなアプリケーションを作る場合は、React Router や App Router などの利用も検討してください。
機能の解説
このセクションでは各機能の実装について解説します。
Avatar の表示とアニメーション表示
<Avatar>
コンポーネントは、アバターの表示とアニメーションの変更を行います。
const playerInput = usePlayerInput();
const defaultAnimationMap: { [name: string]: string } = {
Idle: "./motions/Happy_Idle.fbx",
Walking: "./motions/Walking.fbx",
Running: "./motions/Running.fbx",
Jump: "./motions/Jumping.fbx",
};
return (
<Avatar
avatarPath={"./avatars/amy.vrm"}
animationMap={defaultAnimationMap}
controller={playerInput.movement}
/>
);
props として以下を渡すことでアバターとア ニメーション表示、キー操作を行えます。 アバターは VRM ファイル、モーションは FBX ファイルを使っています。
props | 型 | 説明 |
---|---|---|
avatarPath | string | アバターの VRM ファイルへのパス |
animationMap | { [name: string]: string } | モーション名と FBX ファイルへのパスのマップ |
controller | CharacterController | キーの操作や仮想 JoyStick の操作を表すオブジェクト |
Spot Select とバックエンド API
スポット選択画面では、バックエンドの API を呼び出してその結果をスポット情報の一覧として表示しています。
下記コードの 2 行目のuseSpots()
がバックエンドの API を呼び出しています。
useSpots()
は、Tanstack Query を使っており、主要な戻り値は、以下の 3 つです。
isLoading
... バックエンド API を呼び出しており、ロード中か否か。呼び出し終了後は false になります。data
... バックエンド API から取得された正常レスポンス。正常終了した場合のみ取得できます。error
... バックエンド API から取得されたエラーレスポンス。エラー発生した場合のみ取得できます。
const SpotSelectPanel = () => {
const { isLoading, data, error } = useSpots();
if (isLoading) {
return <Spin fullscreen />;
}
if (error) {
return (
<>
Failed to get spot info.
<pre>{error?.message || "Unknow reason."}</pre>
</>
);
}
if (data) {
const spots = data.data;
return (
<>
<div className={styles.content}>
{spots.map((spot) => (
<SpotCard key={spot.id} spot={spot} />
))}
</div>
</>
);
}
return <>No spots found</>;
};
useSpots()
の実装は以下の通りで、Ovalを利用した自動生成コードuseGetSpots()
を呼び出しているだけです。
import { useGetSpots } from "@/generated/spots/spots";
const useSpots = () => {
return useGetSpots();
};
export default useSpots;
Orval を使った自動生成の詳細については、Fintan の記事「React Query と OpenAPI 定義ファイルからのコード自動生成ツール Orval を使った Web アプリケーション開発」を参考にすると良いでしょう。
Image Sphere による 360 度画像の表示
<ImageSphere>
コンポーネントは、360 度画像を球体に表示します。
<ImageSphere imageSourceUrl={<360度画像へのURL>} />
<ImageSphere>
の実装では、useTexture
で画像の URL からテクスチャをロードし、<meshBasicMaterial>
にマップしています。
const ImageSphere: React.FC<ImageSphereProps> = (props: ImageSphereProps) => {
const imageTexture = useTexture(props.imageSourceUrl);
return (
<>
{imageTexture && (
<mesh scale={[100, 100, 100]}>
<sphereGeometry />
<meshBasicMaterial side={THREE.BackSide} map={imageTexture} />
</mesh>
)}
</>
);
};
Video Sphere & Hidden Video による 360 度動画の表示
360 度動画の表示は、ビデオを非表示で再生する<HiddenVideo>
コンポーネントと、360 度球体で表示する<VideoSphere>
から構成されます。
構成が分かれている理由 は、球体にビデオを表示するためには、技術的にビデオの再生中のテクスチャを球体にマップする必要があるためです。<HiddenVideo>
では html 要素の<video>
タグを利用して見えない動画を再生し、<VideoSphere>
ではそのテクスチャを球体にマップしています。
利用する際は、次のように実装します。
<>
<Canvas> <!-- R3F のキャンバス -->
<VideoSphere
videoId="video"
radius={100}
/>
</Canvas>
<div>
<HiddenVideo
videoSourceUrl="<360度動画へのURL>"
videoId="video"
/>
</div>
</>