This document is about: QUANTUM 3
SWITCH TO

新機能紹介

Quantum SDK 3.1では、Quantum 3のパフォーマンスとスケーラビリティが向上し、物理エンジンがより安定するようになりました。

新しい物理ソルバー

Quantumの物理エンジンはステートフルになり、ウォームスタートを備えたProjected Gauss-Seidelソルバーが搭載されました。(従来の動作を維持したい場合、従来のソルバーを選択することも可能です)

この新ソルバーは時間的コヒーレンスを活用していて、従来のソルバーと比較して主に2つの点が改善されています:

  • 優れたスタック処理:複数のフレームに渡って適切な解に収束させることで、物体のスタックや複数のジョイント(例:ラグドール)など、複雑な制約のシステムを処理することが可能になりました。
  • オーバーシュートの軽減:反復処理が相互に関連するようになり、中間のオーバーシュートを補正できるため、より正確な解が得られるようになりました。

新しいソルバーによる物体のスタック:

従来のソルバーによる物体のスタック:

3DラグドールとCharacterJoint

Quantum 3.1で新しく追加されたCharacterJoint3D型は、ラグドールやボールアンドソケット制約の作成に役立ちます。物体の周囲で回転できるアンカーを取り付けられるようになり、必要に応じてツイスト角やスイング角を制限できます。これはPhysicsJoint3Dコンポーネントプロトタイプを通して設定できます。

これをベースにした新しいUnityコンポーネントQuantumRagdollは、簡単にラグドールが作成可能で、シミュレーションステートに基づいてビュー要素を更新するウィザードツールです:

  • 自動ベイク:Animatorコンポーネントの参照から、手足・頭・胸・腰をベイクします。
  • ギズモハンドル:シーンビュー上で直接ラグドール設定を編集できます。
  • 非対称ラグドール:個々の手足・頭・胸・腰のベイク済みプロパティを上書きできます。
  • PhysicsMaterial:すべての四肢のコライダーに一括でPhysicsMaterialを割り当てます。

ラグドールのテンプレートは、ヒエラルキー上の右クリックメニューQuantum > 3D > Ragdoll Entityから作成できます。

新しい物理ソルバーと、改良された2D/3Dジョイント位置ソルバーを組み合わせることで、ラグドールのような複雑なジョイントシステムでも、以前よりはるかに安定して動作するようになります。

Entity ViewのExponential Decay補間モード

QuantumEntityViewは、InterpolationModeドロップダウンから選択可能な3つ目の補間モードとして、PredictionとSnapshotInterpolationに加え、ExponentialDecayをサポートするようになりました。

これは軽量かつフレームレートに依存しないスムージング処理です。シミュレーションのスナップショット間で補間を行うのではなく、ゲームオブジェクトのtransform(position + rotation)を目標に向けて指数関数的にイージングさせます。これは非常に低負荷で、視覚的にもスムーズに動作する上に、transformが外部から書き込まれる場合にも安定します。

Entity View Transform History

ビューの補間処理において、更新ごとに必要だった完全なフレームコピーが2つ分不要になりました。そのかわりとなるQuantumEntityViewTransformHistoryは、必要なエンティティビューのtransformのみを、ティック毎の小さなバッファに記録します。これはQuantumEntityViewUpdater.TransformHistoryからアクセス可能で、すべての補間モードで動作します。

その結果、QuantumGame.Frames.PredictedPrevious/PreviousUpdatePredictedは、デフォルトでは個別のフレームコピーではなくVerifiedフレームを指すようになります。これらがまだ必要なプロジェクトでは、QuantumGameFlags.IsLegacyFrameCopiesEnabled(またはSessionRunner.Arguments.IsLegacyFrameCopiesEnabled)を設定することで従来のコピーを復元できます。

DisableInterpolatableStatesは非推奨となり、IsLegacyFrameCopiesEnabledに置き換えられました。後者は意味が逆で、デフォルトでは無効になっています。

Table Components(テーブルコンポーネント)

QuantumのECSはこれまで、コンポーネントの値の保存にスパースセットのみを使用していて、各コンポーネント型には、エンティティと値を紐づけるスパースセット(辞書の一種)が関連付けられていました。

スパースセットのコンポーネントは、追加/削除が高速ですが、異なる順序で格納されている異なるエンティティに対して、複数を一度に反復処理を行うと速度が低下してしまいます。フィルターは通常、最初のコンポーネントの値のみをシーケンシャルアクセス可能で、それ以外はランダムなルックアップを必要とします。

Quantum 3.1では「table(テーブル)」と呼ばれる新しいコンポーネントストレージが追加されました。テーブルはすべてのエンティティのコンポーネントの配列を保存し、そこに特定のテーブルコンポーネントの組み合わせが含まれています。テーブル内の各エンティティは、各「列(配列)」の「行(インデックス)」になります。エンティティのテーブルコンポーネントを変更する際は、異なるテーブルへ移動させる必要があります。

テーブルコンポーネントでは、利点と欠点が逆転します。反復処理が高速な分、追加/削除が遅くなります。

デフォルトでは、コンポーネントは従来のスパースセットを使用します。テーブルを使用するには、DSLのコンポーネント宣言にtableキーワードを単純に追加するだけです。

table component Health {
    FP Value;
}

同じエンティティ内で、テーブルとスパースセットは自由に組み合わせて使用できるため、両方の長所を最大限に活用できます。APIは従来と変わらないので、コードの変更は不要です。フィルターのエンティティ反復処理中にテーブルを追加/削除しても安全で、処理が遅延することもありません。

組み込みコンポーネント(Transform・Collider・PhysicsBodyなど)は、デフォルトではスパースセットストレージを引き続き使用します。組み込みコンポーネントを含め、任意のコンポーネントのストレージモードも、DSLを変更することなく、SimulationConfig.Entities.ComponentTypeConfigurationにエントリーを追加し、そのStorageフィールドをSparseSet/Tableに設定することで上書きできます。

コンポーネントフィルターのオプション

フィルターでは、一部のコンポーネントをオプションとして指定できるようになりました。オプションのコンポーネントが存在しないエンティティはスキップされませんが、NextUnsafe(またはフィルター構造体)はコンポーネントのポインタに対してnullを出力します。

C#

var filter = frame.Filter<Transform3D, PhysicsBody3D>(optional: ComponentSet.Create<PhysicsBody3D>());
while (filter.NextUnsafe(out var entity, out var transform, out var body)) {
  if (body != null) { }
}

複合エンティティプロトタイプ

複合エンティティプロトタイプによって、ライフサイクルを共有するエンティティプロトタイプの構成を定義できます。これは、ルートのエンティティプロトタイプ内にエンティティプロトタイプを入れ子にすることで実現できます。入れ子のエンティティはすべてルートに依存し、ライフサイクルを共有します。つまり、ルートが作成/破棄されると、入れ子のエンティティもすべて作成/破棄されます。

密接に結合したエンティティを、構造化された階層として整理するのに非常に便利です。

詳細はこちらをご覧ください。

複合プロトタイプ
複合プロトタイプ

複合コマンドの統合

コマンドの送信と処理を行うAPIが変更され、CompoundCommandを使用した余計な管理を必要とせずに、1フレーム内に複数のコマンドを送信できるようになりました。

コマンドの送信:

C#

// 旧方式
QuantumRunner.Default.Game.SendCommand(new MyCommand());
QuantumRunner.Default.Game.SendCommand(playerSlot: 0, command: new MyCommand());

// 新方式
QuantumRunner.Default.Game.AddCommand(new MyCommand());
QuantumRunner.Default.Game.AddCommand(playerSlot: 0, command: new MyCommand());

コマンドの処理:

C#

public override void Update(Frame frame) {
  for (int player = 0; player < frame.MaxPlayerCount; player++) {

    // 旧方式(1フレーム内で1つのコマンドを処理する)
    {
      var commandA = frame.GetPlayerCommand(player) as MyCommand;
      if (commandA != null) { }
      // または
      var commandB = frame.GetPlayerCommand<MyCommand>(player);
      // または
      if (frame.TryGetPlayerCommand<MyCommand>(player, out var commandC)) { }
    }

    // 新方式(1フレーム内で複数のコマンドを処理する)
    {
      // 新しいイテレーターの使用
      foreach (var command in frame.GetPlayerCommands<MyCommand>(player)) { }
      
      // または、forループを使用してコマンドを処理する
      for (int i = 0; i < frame.GetPlayerCommandCount(player); i++) 
        var commandA = frame.GetPlayerCommand(player, i) as  MyCommand;
        if (commandA != null) { }
        // または
        if (frame.TryGetPlayerCommand<MyCommand>(player, i, out var commandB)) { }
      }
    }
  }
}

必要に応じて、QuantumGame.SendBufferedCommands()を呼び出すことで、キューに貯めたすべてのコマンドを一度に送信できます。

Quantum Server SDKのコマンド処理も変更されました。APIが調整・簡素化されて、コマンドのシリアライズに必要なボイラープレートコードが削減されました。

デシリアライズ済みコマンドを保持する新しいコールバックが導入されました。OnDeterministicCommand(DeterministicPluginClient client, Command commandMessage)は従来通り動作しますが、新しいコールバックへの移行を推奨します。

戻り値のenum OnDeterministicCommandResultを使用することで、インターセプトしたコマンドをAccept・Discard・Modifiedでマークできます。変更されたコマンドは再シリアライズされます。

C#

public override OnDeterministicCommandResult OnDeterministicCommand(DeterministicPluginClient client, DeterministicCommand command) {
  if (command is MyCommand myCmd) {
    myCmd.Foo = 42;
  }
  return OnDeterministicCommandResult.Modified;
}

SendDeterministicCommand()に追加された新しいオーバーロードで、具体的なDeterministicCommandオブジェクトを送信できるようになりました。

ヒープとメモリストレージの改善

Quantum 3.1では、フレームのヒープにいくつかの改良が加わっています。

  • 大型ブロック:ブロックはページサイズに制限されなくなりました。補助メモリのチャンク全体(「セグメント」と呼ばれる)を、一つのブロックとして割り当てることも可能です。
  • 遅延拡張:フレームのヒープが既存のメモリセグメントを使い切った場合は、新しいメモリ割り当てが行われます。

新種のフレームのヒープを導入し、同一サイズのブロックではなく可変サイズのブロックを保持できるようにすることで、ヒープの断片化を軽減できます。このヒープを使用するには、SimulationConfigで「block-based」ヒープ管理を選択してください。

スパースセットのコンポーネントのブロックも、遅延割り当てが行われるスパースバッファチャンクで再実装されました。これによって、エンティティとコンポーネントが大量にあるケースにおいて、フレームのCopyFrom呼び出しにかかる時間が大幅に削減されます。

シミュレーターのコールバック

新しいコールバックによって、シミュレーターの内部処理の一部がSimulatorContextを通して公開されます。これによって、シミュレーションフローに関する情報の取得や制御が可能になります。 これらは、シミュレーション処理を厳密に制御する必要があるプロジェクトで便利です。

  • シミュレーターの統計:確定/予測フレーム数・目標ティック・現在のステージ・ストール状態・ロールバックが発生するか/したか(WillRollbackPrediction/HasRolledBackPrediction)などを取得できます。
  • 予測ロールバックのスキップ:SkipPredictionRollbackIfPossibleを設定して、予測ロールバックを選択的にスキップすることで、予測精度を犠牲にしてパフォーマンスを優先できます。予測ステージは引き続き実行され、前回の予測ステートから処理が再開します。これによって「ロールバックや再シミュレーションを行わずに、確定フレームを複数回進める」「進められる確定フレームがない場合に、ロールバックと完全な再シミュレーションによって予測ミスを修正する」場合を柔軟に切り替えることが可能になります。
  • 予測のスキップ:SkipPredictionを設定すると、更新時のロールバックと予測ステージを両方スキップし、前回更新時の予測ステートを維持します。
  • シミュレーション数の制限:SimulatorContextのTicksToVerify/DeterministicSession.MaxVerifiedTicksPerUpdateを設定すると、各更新でシミュレートされる確定ティック数を制限できます。これは上記の設定と組み合わせて使用することで、実践的なケースにおいても可能な限りスムーズなパフォーマンスを実現できます。

スキップはリクエストであり、保証ではありません。ロールバックは最終的に実行される必要があり、確定ティックが、以前の予測ティックより先に進んだ場合(IsRollbackRequired)、ロールバックは常に実行されます。どちらのスキップフラグも、遅くともBeforePredictionステージまでに設定する必要があります。

新しいコールバックは、シミュレーションの確定/予測ステージの前後で呼び出されます。 さらに、SimulationFinishedなどの既存のコールバックもSimulatorContextを持つようになりました。

コールバック 説明
CallbackBeforeSimulationStage 各シミュレーションステージ(確定/予測)の開始前に一度呼び出されます。
CallbackSimulationStageFinished 各シミュレーションステージ(確定/予測)の終了時に一度呼び出されます。
CallbackSimulateFinished フレームのシミュレーション(確定/予測)が完了した際に呼び出されます。

SDKにはQuantumRunnerRollbackPolicyによって実装されたデフォルトのシミュレーション/ロールバックのペースのルールセットが付属しています。これはStartUIプレハブに既に含まれていますが、スクリプトをUnityシーンに追加することもできます。

CodeGenの改善

Quantum DSLのコード生成にはいくつかの改善が加えられ、大規模なプロジェクトや再利用可能なライブラリへのサポートが手厚くなりました。

モジュール:.qtnファイルに#pragma module <name>を追加すると、出力(コンポーネント・プロトタイプ・イベントなど)が、共有コアファイルではなく、モジュール個別の.csファイルで生成されます。これによって、生成されたコードが扱いにくくなった場合、コードを分割することができます。

#pragma module Asteroids

component AsteroidsShip {
    FP Score;
}

アクセス修飾子:DSLで宣言する型やフィールドに、アクセス修飾子を指定できるようになりました。デフォルトはpublicですが、型にはinternalも指定可能で、フィールドにはinternal/privateも指定可能です。

internal component Health {
    FP Value;
    private FP RegenTimer;
}

ファイル単位の設定:.qtnファイルのインポーターでUseCustomSettingsを有効にすると、ファイル個別のGeneratorOptionsと出力フォルダーが使用されます。これによって、DSLの一部を独立したライブラリやアドオンとして分離できます。詳細と制限事項についてはQuantumQtnAssetImporterのドキュメントをご覧ください。

非constのイベントID:生成されるイベントIDはconst値ではなく静的プロパティになったため、イベントの追加/削除によってコードが連鎖的に変更されることが無くなりました。以前の動作に戻すには、Project Settings > Photon Quantum > Code Gen内のLegacyCodeGenOptionsにConstEventIdsを追加してください。

コンポーネントのインポート:インポートされたコンポーネントは、他のコンポーネントや構造体のフィールドとして使用されない限り、サイズを指定する必要が無くなったため、import component Foo;が有効になりました。

エージェントフレンドリーなドキュメント

SDKには、AIコーディングエージェント向けに最適化されたQuantumドキュメントのオフライン版が含まれるようになりました。以下の手順は、Quantum Hubから利用可能です:

  • エージェントフレンドリーなドキュメント:QuantumDocumentation.zipをAssets/Photon/Quantum/Documentation~に展開すると、エージェントはllms.txtを通してオフラインでマニュアルを参照できるようになります。Unityは~付きのフォルダーを無視するため、インポートは何も行われません。SDKアップグレード後は、ページを更新するためこの手順を再実行してください。
  • AGENTS.mdスニペットのコピー:AGENTS.md/CLAUDE.mdが存在するリポジトリのルートを選択します。Quantumエージェントガイド(Assets/Photon/Quantum/AGENTS.md)へのリンクを含むスニペットが、クリップボードにコピーされ、エージェントファイルにペーストできるようになります。

Last updated on

Back to top