#アイテムデュプリケートとは
アイテムデュプリケートとは、ゲームのネットワーク通信が原因で発生するアイテムの増殖のことを意味します。
例えば、ネットワークを介して同期しているアイテムがあったとします。このアイテムをAさんとBさんが拾おうとした時にどうなるでしょうか。
Aさんの方が早い場合
Aさんの方が早く拾い、その状態の同期が完了してからBさんが拾おうとしたケースです。
この場合Aさんはアイテム取得に成功し、Bさんはアイテムの取得に失敗します。これは正しく想定された動作となります。
AさんとBさんがほぼ同時に拾おうとした場合
では2人がほぼ同時に拾おうとした時にどうなるでしょうか。この場合は問題が発生します。
Aさんがアイテムを拾い、その結果が通知される前にBさんがアイテムを拾ってしまいました。
つまり、1つのアイテムを2人が拾ったことになってしまい、 アイテムの増殖(アイテムデュプリケート)が発生してしまいました。
対策方法
アイテムデュプリケートの対策方法としては、アイテムの状態管理をMasterClient(またはOwnerユーザ)に委ねてしまうのが一般的でしょう。
アイテム自身が持つ状態は信用せず、信頼できる状態をMasterClientに問い合わせて取得することになります。
実装してみる(PUN+UniRxを使う)
ではこの「MasterClientに都度問い合わせる」という処理をPUNで実装してみましょう。
今作っているゲームのコードの流用なのでMasterClientにアイテムの取得予約をし、予約できたらアイテムを拾うという2段構成になっています。以下のコードはその「予約する」の部分です。
アイテム側
using Photon;
using UniRx;
/// <summary>
/// アイテム
/// </summary>
public class Item : MonoBehaviour
{
/// <summary>
/// アイテムの取得予約を試みる(非同期)
/// </summary>
/// <param name="playerId">予約するPlayerId</param>
/// <returns>結果のIObservable</returns>
public IObservable<bool> ReservePickUpItemAsync(int playerId)
{
// MasterClientにリクエストを飛ばしつつ結果を通知するIObservableを生成
return Observable.Create<bool>(observer =>
{
//コールバックを通知するストリーム
var disposable = reservedPlayerIdSubject
.FirstOrDefault()
.Select(x => x == playerId) //自分のIDと一致したIDが返ってきたらtrue
.Subscribe(observer);
//MasterClientに取得予約を試みる
//ストリームの購読をした後に実行しないと、MasterClient上では通知の取得ができない
//(MasterClient上では全て同期で実行されるのでSubscribe前に通知処理まで走ってしまう)
RequestReserve(playerId);
return disposable;
});
}
/// <summary>
/// 取得予約をしてきた人のID
/// </summary>
private int reservedPlayerId = -1; //-1使うか、null許容型使うかとどっちがマシだろうか?
/// <summary>
/// アイテム予約をしたPlayerIDを通知する
/// </summary>
private Subject<int> reservedPlayerIdSubject = new Subject<int>();
/// <summary>
/// 指定のPlayerIdで取得予約をする
/// </summary>
private void RequestReserve(int playerId)
{
//マスタークライアントへ取得予約
photonView.RPC("RpcReserveItem", PhotonTargets.MasterClient, playerId);
}
/// <summary>
/// アイテムの取得予約をする
/// Owner側でのみ実行される
/// </summary>
[PunRPC]
protected void RpcReserveItem(int playerId)
{
if (!PhotonNetwork.isMasterClient) return;
if (reservedPlayerId == -1)
{
//誰も予約していないなら予約
reservedPlayerId = playerId;
}
//現在の予約済みユーザのIDを返す
photonView.RPC("RpcResponseReserveItem", PhotonPlayer.Find(playerId), reservedPlayerId);
}
/// <summary>
/// RequestReserveのコールバック
/// </summary>
/// <param name="reservedPlayerId">予約できたPlayerId</param>
[PunRPC]
protected void RpcResponseReserveItem(int reservedPlayerId)
{
this.reservedPlayerId = reservedPlayerId;
reservedPlayerIdSubject.OnNext(reservedPlayerId);
}
}
1つのファイル上にメソッドが並んでいますが、それぞれ呼び出され方が違います。 RPC使うと本当にややこしくなる
流れは以下のようになっています。
1.RequestReserve
をクライアントが実行する
2. MasterClientへ問い合わせる(RpcReserveItem
をRPCで実行)
3. MasterClient側でRpcReserveItem
が実行される
4. クライアントへ結果が通知される(RpcResponseReserveItem
がRPCで実行される)
5. 結果をUniRxのSubjectで通知
ミソなのは、IObservable<bool> ReservePickUpItemAsync
がIObservableで結果を返すようになっていることです。
これにより、呼び出し元は非同期で結果の待機ができるようになります。
プレイヤ側
(もともとあった実装から該当箇所のみ抜き取ったのでちょっと中途半端)
using System.Collections;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
public class Player : MonoBehaviour
{
private void Update()
{
//省略
//アイテムの近くにきたら実行
StartCoroutine(PickUpItemCoroutine(targetItem));
}
/// <summary>
/// アイテム取得コルーチン
/// </summary>
/// <param name="itemObject">対象のアイテム</param>
private IEnumerator PickUpItemCoroutine(GameObject itemObject)
{
var itemComponent = itemObject.GetComponent<Item>();
var canPickUp = false;
// アイテムの取得予約をして待機
yield return itemComponent
.ReservePickUpItemAsync(PhotonNetwork.player.ID)
.StartAsCoroutine(x => canPickUp = x, ex => { });
if (!canPickUp)
{
//予約に失敗したら終了
yield break;
}
//以下、アイテムを実際に拾う処理が続く
// 略
}
}
ReservePickUpItemAsync
がIObservableで予約の結果を通知するようにしてあるので、StartAsCoroutine
と組み合わせてコルーチン上で非同期待受を行います。
成功した場合はtrue、失敗時はfalseが返ってくるので、コルーチン上で判定して処理の続行の可否を判断します。
まとめ
- アイテムデュプリケートは面倒くさいけどちゃんと対策しよう
- ネットワーク同期ってやっぱりややこしい
- 非同期処理はUniRxとコルーチンを組み合わせると綺麗に書ける