//tips
//PUN2の処理
一般的に、オブジェクト同期は、データの定期的送信で成り立つが、その頻度によって、同期の精度や処理コスト、通信量やその負荷などが大きく変わってくる。
PhotonではPhotonNetworkから簡単に送信頻度を調整することができ、オブジェクト同期で、送信する必要がないデータを送信しないようにすることで、通信量を削減できる。
例えば、スクリプトの isMoving = direction.magnitude > 0f;の値を変更してみるなどの工夫ができる。
今度は球を発射させる処理も追加していく。球に下記のスクリプトをつけたものをprefab化する。
using UnityEngine;
public class Projectile : MonoBehaviour
{
private Vector3 velocity;
public void Init(Vector3 origin, float angle) {
transform.position = origin;
velocity = 9f * new Vector3(Mathf.Cos(angle), Mathf.Sin(angle));
}
private void Update() {
var dv = velocity * Time.deltaTime;
transform.Translate(dv.x, dv.y, 0f);
}
// 画面外になった時に削除する(エディターのSceneビューの画面も影響するので注意)
private void OnBecameInvisible() {
Destroy(gameObject);
}
}
その上で、プレイヤーのネットワークオブジェクト(GamePlayer)のスクリプトを修正する。
クリックした位置に球を発射させるようにしたいので一部修正する。
Debug.Log(angle);
Debug.Log(dp.y);
Debug.Log(dp.x);
Debug.Log(dp.z);
で調べたところangle,dpがそれぞれ固定値になっていたのでCamera.main.ScreenToWorldPoint(Input.mousePosition)の見直しを行う。
Input.mousePositionの値をそのまま使うとZ軸が0のため上手く取得できないのでz値を入れる。
Vector3 pos = Input.mousePosition;
pos.z = 10.0f;
Vector3 mouseWorldPosition = Camera.main.ScreenToWorldPoint(pos);
これによりきちんとクリックしたマウスの方に球が飛ぶようになった。
球をネットワークオブジェクトにすれば簡単に同期できるが、通信コストが当然かかるため、ネットワークオブジェクトにしないやり方で同期する方法を考える。
他プレイヤー側でメソッドを呼び出して実行するRPC(リモートプロシージャコール)を使用する。
PhotonではRPCで実行したいメソッドに[PunRPC]属性をつけることで、PhotonView.RPC()から呼び出せるようになるので、GamePlayer.FireProjectile()をRPCで実行し、同期させる。
// FireProjectile(angle)をRPCで実行する
photonView.RPC(nameof(FireProjectile), RpcTarget.All, angle);
// [PunRPC]属性をつけると、RPCでの実行が有効になる
[PunRPC]
private void FireProjectile(float angle) {
var projectile = Instantiate(projectilePrefab);
projectile.Init(transform.position, angle);
これらを修正するときちんと同期することがわかる。
Object.Instantiate()やObject.Destroy()は重い処理なので、一度生成された弾のインスタンスはGameObject.SetActive()の切り替えで使い回せるようにする。
まず弾のスクリプトからObject.Destroy()を取り除く。
using UnityEngine;
public class Projectile : MonoBehaviour
{
private Vector3 velocity;
public bool IsActive => gameObject.activeSelf;
public void Activate(Vector3 origin, float angle)
{ // メソッド名変更
{
transform.position = origin;
velocity = 9f * new Vector3(Mathf.Cos(angle), Mathf.Sin(angle));
gameObject.SetActive(true);
}
}
public void OnUpdate()
{ // publicにしてメソッド名変更
var dv = velocity * Time.deltaTime;
transform.Translate(dv.x, dv.y, 0f);
}
public void Deactivate()
{
gameObject.SetActive(false);
}
// 画面外になった時に削除する(エディターのSceneビューの画面も影響するので注意)
private void OnBecameInvisible()
{
Deactivate();
}
}
弾のインスタンスを管理する空オブジェクトProjectileManagerを作成し、ヒエラルキーに配置。ProjectileManagerタグを新規作成し、付けることで、GameObject.Find()より高速なGameObject.FindWithTag()で参照できる。
ProjectileManagerに新規スクリプトを追加する。
using System.Collections.Generic;
using UnityEngine;
public class ProjectileManager : MonoBehaviour
{
[SerializeField]
private Projectile projectilePrefab = default; // ProjectileのPrefabの参照
// アクティブな弾のリスト
private List<Projectile> activeList = new List<Projectile>();
// 非アクティブな弾のオブジェクトプール
private Stack<Projectile> inactivePool = new Stack<Projectile>();
private void Update()
{
// 逆順にループを回して、activeListの要素が途中で削除されても正しくループが回るようにする
for (int i = activeList.Count - 1; i >= 0; i--)
{
var projectile = activeList[i];
if (projectile.IsActive)
{
projectile.OnUpdate();
}
else
{
Remove(projectile);
}
}
}
// 弾を発射(アクティブ化)するメソッド
public void Fire(Vector3 origin, float angle)
{
// 非アクティブの弾があれば使い回す、なければ生成する
var projectile = (inactivePool.Count > 0)
? inactivePool.Pop()
: Instantiate(projectilePrefab, transform);
projectile.Activate(origin, angle);
activeList.Add(projectile);
}
// 弾を消去(非アクティブ化)するメソッド
public void Remove(Projectile projectile)
{
activeList.Remove(projectile);
projectile.Deactivate();
inactivePool.Push(projectile);
}
}
まだ、GamePlayerスクリプトにprojectileのinitの残りがあり、エラーとなっているのでそちらも修正する。
弾のインスタンスを生成する処理はProjectileManagerに移したので、弾のPrefabの参照は削除して、かわりにProjectileManagerの参照をタグから取得する。
using Photon.Pun;
using UnityEngine;
// IPunObservableインターフェースを実装する
public class GamePlayer : MonoBehaviourPunCallbacks, IPunObservable
{
private ProjectileManager projectileManager;
private Renderer spriteRenderer;
private float hue = 0f; // 色相値
private bool isMoving = false; // 移動中フラグ
private void Awake()
{
projectileManager = GameObject.FindWithTag("ProjectileManager").GetComponent<ProjectileManager>();
spriteRenderer = GetComponent<Renderer>();
ChangeBodyColor();
}
private void Update()
{
if (photonView.IsMine)
{
var direction = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")).normalized;
var dv = 6f * Time.deltaTime * direction;
transform.Translate(dv.x, dv.y, 0f);
if (Input.GetMouseButtonDown(0))
{
var playerWorldPosition = transform.position;
//var mouseWorldPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3 pos = Input.mousePosition;
pos.z = 10.0f;
Vector3 mouseWorldPosition = Camera.main.ScreenToWorldPoint(pos);
var dp = mouseWorldPosition - playerWorldPosition;
float angle = Mathf.Atan2(dp.y, dp.x);
Debug.Log(angle);
Debug.Log(dp.y);
Debug.Log(dp.x);
Debug.Log(dp.z);
// FireProjectile(angle)をRPCで実行する
photonView.RPC(nameof(FireProjectile), RpcTarget.All, angle);
}
// 移動中なら色相値を変化させていく
isMoving = direction.magnitude > 0f;
if (isMoving)
{
hue = (hue + Time.deltaTime) % 1f;
}
ChangeBodyColor();
}
}
// [PunRPC]属性をつけると、RPCでの実行が有効になる
[PunRPC]
private void FireProjectile(float angle)
{
projectileManager.Fire(transform.position, angle);
}
// データを送受信するメソッド
void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
// 自身側が生成したオブジェクトの場合は
// 色相値と移動中フラグのデータを送信する
stream.SendNext(hue);
stream.SendNext(isMoving);
}
else
{
// 他プレイヤー側が生成したオブジェクトの場合は
// 受信したデータから色相値と移動中フラグを更新する
hue = (float)stream.ReceiveNext();
isMoving = (bool)stream.ReceiveNext();
ChangeBodyColor();
}
}
private void ChangeBodyColor()
{
gameObject.GetComponent<Renderer>().material.color = new Color(hue, hue, hue);
}
}
これでエラーが消えたので球が使いまわされているかコンソールから確認する。
きちんと画面外から出たものを新規使用分として場所移動させて再利用できているのが確認できた。
次にプレイヤーと球の当たり判定を実装するため、コライダーを追加する。
GamePlayerにrigidbodyとコライダーを追加、projectileにはコライダーを追加し、is triggerをオンにする。
弾のスクリプトに、弾自体のIDと弾を発射したプレイヤーのIDを持たせるようにすることで、誰が発射したどの弾に当たったのかを、IDで判別したり、ネットワーク上で通信できるので、そちらもProjectileに組み込む。
using UnityEngine;
public class Projectile : MonoBehaviour
{
private Vector3 velocity;
public int Id { get; private set; } // 弾のID
public int OwnerId { get; private set; } // 弾を発射したプレイヤーのID
public bool Equals(int id, int ownerId) => id == Id && ownerId == OwnerId;
public bool IsActive => gameObject.activeSelf;
public void Activate(int id, int ownerId, Vector3 origin, float angle)
{ // メソッド名変更
{
Id = id;
OwnerId = ownerId;
transform.position = origin;
velocity = 9f * new Vector3(Mathf.Cos(angle), Mathf.Sin(angle));
gameObject.SetActive(true);
}
}
public void OnUpdate()
{ // publicにしてメソッド名変更
var dv = velocity * Time.deltaTime;
transform.Translate(dv.x, dv.y, 0f);
}
public void Deactivate()
{
gameObject.SetActive(false);
}
// 画面外になった時に削除する(エディターのSceneビューの画面も影響するので注意)
private void OnBecameInvisible()
{
Deactivate();
}
}
using System.Collections.Generic;
using UnityEngine;
public class ProjectileManager : MonoBehaviour
{
[SerializeField]
private Projectile projectilePrefab = default; // ProjectileのPrefabの参照
// アクティブな弾のリスト
private List<Projectile> activeList = new List<Projectile>();
// 非アクティブな弾のオブジェクトプール
private Stack<Projectile> inactivePool = new Stack<Projectile>();
private void Update()
{
// 逆順にループを回して、activeListの要素が途中で削除されても正しくループが回るようにする
for (int i = activeList.Count - 1; i >= 0; i--)
{
var projectile = activeList[i];
if (projectile.IsActive)
{
projectile.OnUpdate();
}
else
{
Remove(projectile);
}
}
}
// 弾を発射(アクティブ化)するメソッド
public void Fire(int id, int ownerId, Vector3 origin, float angle)
{
// 非アクティブの弾があれば使い回す、なければ生成する
var projectile = (inactivePool.Count > 0)
? inactivePool.Pop()
: Instantiate(projectilePrefab, transform);
projectile.Activate(id, ownerId, origin, angle);
activeList.Add(projectile);
}
// 弾を消去(非アクティブ化)するメソッド
public void Remove(Projectile projectile)
{
activeList.Remove(projectile);
projectile.Deactivate();
inactivePool.Push(projectile);
}
public void Remove(int id, int ownerId)
{
foreach (var projectile in activeList)
{
if (projectile.Equals(id, ownerId))
{
Remove(projectile);
break;
}
}
}
}
さらに、プレイヤーのスクリプトで実際のIDを渡す。PhotonView.Owner.ActorNumberから、ネットワークオブジェクトを生成したプレイヤーのIDを取得することができるので、それを使う。
using Photon.Pun;
using UnityEngine;
// IPunObservableインターフェースを実装する
public class GamePlayer : MonoBehaviourPunCallbacks, IPunObservable
{
private ProjectileManager projectileManager;
// 弾を発射する時に使う弾のID
private int projectileId = 0;
private Renderer spriteRenderer;
private float hue = 0f; // 色相値
private bool isMoving = false; // 移動中フラグ
private void Awake()
{
projectileManager = GameObject.FindWithTag("ProjectileManager").GetComponent<ProjectileManager>();
spriteRenderer = GetComponent<Renderer>();
ChangeBodyColor();
}
private void Update()
{
if (photonView.IsMine)
{
var direction = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")).normalized;
var dv = 6f * Time.deltaTime * direction;
transform.Translate(dv.x, dv.y, 0f);
if (Input.GetMouseButtonDown(0))
{
var playerWorldPosition = transform.position;
//var mouseWorldPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3 pos = Input.mousePosition;
pos.z = 10.0f;
Vector3 mouseWorldPosition = Camera.main.ScreenToWorldPoint(pos);
var dp = mouseWorldPosition - playerWorldPosition;
float angle = Mathf.Atan2(dp.y, dp.x);
// FireProjectile(angle)をRPCで実行する
photonView.RPC(nameof(FireProjectile), RpcTarget.All, angle);
// 弾を発射するたびに弾のIDを1ずつ増やしていく
photonView.RPC(nameof(FireProjectile), RpcTarget.All, ++projectileId, angle);
}
// 移動中なら色相値を変化させていく
isMoving = direction.magnitude > 0f;
if (isMoving)
{
hue = (hue + Time.deltaTime) % 1f;
}
ChangeBodyColor();
}
}
// [PunRPC]属性をつけると、RPCでの実行が有効になる
[PunRPC]
private void FireProjectile(int id, float angle)
{
projectileManager.Fire(id, photonView.OwnerActorNr, transform.position, angle);
}
// データを送受信するメソッド
void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
// 自身側が生成したオブジェクトの場合は
// 色相値と移動中フラグのデータを送信する
stream.SendNext(hue);
stream.SendNext(isMoving);
}
else
{
// 他プレイヤー側が生成したオブジェクトの場合は
// 受信したデータから色相値と移動中フラグを更新する
hue = (float)stream.ReceiveNext();
isMoving = (bool)stream.ReceiveNext();
ChangeBodyColor();
}
}
private void ChangeBodyColor()
{
gameObject.GetComponent<Renderer>().material.color = new Color(hue, hue, hue);
}
}
次に、当たり判定の処理を考える。
Photon Cloudではサーバー側の処理を入れることはできないので、弾を受ける側か弾を当てる側のどちらかで当たり判定を処理する必要がある。
当たり判定を弾を受ける側に設定すると、弾を見た目通りに避けられるが、相手に当てたはずの弾が当たらないことがあり、弾を当てる側に当たり判定を設定すると、自身の弾が見た目通りに相手に当たるが、避けたはずの相手の弾に当たることがあるという問題を抱える。
これは、どちらに問題を置いた方がより軽微な問題になるかを検討する必要がある。
今回は自身のオブジェクトが他プレイヤーの発射した弾に当たったかを判定することにしたので、自身のプレイヤーIDをPhotonNetwork.LocalPlayer.ActorNumberで取得し、それと弾を発射したプレイヤーのIDとを比較することで判定する。
GamePlayerにメソッドを追加した。
private void OnTriggerEnter(Collider collision)
{
if (photonView.IsMine)
{
var projectile = collision.GetComponent<Projectile>();
if (projectile != null && projectile.OwnerId != PhotonNetwork.LocalPlayer.ActorNumber)
{
photonView.RPC(nameof(HitByProjectile), RpcTarget.All, projectile.Id, projectile.OwnerId);
}
}
}
[PunRPC]
private void HitByProjectile(int projectileId, int ownerId)
{
projectileManager.Remove(projectileId, ownerId);
}