はじめに
こちらの記事はReazon Advent Calendar 2025の記事となります。
よろしければ他のメンバーの記事もご覧ください!
こんにちは、株式会社ルーデル所属エンジニアの栗原です!
本記事では、UnityでPhoton(PUN2)を使ったマルチプレイヤーゲームの実装について、パラメータ処理/同期周りを主に取り上げて記事にしてみました。
Photonの基礎知識
Photonとは
Photon Unity Networking (PUN) は、Unityでマルチプレイヤーゲームを簡単に作れるネットワークライブラリです。
通常、ネットワークゲームを作るには、サーバーとの通信、データの送受信、オブジェクトの同期など、複雑な処理を自分で実装する必要があります。
Photonを使えば、これらの処理を簡単に実装できます。
Photon公式ページ
Photon Punの公式ドキュメント
※公式ページに記載されていますが、PUN2は現在LTS体制に移行しています。新規プロジェクト等ではFusionまたはQuantumの利用検討が推奨されています。
Fusionの公式ドキュメント
Quantumの公式ドキュメント
MonoBehaviourPunとは?
MonoBehaviourPunは、MonoBehaviourを継承した、Photon専用のベースクラスです。
using Photon.Pun;
public class CharacterBase : MonoBehaviourPun
{
void Start()
{
// photonViewプロパティに直接アクセスできる
if (photonView.IsMine)
{
// 自分のオブジェクトの場合の処理
}
}
}
何ができるようになるか?
MonoBehaviourを継承しているので、当然MonoBehaviourの機能は使える上で、
photonViewプロパティにアクセスできるようになります。
(つまり、GetComponent<PhotonView>();をする必要がなくなる)
PhotonViewとは?
PhotonViewは、ネットワーク上でオブジェクトを識別するためのコンポーネントです。
Photonを通して管理したいオブジェクトにはPhotonViewコンポーネントをつける必要があります。
キャラクター(CharacterBaseをアタッチするオブジェクト)をPhotonで管理する場合、下記のようにRequireComponent属性をつけておくと安心です。
[RequireComponent(typeof(PhotonView))]
public class CharacterBase : MonoBehaviourPun, IPunObservable
よく使うPhotonViewのプロパティ
// 1. IsMine - このオブジェクトは自分のものか?
if (photonView.IsMine)
{
// 自分が操作するキャラクターの場合
transform.position += input * speed;
}
// 2. ViewID - ネットワーク上での一意なID
int id = photonView.ViewID; // 例: 1001
// 3. Owner - このオブジェクトの所有者
Player owner = photonView.Owner;
photonView.isMineは所有権を示すプロパティです。
所有しているということは、自分の端末で生成したオブジェクトであるということです。
※PhotonではUnityのInstantiateではなく、PhotonNetwork.Instantiateメソッドを利用します。
なぜPhotonViewが必要なのか?
ネットワークゲームでは、同じオブジェクトが各プレイヤーの画面に存在します。PhotonViewのViewIDを使って、「どのオブジェクトのことを言っているのか」を識別します。
RPCとは?
RPC(Remote Procedure Call) は「ネットワーク上の端末に対して関数の実行を要求する」機能です。
RpcTargetの種類:
-
RpcTarget.All- 全プレイヤー(自分含む) -
RpcTarget.Others- 他のプレイヤーのみ -
RpcTarget.MasterClient- マスタークライアントのみ
RPCを用いた攻撃処理周りの例
using Photon.Pun;
public class CharacterBase : MonoBehaviourPun
{
// [PunRPC] 属性を付けると、RPCで呼び出せる
[PunRPC]
public void RequireTakeDamage(float damage)
{
// SE等,両方の端末で実行したいものがあればifの前に記載
// 所有権が自身にある場合のみ被ダメ関数を走らせる
if (this.photonView.IsMine)
{
TakeDamage(damage);
}
}
public void TakeDamage(float damage)
{
_hp -= damage;
}
void Attack(CharacterBase targetCharacter)
{
PhotonView targetPhotonView = targetCharacter.photonView;
// 全プレイヤーの端末でRequireTakeDamage関数を実行
targetPhotonView.RPC("RequireTakeDamage", RpcTarget.All, 10f);
}
}
今回はRpcTarget.Allを使用していますが、AllはプレイヤーAとプレイヤーBの端末両方でという意味です。
(RpcTarget.Othersでもこのシチュエーションなら動きます、ただし効果音など両方の端末で処理したい内容が増えた場合はRpcTarget.Allにした上で、所有権(IsMine)判定することで「両方の端末で攻撃音は鳴らしつつ、ダメージ処理は所有権を持つ側のみ行う」ということができます。)
TakeDamage関数が重複して実行されない(2回攻撃のようにならない)状況を作り出すことはできていますが、
プレイヤーAの端末では青色のキャラクターのHPが減ったことを検知できていません。
HP等のパラメータは、別途同期してあげる必要があります。
同期についての詳細を次の章で解説します。
パラメータの同期について
RPCは「特定のタイミングで関数を実行する」機能でしたが、HP値のように継続的に変化する値を同期するには、別の仕組みが必要です。
Photonでは、IPunObservableインターフェースを実装することで、パラメータの自動同期を実現できます。
IPunObservableとは?
IPunObservableは、継続的に変化する値を効率的に同期するためのインターフェースです。
OnPhotonSerializeViewを実装する必要があります。
using Photon.Pun;
public class CharacterBase : MonoBehaviourPun, IPunObservable
{
private float _hp = 20f;
// IPunObservableを実装すると、この関数を実装する必要がある
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
// 自分のデータを送信
stream.SendNext(_hp);
}
else
{
// 相手のデータを受信
_hp = (float)stream.ReceiveNext();
}
}
}
OnPhotonSerializeViewの仕組み:
- **毎秒10回(デフォルト)**自動で呼ばれる
-
stream.IsWritingがtrueの場合:自分のデータを送信 -
stream.IsWritingがfalseの場合:相手のデータを受信
※インターフェースの実装に加えて、PhotonViewコンポーネントでObservable Searchの設定をする必要があります
Auto Find Allに設定していますがManualオプションもあります。

HP同期の実装
前章のRPCの例で、プレイヤーAの端末では青色のキャラクターのHPが減ったことを検知できていませんでした。
IPunObservableを使って、HP値を同期してみます。
using Photon.Pun;
public class CharacterBase : MonoBehaviourPun, IPunObservable
{
private float _hp = 20f;
[PunRPC]
public void RequireTakeDamage(float damage)
{
if (this.photonView.IsMine)
{
TakeDamage(damage);
}
}
public void TakeDamage(float damage)
{
_hp -= damage;
// HP値はOnPhotonSerializeViewで自動的に同期される
}
// IPunObservableの実装
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
// 所有者が自分のHP値を送信
stream.SendNext(_hp);
}
else
{
// 他のプレイヤーがHP値を受信
_hp = (float)stream.ReceiveNext();
}
}
}
まとめ
攻撃処理とHPの同期処理を、RPCとOnPhotonSerializeViewで分けていますが、RPCで同期も行うことが可能です。
プロパティの更新頻度などからどちらが適切かを判断して使い分ける形が良いかと思います。
ここまで閲覧していただきありがとうございました!🙏
▼新卒エンジニア研修のご紹介
レアゾン・ホールディングスでは、2025年新卒エンジニア研修にて「個のスキル」と「チーム開発力」の両立を重視した育成に取り組んでいます。 実際の研修の様子や、若手エンジニアの成長ストーリーは以下の記事で詳しくご紹介していますので、ぜひご覧ください!
▼採用情報
レアゾン・ホールディングスは、「世界一の企業へ」というビジョンを掲げ、「新しい"当たり前"を作り続ける」というミッションを推進しています。 現在、エンジニア採用を積極的に行っておりますので、ご興味をお持ちいただけましたら、ぜひ下記リンクからご応募ください。
