This document is about: QUANTUM 3
SWITCH TO

Entity View

概述

**QuantumEntityViews**通過 Quantum 的View組件與Entities關聯。它包含一個指向 GameObject 的 AssetRef,該 GameObject 代表特定實體的視圖並應被實例化。在 Unity 中配置帶有QuantumEntityView 實體原型 預製體或場景物件時,Quantum 的View組件會自動填充到原型中。

**QuantumEntityViewUpdater**負責處理 Unity 中所有實體的視圖端,例如銷毀、創建和更新關聯實體的視圖遊戲物件,這些操作基於模擬數據。QuantumEntityViewUpdater腳本需要添加到所有包含 QuantumMap 的場景中。

**QuantumEntityViewComponents**可用於為個別 EntityViews 添加視圖相關功能。請參閱 實體視圖組件部分。

池化

默認情況下,QuantumEntityViewUpdater在實體創建時會創建新的QuantumEntityView預製體實例,並在實體銷毀時銷毀相應的視圖 GameObject。

要啟用實體視圖的池化,請將QuantumEntityViewPool腳本添加到QuantumEntityViewUpdater遊戲物件上。該池與 EVU 無縫協作,並可以通過IQuantumEntityViewPool接口替換為自定義實現。

HidePooledObjectsInHierarchy 啟用後,池化物件的HideFlags.HideInHierarchy會被設置
ResetGameObjectScale 啟用後,物件的局部縮放會被重置為一
Precache Items 物件會在Awake()期間實例化並放入池中

訂閱 Quantum 回調或事件的 EntityViews 必須確保:

  • 在遊戲物件返回池之前取消訂閱
  • 或者在訂閱時啟用onlyIfActiveAndEnabled參數,如下所示

C#

QuantumEvent.Subscribe<EventPlayerKilled>(this, OnKilled, onlyIfActiveAndEnabled: true);

綁定行為

Unity 中的QuantumEntityView腳本有一個名為Bind Behaviour的屬性,可以設置為:

  • Non Verified: 視圖 GameObject 可以在預測幀中創建
  • Verified: 視圖 GameObject 只能在驗證幀中創建

對於頻繁實例化的實體視圖和/或由於遊戲反應時間機制等需要儘快在玩家畫面上顯示的實體視圖,使用Non Verified通常更好。例如,在快節奏射擊遊戲中創建投射物 應該使用這種替代方法。

Verified則適用於不需要立即顯示且可以承受等待驗證幀的小延遲的實體視圖。這可以避免在預測錯誤時創建/銷毀視圖物件。例如,可玩角色實體的創建 就是一個適合使用此選項的例子。

手動銷毀

如果啟用了Manual Disposal屬性,QuantumEntityViewUpdater中的銷毀方法會被跳過。這允許通過QuantumEntityViewOnEntityDestroyed回調手動銷毀視圖,或者通過自定義銷毀事件銷毀它們。

視圖剔除 (Quantum 3.0.3)

EntityViewUpdater 可以通過在QuantumEntityViewUpdater GameObject 上添加IQuantumEntityViewCulling接口來擴展,以控制哪些 Quantum 實體與 Unity 視圖同步。它可以覆蓋動態實體和地圖實體的迭代器。

示例實現了一個基於球體檢查的基本剔除算法,同時在線上模式中重用非驗證視圖的預測剔除功能。

C#

namespace Quantum {
  using System.Collections.Generic;
  using Photon.Deterministic;
  using UnityEngine;

  /// <summary>
  /// Sample implementation of entity view culling using a sphere.
  /// Add this script to the same GameObject that has the <see cref="QuantumEntityViewUpdater"/>.
  /// </summary>
  public class QuantumEntityViewCulling : QuantumMonoBehaviour, IQuantumEntityViewCulling {
    /// <summary>
    /// The culling sphere center.
    /// </summary>
    public FPVector3 ViewCullingCenter;
    /// <summary>
    /// The culling radius.
    /// </summary>
    public FP ViewCullingRadius = 20;

    List<(EntityRef, View)> _dynamicEntities = new List<(EntityRef, View)>();
    List<(EntityRef, MapEntityLink)> _mapEntities = new List<(EntityRef, MapEntityLink)>();

    /// <summary>
    /// Only return dynamic entities inside the culling sphere.
    /// </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) {
        // Use the prediction culling for non-verified bindings (this frame is predicted only in online mode)
        var filter = frame.Filter<View>();
        // Make sure to enabled prediction culling on the filter
        filter.UseCulling = true;
        while (filter.NextUnsafe(out var entity, out var view)) {
          _dynamicEntities.Add((entity, *view));
        }
      } else {
        // Use sphere distance check to cull entities
        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>
    /// Only return map entities inside the culling sphere.
    /// </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) {
        // Use the prediction culling for non-verified bindings (this frame is predicted only in online mode)
        var filter = frame.Filter<MapEntityLink>();
        // Make sure to enabled prediction culling on the filter
        filter.UseCulling = true;
        while (filter.NextUnsafe(out var entity, out var link)) {
          _mapEntities.Add((entity, *link));
        }
      } else {
        // Use sphere distance check to cull entities
        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>
    /// Gizmo rendering of view culling sphere.
    /// </summary>
    public void OnDrawGizmosSelected() {
      Gizmos.DrawWireSphere(ViewCullingCenter.ToUnityVector3(), ViewCullingRadius.AsFloat);
    }
  }
}

視圖標誌

視圖標誌用於配置細節並對實體視圖進行性能調整。

DisableUpdateView 不處理QuantumEntityView.UpdateView()QuantumEntityView.LateUpdateView(),也不轉發給實體視圖組件。
DisableUpdatePosition 完全禁用更新實體視圖位置。
UseCachedTransform 使用緩存的變換以提高性能,避免調用 Transform 屬性。
DisableEntityRefNaming 默認情況下,實體遊戲物件會根據其 EntityRef 值命名。啟用此標誌可禁用此行為。
DisableSearchChildrenForEntityViewComponents 禁用搜索實體視圖遊戲物件的子物件以查找實體視圖組件。
DisableSearchInactiveForEntityViewComponents 禁用搜索實體視圖的禁用遊戲物件子物件以查找實體視圖組件。
EnableSnapshotInterpolation 初始化變換緩衝區,以便僅使用驗證幀更新時可以開啟平滑視覺效果。使用時,視覺效果會呈現與延遲成比例的延遲。啟用此選項僅準備緩衝區和回調。插值模式的切換由 QuantumEntityView 上的單獨開關控制。

預測錯誤校正

微調實體視圖的錯誤校正設置。每個參數在 Unity 檢查器中都有詳細說明。

事件

為實體視圖的創建(包括從池中創建)和銷毀添加 Unity 事件。它使用UnityEvent<QuantumGame>

傳送實體

默認情況下,QuantumEntityView腳本會對實體GameObject的視覺效果進行插值。這是為了適應模擬速率、渲染速率以及預測錯誤校正的差異。

當在單一幀中將實體移動到遠處位置(即「傳送」它)時,即使模擬中的實體數據會立即切換到目標位置,視圖插值仍會在幾幀內將其從起始位置平滑移動到結束位置。
這可能會導致視圖遊戲物件在屏幕上快速移動,這不是我們希望看到的。

為了避免這種情況,當實體的位置需要大幅改變時(通常在重生實體或遊戲中有傳送功能時),請使用transform->Teleport(frame, newPosition);。這會使實體視圖組件自動應用非插值的移動。

查找視圖

一個常見的需求是查找特定實體的視圖。由於模擬端不知道QuantumEntityViews,因此必須通過事件將EntityRefs傳遞給視圖,或者從幀中輪詢(只讀)。QuantumEnityViewUpdater有一個GetView(EntityRef)函數,可以用於查找特定EntityRef的視圖。視圖會被緩存在字典中,因此查找非常高效。

事件與更新順序

QuantumEntityViewUpdater上的OnObservedGameUpdated函數負責創建、銷毀和更新EntityViews,它會在事件處理之前被調用。這意味著事件中銷毀的實體可能已經銷毀了其視圖。

自定義銷毀事件

一個常見的模式是銷毀實體,但仍希望向視圖發送包含銷毀相關附加信息的事件。為了防止QuantumEntityView在事件處理之前被銷毀,請將其Manual Disposal字段設置為 true。

這將保持視圖存活,而不是將其傳遞給QuantumEntityViewUpdater's的默認銷毀GameObject的函數DestroyEntityViewInstance

這樣,事件處理程序仍然可以找到視圖並執行銷毀事件。之後,需要手動清理QuantumEntityView,例如銷毀它或將其返回到物件池。

AutoFindMapData

在使用帶有QuantumEntityView的地圖時,必須啟用AutoFindMapData。啟用後,視圖會搜索對應的 MapData 物件並將地圖實體與其視圖匹配。如果不使用帶有實體的地圖,請禁用此選項,以允許場景中沒有MapData腳本。

自定義

QuantumEntityViewUpdater提供了以下自定義選項。

覆蓋創建

要從池中獲取 GameObject 而不是實例化它們,請覆蓋CreateEntityViewInstance函數。該函數有一個Quantum.EntityView參數,指示要生成哪個視圖。QuantumEntityView.AssetGuid可以用作池化物件字典的鍵。

C#

protected override QuantumEntityView CreateEntityViewInstance(Quantum.EntityView asset, Vector3? position = null, Quaternion? rotation = null) {
    Debug.Assert(asset.View != null);

    // view pooling can also be customized by using IQuantumEntityViewPool
    EntityView view = _myObjectPool.GetInstance(asset);

    view.transform.position = position ?? default;
    view.transform.rotation = rotation ?? Quaternion.identity;

    return view;
}

CreateEntityViewInstance()的結果會在OnEntityViewInstantiated()中分配給實體。該方法是虛擬的,可以覆蓋,但在大多數情況下不需要這樣做。覆蓋時,務必保持 EntityRef 的分配。

覆蓋銷毀

要將視圖返回到池中而不是銷毀它們,請覆蓋 DestroyEntityViewInstance()

C#

protected virtual void DestroyEntityViewInstance(QuantumEntityView instance) {
    _myObjectPool.ReturnInstance(instance);
}

地圖實體

對於地圖實體,ActivateMapEntityInstance()負責激活視圖,可以根據需要覆蓋以實現自定義行為。

DisableMapEntityInstance()默認會禁用 GameObject。此函數也可以覆蓋以實現自定義行為。

Back to top