This document is about: FUSION 2
SWITCH TO

Metaverse Picazoo


Available in the Industries Circle
Circle

概要

PicaZooシーンは、プレイヤーがペイント銃で動物像を見つけるミニゲームです。

ここでは、ネットワークを通してテクスチャ変更を同期する方法を示します。

もしシューティングゲームを開発したのであれば、専用の技術サンプルをご覧ください。

デスクトップモードでは、マウスでの銃の操作を改善するために特別なコントローラーを使用しています。

Fusion Metaverse PicaZoo

PaintBlaster

シーンにはいくつかのペイント銃が置かれています。

  • 定義済みの色の弾を撃つ銃
  • ランダムな色の弾を撃つ銃
Fusion Metaverse Guns

銃はPaintBlasterクラスで管理され、2つのリストを保持します。

  • 弾のリスト
  • インパクトのリスト

リストはネットワーク化されているので、リモートのプレイヤーは、新しい弾/インパクトが発生したかどうかをチェック可能で、それらを表示できます。
これを行うために、ネットワークオブジェクトの弾プレハブをスポーンする必要はありません。

弾のプロパティは、BulletShootネットワーク構造体にまとめられています。

C#

public struct BulletShoot : INetworkStruct
{
    public Vector3 initialPosition;
    public Quaternion initialRotation;
    public float shootTime;
    public int bulletPrefabIndex;
    public PlayerRef source;
}

最初に、射撃のユーザー入力はInputActionPropertyUseActionで制御され、これはIsUsedで設定されています。

C#

public bool IsUsed
{
    get
    {
        return UseAction.action.ReadValue<float>() > minAction;
    }
}

LateUpdate()ごとに、ローカルプレイヤーが銃を持って撃っているかをチェックします。

C#

public void LateUpdate()
{
...
    if (Object == null || Object.HasStateAuthority == false) return;

    if (useInput) // VR
    {
        if (IsGrabbedByLocalPLayer)
        {
            TriggerPressed(IsUsed);
        }
        else
            wasUsed = false;
    }
...

    PurgeBulletShoots();
    PurgeRecentImpacts();
}

C#

public void TriggerPressed(bool isPressed)
{
...
    if (isPressed)
        Shoot();
...
}

デスクトップリグでは、トリガーはDesktopAimクラスで検知されます。

C#

private void LateUpdate()
{
    if (!grabber) return;
    if (grabber.HasStateAuthority == false) return;

    blaster.TriggerPressed(Mouse.current.leftButton.isPressed);
...
    }

弾の管理は、新しい弾をBulletShootsネットワークリストに追加することで構成されます。

C#

public void Shoot()
{
    OnShooting(projectileOriginPoint.transform.position, projectileOriginPoint.transform.rotation);
}

C#

private void OnShooting(Vector3 position, Quaternion rotation)
{
...
    lastBullet.initialPosition = position;
    lastBullet.initialRotation = rotation;
    lastBullet.shootTime = Time.time;
    lastBullet.bulletPrefabIndex = bulletPrefabIndex;
    lastBullet.source = Object.StateAuthority;
    BulletShoots.Add(lastBullet);
...
}

弾リストはネットワーク化されているので、誰が撃ったかに関係なくすべてのプレイヤーが更新データを受信します。
そのため、プレイヤーはRender()で新しいデータを受信したかをチェックして、新しい弾が発生していたらローカルのゲームオブジェクトをスポーンします(CheckShoots())。

C#

const int MAX_BULLET_SHOOT = 50;
[Networked]
[Capacity(MAX_BULLET_SHOOT)]
public NetworkLinkedList<BulletShoot> BulletShoots { get; }

C#

public override void Render()
{
    base.Render();
    CheckShoots();
    CheckRecentImpacts();
}

C#

void CheckShoots()
{
    foreach (var bullet in BulletShoots)
    {
        if (bullet.shootTime > lastSpawnedBulletTime && bullet.source == Object.StateAuthority)
        {
            var bulletPrefab = (bullet.bulletPrefabIndex == -1) ? defaultProjectilePrefab : projectilePrefabs[bullet.bulletPrefabIndex];
            var projectileGO = GameObject.Instantiate(bulletPrefab, bullet.initialPosition, bullet.initialRotation);
            var projectile = projectileGO.GetComponent<PaintBlasterProjectile>();
            projectile.sourceBlaster = this;
            lastSpawnedBulletTime = bullet.shootTime;
        }
    }
}

スポーンした発射物のゲームオブジェクトは、銃自体に登録されることに注意してください。
そうすることで銃の状態権限者のみが、発射物がターゲットに当たったかどうかをチェックすることを保証できます。

リストサイズを制限するために、PurgeBulletShoots()メソッドは古い弾をリストから削除します。
実際、すべての弾を無制限に保持するのは無駄になります。なぜなら、弾がリストに追加されネットワーク上で通信されるとすぐに、すべてのプレイヤーによって処理されるからです。

さらに、状態権限者が変わると弾リストはクリアされます。新しい権限者が以前の権限者と同じ時間を参照しているとは限らないためです(IStateAuthorityChanged.StateAuthorityChanged())。

インパクト

ネットワーク構造体のImpactInfoは、インパクトのパラメーターを保存するために作成されます。

C#

public struct ImpactInfo : INetworkStruct
{
    public Vector2 uv;
    public Color color;
    public float sizeModifier;
    public NetworkBehaviourId networkProjectionPainterId;
    public float impactTime;
    public PlayerRef source;
}

発射物がターゲットに当たると、RecentImpactsネットワークリストに新しいインパクトが追加されます。
それから、銃の状態権限を持つプレイヤーはRender()内のCheckRecentImpacts()メソッドでリストをチェックして、オブジェクトのテクスチャの変更をリクエストします。

C#

const int MAX_IMPACTS = 50;
[Networked]
Capacity(MAX_IMPACTS)]
public NetworkLinkedList<ImpactInfo> RecentImpacts { get; }

C#

void CheckRecentImpacts()
{
    foreach (var impact in RecentImpacts)
    {
        if (impact.impactTime > lastRecentImpactTime && impact.source == Object.StateAuthority)
        {
            if (Runner.TryFindBehaviour<NetworkProjectionPainter>(impact.networkProjectionPainterId, out var networkProjectionPainter))
            {
                if (networkProjectionPainter.HasStateAuthority || Object.HasStateAuthority)
                {
                    networkProjectionPainter.PaintAtUV(impact.uv, impact.sizeModifier, impact.color);
                }
            }
        lastRecentImpactTime = impact.impactTime;
        }
    }
}

弾同様にインパクトリストサイズを制限するため、PurgeRecentImpacts()メソッドは古いインパクトをリストから削除します。
実際、すべてのインパクトを無制限に保持するのは無駄になります。なぜなら、インパクトがリストに追加されネットワーク上で通信されるとすぐに、すべてのプレイヤーによって処理されるからです。

PaintBlasterProjectile

FixedUpdate()内で、銃の状態権限を持つプレイヤーは発射物がオブジェクトと衝突しているかをチェックします。

発射物がオブジェクトと衝突していないなら、一定時間後に発射物はデスポーンします。

C#

private void Update()
{
    if (Time.time > endOfLifeTime)
    {
        Destroy(gameObject);
    }
}

発射物がオブジェクトと衝突しているなら(レイキャストのヒットで決定される)、

  • インパクト位置が計算され、オブジェクトのテクスチャが変更可能(ProjectionPainterNetworkProjectionPainterコンポーネントを持つオブジェクト)ならRecentImpactsネットワークリストに追加される
  • 発射物はデスポーンされる
  • パーティクルシステムのインパクトプレハブがスポーンして、小さなビジュアルエフェクトを生成する

Impactネットワーク変数のuvフィールドは、RayCastが当たったtextureCoordフィールドで埋められます。
このtextureCoordフィールドの値を得るには、いくつかの条件が必要です。

  • オブジェクトは、ペイントしたいものとおなじメッシュのMeshColliderを持つ必要がある
  • メッシュのFBXで、"Meshes > read/write"パラメーターがtrueになっている必要がある

NetworkProjectionPainter & ProjectionPainter

Fusion Metaverse Animal

ネットワーク上でのテクスチャ変更の同期は、以下のステップに要約されます。

  • NetworkProjectionPainterは、発射物を撃った銃のPaintAtUV()メソッドから、テクスチャ変更リクエストを受け取る
  • それからローカルのProjectionPainterコンポーネントにリクエストを送り、実際のテクスチャ変更を行う
  • テクスチャ変更が終了したら、コールバックによってNetworkProjectionPainterに通知される
  • オブジェクトの状態権限を持つプレイヤーは、インパクトのすべての情報を持つネットワークリストを更新する
  • リモートプレイヤーは、ローカルのProjectionPainterコンポーネントを使用して、オブジェクトのテクスチャを更新できる

では、これら動作の詳細を見ていきましょう。

step 1 : テクスチャ変更リクエスト

最初に、NetworkProjectionPainterは、発射物を撃った銃のPaintAtUV()メソッドから、テクスチャ変更リクエストを受け取ります。
オブジェクトの状態権限を持つプレイヤーのみに最終的なテクスチャの状態を維持する責任がありますが、リモートプレイヤー上で状態権限を持たないオブジェクトが衝突した時の遅延を回避するため、PrePaintAtUV()メソッドによって一時的なテクスチャ変更が行われます。

C#

public void PaintAtUV(Vector2 uv, float sizeModifier, Color color)
{
    if (Object.HasStateAuthority)
    {
        painter.PaintAtUV(uv, sizeModifier, color);
    }
    else
    {
        painter.PrePaintAtUV(uv, sizeModifier, color);
    }
}

step 2 : テクスチャ変更

ローカルのProjectionPainterは、受信したインパクトのパラメーター(UV座標・発射物のサイズと色)を使用して、オブジェクトのテクスチャ変更を行います。

テクスチャ変更を手短に説明すると、

  • ペイントするオブジェクトのテクスチャは、一時的なカメラレンダーテクスチャに置き換えられる
  • インパクトごとに、インパクトブラシがテクスチャの正しいなUV座標の前に表示される
  • それからカメラがキャプチャされレンダーテクスチャを更新するため、プレイヤーは新しい弾のインパクトを見ることができる
  • 定期的に、最終的なオブジェクトのテクスチャが、すべての過去のインパクトを含んだ新しいテクスチャに更新されることで、リソース消費を抑える

全体的なプロセスのさらなる説明は、Texture Paintingの記事をご覧ください。

step 3 : テクスチャ変更コールバック

Awake()内で、ProjectionPainterはリスナーを探します(NetworkProjectionPainterIProjectionListenerインターフェースを実装しています)。

C#

private void Awake()
{
    listeners = new List<IProjectionListener>(GetComponentsInParent<IProjectionListener>());
}

そのため、テクスチャ変更が終了した時、それをNetworkProjectionPainterに通知できます。

C#

public void PaintAtUV(Vector2 uv, float sizeModifier, Color color)
{
    var paintPosition = UV2CaptureLocalPosition(uv);
    Paint(paintPosition, sizeModifier, color);
    foreach (var listener in listeners)
    {
        listener.OnPaintAtUV(uv, sizeModifier, color);
    }
}

step 4 : リストに新しいインパクトを追加し、リモートプレイヤーに伝える

オブジェクトの状態権限者のNetworkProjectionPainterは、すべてのインパクトの情報を持つネットワーク配列を更新できます。

C#

public void OnPaintAtUV(Vector2 uv, float sizeModifier, Color color)
{
    if (Object.HasStateAuthority)
    {
    ...
        ProjectionInfosLinkedList.Add(new ProjectionInfo { uv = uv, sizeModifier = sizeModifier, color = color, paintId = nextStoredPaintId });
    ...
    }
}

テクスチャ変更に必要なインパクトのパラメーターは、ProjectionInfoネットワーク構造体に保存されます。
すべてのインパクトは、ネットワークリストProjectionInfoLinkedListに保存されます。
そのため、新しいインパクトがリストに追加された時、すべてのプレイヤーに通知されます。

C#

public struct ProjectionInfo:INetworkStruct
{
    public Vector2 uv;
    public Color color;
    public float sizeModifier;
    public int paintId;
}

C#

[Networked]
[Capacity(MAX_PROJECTIONS)]
public NetworkLinkedList<ProjectionInfo> ProjectionInfosLinkedList { get; }

step 5 : リモートプレイヤーによるテクスチャ更新

これでリモートプレイヤーは、ネットワークリストProjectionInfoLinkedListとローカルのProjectionPainterコンポーネントを使用して、オブジェクトのテクスチャを更新できます。

C#

ChangeDetector changeDetector;

public override void Render()
{
    base.Render();
    foreach (var changedVar in changeDetector.DetectChanges(this))
    {
        if (changedVar == nameof(ProjectionInfosLinkedList))
        {
            OnInfosChanged();
        }
    }
}

void OnInfosChanged()
{
...
    painter.PaintAtUV(info.uv, info.sizeModifier, info.color);
...
}
Back to top