コンテンツにスキップ

パフォーマンスのベストプラクティス

バランスの取れた視点

SOAR は、クリーンで疎結合、かつ保守しやすいアーキテクチャのために設計されています。UI のインタラクション、インベントリ管理、クエストの更新、ゲームの状態変更など、ゲーム開発のシナリオの大部分において、SOAR のパフォーマンスオーバーヘッドはごくわずかであり、アーキテクチャ上の利点は計り知れません。

このガイドは、SOAR の機能の使用を思いとどまらせることを目的としたものではありません。代わりに、フレームワークをインテリジェントに使用し、パフォーマンスが重要になる可能性のある状況を特定できるように、内部で何が起こっているかを透過的に見ることを目的としています。最適化の黄金律は常に適用されます:まずプロファイルし、次に最適化する。


最も重要な単一のルール:高頻度のイベントを避ける

経験則として、イベントやリアクティブ変数は、フレームごとに数百回または数千回実行されるロジックには使用しないでください。

Pub/Sub パターンには、Raise() 呼び出しごとに小さな固定オーバーヘッドがあります。これはボタンのクリックでは重要ではありませんが、不適切に使用すると顕著になる可能性があります。

悪いユースケース:リアルタイムのキャラクターの位置

// PlayerController.cs の Update() 内
// アンチパターン:これは避けるべきです!
void Update()
{
    // これは毎フレームイベントを発生させ、すべてのサブスクライバーに通知します。
    playerPositionVariable.Value = transform.position; 
}

このような高頻度のデータの場合、他のコンポーネントが必要なときに PlayerController または関連するデータプロバイダーへの直接参照を保持してデータをプルする方が適切です。SOAR は、継続的な状態の更新ではなく、状態の変更に使用する必要があります。


機能ごとのパフォーマンスの内訳

1. GameEventVariable

  • 何が起こるか: Raise() が呼び出されると、GameEvent はサブスクライバーの内部リストを反復処理し、それぞれを呼び出します。Variable の場合、.Value プロパティを設定すると同じことが行われます。
  • パフォーマンスコスト:
    • コストはアクティブなサブスクライバーの数に比例します。リスナーが多いほど、メソッド呼び出しが多くなります。
    • 直接の C# メソッド呼び出しと比較してわずかなオーバーヘッドがありますが、ほとんどのユースケースでは非常に高速です。
  • ベストプラクティス:
    • ValueChanged vs. ValueAssign Variable を作成すると、valueEventType はデフォルトで ValueChanged になります。これは一般的に望ましい動作です。イベントが発生する前に等価性チェック(.Equals())が実行され、値が実際に変更されていない場合は通知が防止されます。複雑な struct 型の場合、これを効率的にするためにカスタムの .Equals() 実装が必要になる場合があることに注意してください。ValueAssign を使用すると、チェックがスキップされるためわずかに高速ですが、サブスクライバーによって不要なロジックが実行される可能性があります。
    • サブスクライブ解除: サブスクリプションは常にクリーンアップする必要があります。SOAR の CompositeDisposable と R3 の AddTo(this) を使用すると簡単になりますが、これを忘れると、不必要にリソースを消費し続ける「ゾンビ」リスナーが残る可能性があります。

2. Collection (リストと辞書)

  • 何が起こるか: SoarList または SoarDictionary を変更すると、複数のイベントがトリガーされる可能性があります。たとえば、Add()OnAddOnValueChanged、および OnCountChanged をトリガーします。
  • パフォーマンスコスト: コストは、標準の C# List<T> または Dictionary<> を変更するよりも高くなります。
  • ベストプラクティス:
    • バッチ操作: 一度に多くのアイテムを追加または削除する必要がある場合は、個別の Add() 呼び出しのループでそれを行うことは避けるべきです。SOAR には単一のイベントを発生させる真の「バッチ」モードはありませんが、ロジックをグループ化すると、コードがクリーンになり、プロファイルしやすくなります。
    • 賢明な選択: データのリストがリアクティブである必要がなく、単一のシステムによってのみ使用される場合は、MonoBehaviour 内の標準の List<T> の方がパフォーマンスが高くなります。SoarList は、複数の疎結合システムがコレクションが変更されたことを明示的に通知する必要がある場合に使用する必要があります。

3. JsonableVariableautoResetValue

これは、最も重要で明白でないパフォーマンスコストが発生する領域です。

  • 何が起こるか: 複雑なクラス型の autoResetValue 機能をサポートするために、Variable<T> は、初期値を JSON 文字列にシリアル化して保存することで「ディープコピー」を実行します。ResetValue() が呼び出されると、この文字列をオブジェクトに逆シリアル化します。
  • パフォーマンスコスト:
    • GC 割り当て: JsonUtility.ToJson() は、マネージドヒープに文字列を割り当てます。これにより、ガベージコレクター(GC)が後でクリーンアップする必要のあるガベージが作成され、フレームレートの低下を引き起こす可能性があります。
    • CPU オーバーヘッド: シリアル化と逆シリアル化は、単純な値の割り当てよりも計算コストが高くなります。
    • このコストは、クラス型を使用するすべての Variable に対して初期化時(ゲームの開始時やエディターの再コンパイル時など)に支払われます。
  • ベストプラクティス:
    • 構造体とプリミティブを強く推奨: autoResetValue が必要な Variable には、可能な限りプリミティブ型(intfloatbool)または struct を使用する必要があります。これらは値でコピーされるため、JSON シリアル化パスを完全に回避できます。
    • クラスの autoResetValue を無効にする: Variable でクラス型を使用する必要がある場合(例:MyDataClassVariable)、リセット動作が不要な場合は autoResetValue を無効にするか、リセットを手動で処理する必要があります。
    • 起動コストに注意: これらの変数が数百個使用されている場合、アプリケーションの起動中に小さな遅延が目立つ場合があります。これが理由です。

4. R3 との連携

  • 何が起こるか: R3 の LINQ スタイルの演算子(WhereSelectCombineLatest など)を使用すると、小さなオブザーバーオブジェクトのチェーンが作成されます。
  • パフォーマンスコスト: R3 は高度に最適化されていますが、チェーン内のすべての演算子は、小さな割り当てと間接参照のレイヤーを追加します。非常に長く複雑なチェーンは、単純な Subscribe() よりも多くのオーバーヘッドがあります。
  • ベストプラクティス:
    • 自信を持って使用する: このオーバーヘッドはごくわずかであり、R3 の表現力は計り知れません。これを使用しない理由にはなりません。
    • 推測せずにプロファイルする: 非常に複雑なリアクティブストリームがボトルネックであると疑われる場合は、Unity プロファイラーを使用して調査する必要があります。99% の場合、パフォーマンスの問題の原因ではありません。

まとめと重要なポイント

  • ✅ SOAR は、UI、ゲームの状態、および個別のイベントに応答するロジックに使用する必要があります。
  • ❌ SOAR は、毎フレーム発生する高頻度の更新には使用しないでください。
  • 🧠 クラス型での autoResetValue 機能は注意して使用する必要があります。JSON シリアル化を使用し、ガベージを生成します。構造体が推奨されます。
  • ⚖️ Collection へのサブスクライブは、標準の C# List を変更するよりもコストがかかります。リアクティビティが必要な場合に使用する必要があります。
  • 🗑️ メモリリークや不要な作業を防ぐために、サブスクリプションは常にクリーンアップする必要があります。