Metaverse Picazoo
概要
PicaZooシーンは、プレイヤーがペイント銃で動物像を見つけるミニゲームです。
ここでは、ネットワークを通してテクスチャ変更を同期する方法を示します。
もしシューティングゲームを開発したのであれば、専用の技術サンプルをご覧ください。
デスクトップモードでは、マウスでの銃の操作を改善するために特別なコントローラーを使用しています。
PaintBlaster
シーンにはいくつかのペイント銃が置かれています。
- 定義済みの色の弾を撃つ銃
- ランダムな色の弾を撃つ銃
銃はPaintBlaster
クラスで管理され、2つのリストを保持します。
- 弾のリスト
- インパクトのリスト
リストはネットワーク化されているので、リモートのプレイヤーは、新しい弾/インパクトが発生したかどうかをチェック可能で、それらを表示できます。
これを行うために、ネットワークオブジェクトの弾プレハブをスポーンする必要はありません。
弾
弾のプロパティは、BulletShoot
ネットワーク構造体にまとめられています。
C#
public struct BulletShoot : INetworkStruct
{
public Vector3 initialPosition;
public Quaternion initialRotation;
public float shootTime;
public int bulletPrefabIndex;
public PlayerRef source;
}
最初に、射撃のユーザー入力はInputActionProperty
のUseAction
で制御され、これは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);
}
}
発射物がオブジェクトと衝突しているなら(レイキャストのヒットで決定される)、
- インパクト位置が計算され、オブジェクトのテクスチャが変更可能(
ProjectionPainter
とNetworkProjectionPainter
コンポーネントを持つオブジェクト)ならRecentImpacts
ネットワークリストに追加される - 発射物はデスポーンされる
- パーティクルシステムのインパクトプレハブがスポーンして、小さなビジュアルエフェクトを生成する
Impact
ネットワーク変数のuv
フィールドは、RayCast
が当たったtextureCoord
フィールドで埋められます。
このtextureCoord
フィールドの値を得るには、いくつかの条件が必要です。
- オブジェクトは、ペイントしたいものとおなじメッシュの
MeshCollider
を持つ必要がある - メッシュのFBXで、"Meshes > read/write"パラメーターがtrueになっている必要がある
NetworkProjectionPainter & ProjectionPainter
ネットワーク上でのテクスチャ変更の同期は、以下のステップに要約されます。
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
はリスナーを探します(NetworkProjectionPainter
はIProjectionListener
インターフェースを実装しています)。
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);
...
}