エンティティビュー
はじめに
QuantumEntityViewは、QuantumのViewコンポーネントからエンティティにリンクされます。これはゲームオブジェクトへのAssetRefを持ち、ゲームオブジェクトは特定のエンティティのビューを表現するためにインスタンス化されます。エンティティプロトタイプのプレハブか、UnityでQuantumEntityViewを持つシーンオブジェクトを設定すると、QuantumのViewコンポーネントが自動的にプロトタイプに追加されます。
 
QuantumEntityViewUpdaterは、Unity内のすべてのエンティティのビューを処理する役割を担っています。シミュレーションのデータに基づいて、エンティティに関連するビューのゲームオブジェクトの作成・更新・破棄を行います。QuantumEntityViewUpdaterスクリプトは、QuantumMapを含むすべてのシーンに追加する必要があります。
QuantumEntityViewComponentを使用して、各EntityViewにビュー関連の機能を追加できます。詳細はエンティティビューコンポーネントをご覧ください。
プーリング
デフォルトでQuantumEntityViewUpdaterは、エンティティが作成されるたびにQuantumEntityViewプレハブから新しいインスタンスを作成し、破棄も同様にビューのゲームオブジェクトを破棄します。
エンティティビューのプーリングを有効にするには、QuantumEntityViewUpdaterゲームオブジェクトにQuantumEntityViewPoolスクリプトを追加してください。これはQuantumEntityViewUpdaterとシームレスに連携し、IQuantumEntityViewPoolインターフェースを使用した独自実装で置き換えることができます。
| HidePooledObjectsInHierarchy | 有効にすると、プールされたオブジェクトに HideFlags.HideInHierarchyが設定されます | 
| ResetGameObjectScale | 有効にすると、オブジェクトのローカルスケールが1にリセットされます | 
| Precache Items | オブジェクトは Awake()でインスタンス化され、プール内で利用可能になります | 
Quantumのコールバック/イベントを購読しているエンティティビューは、次のいずれかを行う必要があります。
- ゲームオブジェクトがプールに戻される前に購読解除する
- 以下のようにonlyIfActivateAndEnabledパラメーターを有効にする
C#
QuantumEvent.Subscribe<EventPlayerKilled>(this, OnKilled, onlyIfActiveAndEnabled: true);
Bind Behaviour
UnityのQuantumEntityViewスクリプトにあるBind Behaviourプロパティは、次のいずれかに設定できます。
- Non Verified:ビューのゲームオブジェクトを予測フレームで作成できる
- Verified:ビューのゲームオブジェクトは確定フレームのみで作成できる
Non Verifiedは、エンティティビューが高頻度でインスタンス化される場合や、ゲームプレイメカニクスの反応時間の関係から、できるだけ素早くプレイヤーの画面に表示する必要がある場合に通常使用されます。例えば、ハイペースなシューティングゲームの弾の作成には、この選択肢を使用すべきです。
一方Verifiedは、エンティティビューを即座に表示する必要がなく、確認フレームを待つ分のわずかな遅延を許容できる場合に役立ちます。これは、予測ミスによるビューオブジェクトの作成/破棄を避けたい場合にも便利です。プレイヤーキャラクターのエンティティの作成時に使用するのが良い例になるでしょう。
Manual Disposal
Manual Disposalプロパティが有効の場合、QuantumEntityViewUpdaterの破棄メソッドがスキップされます。これによって、QuantumEntityViewのOnEntityDestroyedコールバックを使用して手動で破棄したり、カスタム破棄イベントから破棄したりすることができます。
ビューカリング(Quantum 3.0.3以降)
QuantumEntityViewUpdaterゲームオブジェクトに追加したIQuantumEntityViewCullingインターフェースを使用して、どのQuantumエンティティをUnityビューに同期させるかを制御ように拡張できます。これは、動的エンティティとマップエンティティのイテレーターを上書きします。
以下は球チェックを使用した基本的なカリングアルゴリズムのサンプル実装で、オンラインモード時のNon Verifiedビューの予測カリングも再利用しています。
C#
namespace Quantum {
  using System.Collections.Generic;
  using Photon.Deterministic;
  using UnityEngine;
  /// <summary>
  /// 球を使用したエンティティビューカリングのサンプル実装
  /// このスクリプトは<see cref="QuantumEntityViewUpdater"/>と同じゲームオブジェクトに追加します。
  /// </summary>
  public class QuantumEntityViewCulling : QuantumMonoBehaviour, IQuantumEntityViewCulling {
    /// <summary>
    /// カリング球の中心点
    /// </summary>
    public FPVector3 ViewCullingCenter;
    /// <summary>
    /// カリング半径
    /// </summary>
    public FP ViewCullingRadius = 20;
    List<(EntityRef, View)> _dynamicEntities = new List<(EntityRef, View)>();
    List<(EntityRef, MapEntityLink)> _mapEntities = new List<(EntityRef, MapEntityLink)>();
    /// <summary>
    /// カリング球内の動的エンティティのみを返す
    /// </summary>
    public unsafe IEnumerable<(EntityRef, View)> DynamicEntityIterator(QuantumGame game, Frame frame, QuantumEntityViewBindBehaviour createBehaviour) {
      _dynamicEntities.Clear();
      var radiusSqr = ViewCullingRadius * ViewCullingRadius;
      if (createBehaviour == QuantumEntityViewBindBehaviour.NonVerified && frame.IsPredicted) {
        // non-verifiedの予測カリングを使用する(予測フレームはオンラインモードのみ)
        var filter = frame.Filter<View>();
        // フィルターの予測カリングを有効にする
        filter.UseCulling = true;
        while (filter.NextUnsafe(out var entity, out var view)) {
          _dynamicEntities.Add((entity, *view));
        }
      } else {
        // 球からの距離をチェックして、エンティティをカリングする
        var filter3D = frame.Filter<Transform3D, View>();
        while (filter3D.NextUnsafe(out var entity, out var transform, out var view)) {
          var distanceSqr = (transform->Position - ViewCullingCenter).SqrMagnitude;
          if (distanceSqr < radiusSqr) {
            _dynamicEntities.Add((entity, *view));
          }
        }
        var filter2D = frame.Filter<Transform2D, View>();
        while (filter2D.NextUnsafe(out var entity, out var transform, out var view)) {   
          var distanceSqr = (transform->Position.XOY - ViewCullingCenter).SqrMagnitude;
          if (distanceSqr < radiusSqr) {
            _dynamicEntities.Add((entity, *view));
          }
        }
      }
      return _dynamicEntities;
    }
    /// <summary>
    /// カリング球内のマップエンティティのみを返す
    /// </summary>
    public unsafe IEnumerable<(EntityRef, MapEntityLink)> MapEntityIterator(QuantumGame game, Frame frame, QuantumEntityViewBindBehaviour createBehaviour) {
      _mapEntities.Clear();
      var radiusSqr = ViewCullingRadius * ViewCullingRadius;
      if (createBehaviour == QuantumEntityViewBindBehaviour.NonVerified && frame.IsPredicted) {
        // non-verifiedの予測カリングを使用する(予測フレームはオンラインモードのみ)
        var filter = frame.Filter<MapEntityLink>();
        // フィルターの予測カリングを有効にする
        filter.UseCulling = true;
        while (filter.NextUnsafe(out var entity, out var link)) {
          _mapEntities.Add((entity, *link));
        }
      } else {
        // 球からの距離をチェックして、エンティティをカリングする
        var filter3D = frame.Filter<Transform3D, MapEntityLink>();
        while (filter3D.NextUnsafe(out var entity, out var transform, out var link)) {
          var distanceSqr = (transform->Position - ViewCullingCenter).SqrMagnitude;
          if (distanceSqr < radiusSqr) {
            _mapEntities.Add((entity, *link));
          }
        }
        var filter2D = frame.Filter<Transform2D, MapEntityLink>();
        while (filter2D.NextUnsafe(out var entity, out var transform, out var link)) {
          var distanceSqr = (transform->Position.XOY - ViewCullingCenter).SqrMagnitude;
          if (distanceSqr < radiusSqr) {
            _mapEntities.Add((entity, *link));
          }
        }
      }
      return _mapEntities;
    }
    /// <summary>
    /// カリング球のギズモ描画
    /// </summary>
    public void OnDrawGizmosSelected() {
      Gizmos.DrawWireSphere(ViewCullingCenter.ToUnityVector3(), ViewCullingRadius.AsFloat);
    }
  }
}
View Flags
View Flagsによって、エンティティビューの詳細を設定し、パフォーマンスを調整できます。
| DisableUpdateView | QuantumEntityView.UpdateView()/QuantumEntityView.LateUpdateView()は処理されず、エンティティビューコンポーネントには転送されません。 | 
| DisableUpdatePosition | エンティティビューの位置の更新を完全に無効にします。 | 
| UseCachedTransform | Transformのプロパティを呼び出さずに、キャッシュされたTransformを使用することで、パフォーマンスを向上させます。 | 
| DisableEntityRefNaming | デフォルトでは、エンティティのゲームオブジェクトにはEntityRefの値に合わせた名前が付けられます。このフラグを設定すると、この動作が無効になります。 | 
| DisableSearchChildrenForEntityViewComponents | エンティティビューコンポーネントから、エンティティビューの子ゲームオブジェクトの検索を無効にします。 | 
| DisableSearchInactiveForEntityViewComponents | エンティティビューコンポーネントから、非アクティブなエンティティビューの子ゲームオブジェクトの検索を無効にします。 | 
| EnableSnapshotInterpolation | 確定フレームのみで更新できるようにTransformバッファを初期化することで、スムーズな視覚表現を保証します。使用中は、pingに比例してビジュアル表示が遅延します。バッファとコールバックは、有効時のみ作成されます。補間モードは、各QuantumEntityViewごとの設定で制御されます。 | 
Predction Error Correction
エンティティビューの誤差修正の設定を微調整します。各パラメーターの詳細な説明は、Unityのインスペクターから確認できます。
Events
UnityEvent<QuantumGame>を使用して、エンティティビューの作成(プールからの作成含む)/破棄のUnityイベントを追加します。
 
エンティティのテレポート
デフォルトでQuantumEntityViewスクリプトは、エンティティのゲームオブジェクトのビジュアルを補間します。これによって、シミュレーションレートと描画レートの違いを調整し、予測ミスにおける誤差修正が行われます。
エンティティを1フレームで遠方の位置に移動させる(つまり「テレポート」させる)と、シミュレーション中のエンティティのデータは即座に対象の位置になりますが、ビューは数フレームに渡って開始位置から終了位置への線形補間が行われます。するとビューのゲームオブジェクトは、非常に速く動いているように目立って見えることがありますが、これは望ましくありません。
この問題を防ぐため、エンティティの位置が大きく変わる場合(通常、エンティティのリスポーンや、テレポート機能があるゲームでの移動)は、transform->Teleport(frame, newPosition);を使用してください。これによって、エンティティビューコンポーネントは自動的に補間なしの移動を適用します。
ビューの検索
非常に一般的なユースケースに、特定のエンティティのビューの検索があります。シミュレーション側はQuantumEntityViewを認識しないため、イベントからEntityRefをビューに渡したり、フレームから(読み取り専用で)ポーリングしたりする必要があります。QuantumEntityViewUpdaterはGetView(EntityRef)関数を持ち、特定のEntityRefのビューを見つけるために使用できます。ビューはディクショナリーにキャッシュされているため、ルックアップは非常に効率的です。
イベントと更新順序
QuantumEntityViewUpdaterのOnObservedGameUpdated関数では、EntityViewの作成・破棄・更新が行われ、イベントが処理される前に呼び出されます。そのため、イベントで破棄されたエンティティは、既にビューを破棄している可能性があります。
カスタム破棄イベント
一般的なパターンとして、エンティティを破棄する際に、ビュー破棄についての追加情報を含むイベントを実行したい場合があります。イベントが処理される前にQuantumEntityViewが破棄されることを防ぐには、Manual Disposalフィールドをtrueに設定してください。
これによって、ビューが生存し続けるようになり、デフォルトでゲームオブジェクトを破棄するQuantumEntityViewUpdaterのDestroyEntityViewInstance関数には渡されないようになります。
これで、イベントハンドラーはビューを検索して、存在するビューの破棄イベントを実行できます。QuantumEntityViewのクリーンアップ(破棄とオブジェクトプールへの返却)は手動で行う必要があります。
AutoFindMapData
マップ上でQuantumEntityViewを使用している場合は、AutoFindMapDataを有効にする必要があります。これを有効にすると、ビューは対応するMapDataオブジェクトを検索し、マップエンティティとそのビューを一致させます。マップ上でエンティティを使用していない場合は、これを無効にして、MapDataスクリプトが存在しないシーンにすることができます。
カスタマイズ
QuantumEntityViewUpdaterは、以下のようなカスタマイズが可能です。
Createのオーバーライド
ゲームオブジェクトをインスタンス化せずにプールから取得するには、CreateEntityViewInstance関数をオーバーライドしてください。関数は、スポーンするビューを示すQuantum.EntityViewパラメーターを持ちます。QuantumEntityView.AssetGuidは、オブジェクトプールのディクショナリーのキーとして使用できます。
C#
protected override QuantumEntityView CreateEntityViewInstance(Quantum.EntityView asset, Vector3? position = null, Quaternion? rotation = null) {
    Debug.Assert(asset.View != null);
    // IQuantumEntityViewPoolを使用して、ビューのオブジェクトプールもカスタマイズできます
    EntityView view = _myObjectPool.GetInstance(asset);
    view.transform.position = position ?? default;
    view.transform.rotation = rotation ?? Quaternion.identity;
    return view;
}
CreateEntityViewInstance()の戻り値は、OnEntityViewInstantiated()でエンティティに割り当てられます。このメソッドも仮想メソッドなのでオーバーライドできますが、ほとんどの場合は必要ありません。オーバーライドする際は、EntityRefの割り当てを適切に維持することが重要です。
Destroyのオーバーライド
ビューを破棄せずにプールに返却するには、DestroyEntityViewInstance()をオーバーライドしてください。
C#
protected virtual void DestroyEntityViewInstance(QuantumEntityView instance) {
    _myObjectPool.ReturnInstance(instance);
}
マップエンティティ
マップエンティティは、ActivateMapEntityInstance()でビューをアクティブにする必要があります。これは、必要に応じて独自の動作にオーバーライドできます。
DisableMapEntityInstance()が呼び出されると、デフォルトではゲームオブジェクトが無効になります。この関数も独自の動作でオーバーライドできます。