//tips
//PUN2活用
送受信で発生する遅延を受け取りにかかった時間分を補正する。
PhotonではPhotonNetwork.ServerTimestampから、ミリ秒単位で現在のサーバー時刻を取得できるのでサーバー時刻の比較を行うことでこの時間を抽出する。
注意点としては、値はint型の最大値(2,147,483,647)を超えるとint型の最小値(-2,147,483,648)になって進み続けるので、正確に型の枠内で利用するために、差分をとって大小比較する必要がある。
今までは、RPCを受信して実行された時の座標から球が移動を開始していたため、ネットワーク上の遅延の分だけ座標がずれていた。
これを、弾を発射した時刻での座標と、弾を発射した時刻から現在の時刻までの経過時間から、現在の座標を計算できるようにすることで、全く遅延のない座標の同期を可能にする。
スクリプトにtimestampを追加する。
using UnityEngine;
using Photon.Pun;
public class Projectile : MonoBehaviour
{
private Vector3 origin; // 弾を発射した時刻での座標
private Vector3 velocity;
private int timestamp; // 弾を発射した時刻
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, int timestamp)
{ // メソッド名変更
{
Id = id;
OwnerId = ownerId;
this.origin = origin;
velocity = 9f * new Vector3(Mathf.Cos(angle), Mathf.Sin(angle));
this.timestamp = timestamp;
OnUpdate(); // transform.positionの初期値を決めるため、一度更新する
gameObject.SetActive(true);
}
}
public void OnUpdate()
{ // publicにしてメソッド名変更
// 弾を発射した時刻から現在時刻までの経過時間を求める
float elapsedTime = Mathf.Max(0f, unchecked(PhotonNetwork.ServerTimestamp - timestamp) / 1000f);
// 弾を発射した時刻での座標・速度・経過時間から現在の座標を求める
transform.position = origin + velocity * elapsedTime;
}
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 ,int timestamp)
{
// 非アクティブの弾があれば使い回す、なければ生成する
var projectile = (inactivePool.Count > 0)
? inactivePool.Pop()
: Instantiate(projectilePrefab, transform);
projectile.Activate(id, ownerId, origin, angle, timestamp);
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は、弾を発射した時刻を流用することで、RPCで送信するデータを削減する。
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, transform.position, angle);
}
// 移動中なら色相値を変化させていく
isMoving = direction.magnitude > 0f;
if (isMoving)
{
hue = (hue + Time.deltaTime) % 1f;
}
ChangeBodyColor();
}
}
// [PunRPC]属性をつけると、RPCでの実行が有効になる
[PunRPC]
private void FireProjectile(Vector3 origin, float angle, PhotonMessageInfo info)
{
int timestamp = info.SentServerTimestamp;
projectileManager.Fire(timestamp, photonView.OwnerActorNr, origin, angle, timestamp);
}
// データを送受信するメソッド
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);
}
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);
}
}
自身のプレイヤー名をPhotonNetworkから設定していく。
プレイヤーのネットワークオブジェクト(GamePlayer)にテキストを追加して、そこにプレイヤー名を表示できるようにする。
prefabを開き、TextMeshProをGamePlayerにアタッチ、インスペクターからtext部分に入力する。
GamePlayerのスクリプトにもTextMeshProの部分を追記する。
TextMeshProをインストールしたらcubeのmesh rendererが突然別のものに変わり、cubeが表示されなくなるが、冷静にprefabを開きmesh rendererのelementをdefaultのものに直せば良い。
それでもダメだったら新しく表示されたcube (mesh filter)にcubeを設定する。
次に、スクリプトに、カスタムプロパティの設定を追加して、int型のスコアのデータ"Score”の表示と球に当たったプレイヤーの色を変更するスクリプトを書いていく。
色の変更は対応できているが、textproの名前表示部分にスコアをうまく加えられていない。デバッグして確認する。
var customProperties = photonView.Owner.CustomProperties;
// プレイヤー名の横にスコアを表示する
int score = (customProperties["Score"] is int value) ? value : 0;
nameLabel.text = $"{photonView.Owner.NickName}({score.ToString()})";
Debug.Log(score);
Debug.Log(nameLabel.text);
nameLabel.text = $"{photonView.Owner.NickName}({score.ToString()})”;の部分で下記のエラーが発生し、かつDebug.Logの表示がなされていないことからnameLabelへの接続がうまくできていないようだと考える。
NullReferenceException: Object reference not set to an instance of an object
GamePlayer.Start () (at Assets/Script/GamePlayer.cs:39)
PhotonNetwork.LocalPlayer.NickName = "Player”;を差し込んでみるもまだエラーとなる。
移動時に色変更するスクリプト部分を削ってシンプルにして様子を見る。衝突時に色変更するものは残す。
nameLabel.text = $"{photonView.Owner.NickName}({score.ToString()})”;の箇所のエラーは継続。
別途気になるのは下記のエラー。
RPC method 'FireProjectile' found on object with PhotonView 1001 but has wrong parameters. Implement as 'FireProjectile(Single)'. PhotonMessageInfo is optional as final parameter.
UnityEngine.Debug:LogErrorFormat(Object, String, Object[])
Photon.Pun.PhotonNetwork:ExecuteRpc(Hashtable, Player) (at Assets/Photon/PhotonUnityNetworking/Code/PhotonNetworkPart.cs:632)
Photon.Pun.PhotonNetwork:RPC(PhotonView, String, RpcTarget, Player, Boolean, Object[]) (at Assets/Photon/PhotonUnityNetworking/Code/PhotonNetworkPart.cs:1222)
Photon.Pun.PhotonNetwork:RPC(PhotonView, String, RpcTarget, Boolean, Object[]) (at Assets/Photon/PhotonUnityNetworking/Code/PhotonNetwork.cs:2812)
Photon.Pun.PhotonView:RPC(String, RpcTarget, Object[]) (at Assets/Photon/PhotonUnityNetworking/Code/PhotonView.cs:792)
GamePlayer:Update() (at Assets/Script/GamePlayer.cs:57)
カスタムプロパティを取得・設定するクラス(GamePlayerProperty)を作成し、そちらを通すことで改善されるか確認する。
using Photon.Realtime;
using Random = UnityEngine.Random;
using Hashtable = ExitGames.Client.Photon.Hashtable;
public static class GamePlayerProperty
{
private const string ScoreKey = "Score"; // スコアのキーの文字列
private const string HueKey = "Hue"; // 色相値のキーの文字列
private static Hashtable hashtable = new Hashtable();
// (Hashtableに)プレイヤーのスコアがあれば取得する
public static bool TryGetScore(this Hashtable hashtable, out int score)
{
if (hashtable[ScoreKey] is int value)
{
score = value;
return true;
}
score = 0;
return false;
}
// プレイヤーのスコアを取得する
public static int GetScore(this Player player)
{
player.CustomProperties.TryGetScore(out int score);
return score;
}
// (相手に弾を当てた)プレイヤーのカスタムプロパティを更新する
public static void OnDealDamage(this Player player)
{
hashtable[ScoreKey] = player.GetScore() + 100; // スコアを増やす
player.SetCustomProperties(hashtable);
hashtable.Clear();
}
// (Hashtableに)プレイヤーの色相値があれば取得する
public static bool TryGetHue(this Hashtable hashtable, out float hue)
{
if (hashtable[HueKey] is float value)
{
hue = value;
return true;
}
hue = -1f;
return false;
}
// プレイヤーの色相値があれば取得する
public static bool TryGetHue(this Player player, out float hue)
{
return player.CustomProperties.TryGetHue(out hue);
}
// (相手の弾に当たった)プレイヤーのカスタムプロパティを更新する
public static void OnTakeDamage(this Player player)
{
hashtable[HueKey] = Random.value; // 色相値をランダムに変化させる
player.SetCustomProperties(hashtable);
hashtable.Clear();
}
}
GamePlayerのスクリプトをこちらを踏まえて改変する。
コードは見やすくなったがエラーは変わらず出ている状況。
using Photon.Pun;
using Photon.Realtime;
using TMPro;
using UnityEngine;
using Hashtable = ExitGames.Client.Photon.Hashtable;
[RequireComponent(typeof(Renderer))]
public class GamePlayer : MonoBehaviourPunCallbacks
{
[SerializeField]
private TextMeshPro nameLabel = default;
private ProjectileManager projectileManager;
private Renderer spriteRenderer;
private void Awake()
{
projectileManager = GameObject.FindWithTag("ProjectileManager").GetComponent<ProjectileManager>();
spriteRenderer = GetComponent<Renderer>();
}
private void Start()
{
PhotonNetwork.LocalPlayer.NickName = "Player";
var customProperties = photonView.Owner.CustomProperties;
// プレイヤー名の横にスコアを表示する
int score = photonView.Owner.GetScore();
nameLabel.text = $"{photonView.Owner.NickName}({score.ToString()})";
// 色相値が設定されていたら、スプライトの色を変化させる
if (photonView.Owner.TryGetHue(out float hue))
{
spriteRenderer.material.color = Color.HSVToRGB(hue, 1f, 1f);
}
}
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, transform.position, angle);
}
}
}
// [PunRPC]属性をつけると、RPCでの実行が有効になる
[PunRPC]
private void FireProjectile(Vector3 origin, float angle, PhotonMessageInfo info)
{
int timestamp = info.SentServerTimestamp;
projectileManager.Fire(timestamp, photonView.OwnerActorNr, origin, angle, timestamp);
}
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);
if (photonView.IsMine)
{
PhotonNetwork.LocalPlayer.OnTakeDamage();
}
else if (ownerId == PhotonNetwork.LocalPlayer.ActorNumber)
{
PhotonNetwork.LocalPlayer.OnDealDamage();
}
}
// プレイヤーのカスタムプロパティが更新された時に呼ばれるコールバック
public override void OnPlayerPropertiesUpdate(Player target, Hashtable changedProps)
{
if (target.ActorNumber != photonView.OwnerActorNr) { return; }
// スコアが更新されていたら、スコア表示も更新する
if (changedProps.TryGetScore(out int score))
{
nameLabel.text = $"{photonView.Owner.NickName}({score.ToString()})";
}
// 色相値が更新されていたら、スプライトの色を変化させる
if (changedProps.TryGetHue(out float hue))
{
spriteRenderer.material.color = Color.HSVToRGB(hue, 1f, 1f);
}
}
}
エラーは検証中なので、ルーム設定の同期などを確認していく。