Notifications
Article
PUN2で始めるオンラインゲーム開発入門【その4】
Updated 25 days ago
1.4 K
0
カスタムプロパティでいろいろな設定を同期させよう
【その1】|【その2】|【その3】|【その4】|【その5】

プレイヤー名を同期させよう

自身のプレイヤー名はPhotonNetworkからいつでも設定・変更できます。
PhotonNetwork.LocalPlayer.NickName = "Player";
サーバー接続直後から他プレイヤーが自身のプレイヤー名を取得できるようにするため、サーバー接続前にあらかじめ設定しておくのがオススメです。プレイヤー名は、他プレイヤーと重複しても問題ありません。
プレイヤーのネットワークオブジェクト(GamePlayer)にテキストを追加して、そこにプレイヤー名を表示できるようにしてみましょう。プレイヤーのネットワークオブジェクトは、その3で弾の発射と、当たり判定(弾を受ける側)の同期までが済んでいるものとします。
using Photon.Pun; using TMPro; using UnityEngine; public class GamePlayer : MonoBehaviourPunCallbacks { [SerializeField] private TextMeshPro nameLabel = default; private ProjectileManager projectileManager; private void Awake() { projectileManager = GameObject.FindWithTag("ProjectileManager").GetComponent<ProjectileManager>(); } private void Start() { nameLabel.text = photonView.Owner.NickName; } // 省略 }

プレイヤーの設定を同期させよう

プレイヤーはPhotonのカスタムプロパティ(Custom Properties)と呼ばれる仕組みによって、プレイヤー名以外でも好きな値を設定することができます。カスタムプロパティで使われるHashtable型は、任意の文字列のキーと任意のデータ型の値のペアを要素とするコレクションです。(これはほぼDictionary<string, object>型と同じものになります)

カスタムプロパティの取得

プレイヤーのカスタムプロパティの値を取得する場合は、Player.CustomProperties.TryGetValue()を使いましょう。このメソッドは、指定したキーに値が設定されているかどうかを返すと同時に、値が設定されている場合はout引数にその値が入ります。値はobject型でしか取得できないため、適切な型にキャストしてから利用します。
if (PhotonNetwork.LocalPlayer.CustomProperties.TryGetValue("Score", out object scoreObject)) { int score = (int)scoreObject; }
以下のようなコードで値を取得することも可能ですが、値が設定されていない場合はエラーになるので注意しましょう。
int score = (int)PhotonNetwork.LocalPlayer.CustomProperties["Score"];

カスタムプロパティの設定(更新・削除)

プレイヤーのカスタムプロパティの値を設定する場合は、Player.SetCustomProperties()を使って、更新するカスタムプロパティの値を他プレイヤーへ送信して同期する必要があります。直接Player.CustomPropertiesを変更しても値は同期されないので注意してください。値にnullを設定することで、値を削除することもできます。
var hashtable = new ExitGames.Client.Photon.Hashtable(); hashtable["Score"] = 100; PhotonNetwork.LocalPlayer.SetCustomProperties(hashtable);
MonoBehaviourPunCallbacksを継承しているスクリプトは、プレイヤーのカスタムプロパティが更新された時のコールバックを受け取ることができます。
public override void OnPlayerPropertiesUpdate(Player target, Hashtable changedProps) { // 更新されたキーと値のペアを、デバッグログに出力する foreach (var p in changedProps) { Debug.Log($"{p.Key}: {p.Value}"); } }

プレイヤーのスコアを同期させよう

プレイヤーのスクリプトに、カスタムプロパティの設定を追加して、他プレイヤーと同期させてみましょう。
  1. int型のスコアのデータ"Score" - 弾を当てたプレイヤーはスコアが100増える
  2. float型の色相値のデータ"Hue" - 弾に当たったプレイヤーのスプライトはランダムに色が変化する
using Photon.Pun; using Photon.Realtime; using TMPro; using UnityEngine; using Hashtable = ExitGames.Client.Photon.Hashtable; [RequireComponent(typeof(SpriteRenderer))] public class GamePlayer : MonoBehaviourPunCallbacks { [SerializeField] private TextMeshPro nameLabel = default; private ProjectileManager projectileManager; private SpriteRenderer spriteRenderer; private void Awake() { projectileManager = GameObject.FindWithTag("ProjectileManager").GetComponent<ProjectileManager>(); spriteRenderer = GetComponent<SpriteRenderer>(); } private void Start() { var customProperties = photonView.Owner.CustomProperties; // プレイヤー名の横にスコアを表示する bool hasScore = customProperties.TryGetValue("Score", out object scoreObject); int score = (hasScore) ? (int)scoreObject : 0; // まだスコアが設定されていないなら0にする nameLabel.text = $"{photonView.Owner.NickName}({score.ToString()})"; // 色相値が設定されていたら、スプライトの色を変化させる if (customProperties.TryGetValue("Hue", out object hueObject)) { spriteRenderer.color = Color.HSVToRGB((float)hueObject, 1f, 1f); } } // 省略 [PunRPC] private void HitByProjectile(int projectileId, int ownerId) { projectileManager.Remove(projectileId, ownerId); if (photonView.IsMine) { // 弾に当たったのが自身のオブジェクトなら、自身の色相値を更新する var hashtable = new Hashtable(); hashtable["Hue"] = Random.value; PhotonNetwork.LocalPlayer.SetCustomProperties(hashtable); } else if (ownerId == PhotonNetwork.LocalPlayer.ActorNumber) { // (他プレイヤーのオブジェクトが)自身の弾に当たったなら、自身のスコアを更新する var customProperties = PhotonNetwork.LocalPlayer.CustomProperties; bool hasScore = customProperties.TryGetValue("Score", out object scoreObject); int score = (hasScore) ? (int)scoreObject : 0; var hashtable = new Hashtable(); hashtable["Score"] = score + 100; PhotonNetwork.LocalPlayer.SetCustomProperties(hashtable); } } // プレイヤーのカスタムプロパティが更新された時に呼ばれるコールバック public override void OnPlayerPropertiesUpdate(Player target, Hashtable changedProps) { if (target.ActorNumber != photonView.OwnerActorNr) { return; } // スコアが更新されていたら、スコア表示を更新する if (changedProps.TryGetValue("Score", out object scoreObject)) { int score = (int)scoreObject; nameLabel.text = $"{photonView.Owner.NickName}({score.ToString()})"; } // 色相値が更新されていたら、スプライトの色を変化させる if (changedProps.TryGetValue("Hue", out object hueObject)) { spriteRenderer.color = Color.HSVToRGB((float)hueObject, 1f, 1f); } } }

拡張メソッドのすすめ

カスタムプロパティは自由に値を設定できる反面、間違った文字列(誤字・脱字)や想定と違うデータ型を設定してしまってもコンパイルエラーが発生しないため、そのまま使ってしまうとバグや実行時のエラーの原因になる可能性が高いです。
カスタムプロパティを安全に使う方法の一つとして、この記事では以下のように、カスタムプロパティを取得・設定するクラス(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) { bool result = hashtable.TryGetValue(ScoreKey, out object value); score = (result) ? (int)value : 0; return result; } // プレイヤーのスコアを取得する 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) { bool result = hashtable.TryGetValue(HueKey, out object value); hue = (result) ? (float)value : -1f; return result; } // プレイヤーの色相値があれば取得する 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(); } }
拡張メソッドを使って、プレイヤーのスクリプトを修正すると、以下のようになります。文字列でキーを指定したり、値をキャストしたりする必要が無くなり、コードが以前よりシンプルに書けるようになったことに着目してみてください。
using Photon.Pun; using Photon.Realtime; using TMPro; using UnityEngine; using Hashtable = ExitGames.Client.Photon.Hashtable; [RequireComponent(typeof(SpriteRenderer))] public class GamePlayer : MonoBehaviourPunCallbacks { // 省略 private void Start() { // プレイヤー名の横にスコアを表示する int score = photonView.Owner.GetScore(); nameLabel.text = $"{photonView.Owner.NickName}({score.ToString()})"; // 色相値が設定されていたら、スプライトの色を変化させる if (photonView.Owner.TryGetHue(out float hue)) { spriteRenderer.color = Color.HSVToRGB(hue, 1f, 1f); } } // 省略 [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.color = Color.HSVToRGB(hue, 1f, 1f); } } }

ルームの設定を同期させよう

カスタムプロパティはルームに設定することもできます。ルームに参加しているプレイヤー全員と、任意の値を同期させたい場合に活用できます。ここでは、以下の2つの値を同期させてみましょう。
  1. string型のルーム名のデータ"DisplayName" - ルーム名を表示する時に使う
  2. int型のゲーム開始時刻のデータ"StartTime" - ゲーム中の経過時間を計算する時に使う
まずは、プレイヤーのカスタムプロパティと同じように、ルームのカスタムプロパティを取得・設定するクラス(GameRoomProperty)を作成します。
using Photon.Realtime; using Hashtable = ExitGames.Client.Photon.Hashtable; public static class GameRoomProperty { private const string KeyDisplayName = "DisplayName"; // 表示用ルーム名のキーの文字列 private const string KeyStartTime = "StartTime"; // ゲーム開始時刻のキーの文字列 private static Hashtable hashtable = new Hashtable(); // ルームの初期設定オブジェクトを作成する public static RoomOptions CreateRoomOptions(string displayName) { return new RoomOptions() { // カスタムプロパティの初期設定 CustomRoomProperties = new Hashtable() { { KeyDisplayName, displayName } }, // ロビーからカスタムプロパティを取得できるようにする CustomRoomPropertiesForLobby = new string[] { KeyDisplayName } }; } // 表示用ルーム名を取得する public static string GetDisplayName(this Room room) { return (string)room.CustomProperties[KeyDisplayName]; } // ゲーム開始時刻が設定されているか調べる public static bool HasStartTime(this Room room) { return room.CustomProperties.ContainsKey(KeyStartTime); } // ゲーム開始時刻があれば取得する public static bool TryGetStartTime(this Room room, out int timestamp) { bool result = room.CustomProperties.TryGetValue(KeyStartTime, out var value); timestamp = (result) ? (int)value : 0; return result; } // ゲーム開始時刻を設定する public static void SetStartTime(this Room room, int timestamp) { hashtable[KeyStartTime] = timestamp; room.SetCustomProperties(hashtable); hashtable.Clear(); } }
ルーム作成時にRoomOptionsから、ルームのカスタムプロパティの初期設定を渡します。PhotonNetwork.JoinOrCreateRoom()の第一引数で指定するルーム名は、名前の重複は許されていません。ルームのカスタムプロパティで、表示用のルーム名を別に設定することで、重複しても問題がないルーム名を使うことができるようになります。
string displayName = $"{PhotonNetwork.NickName}の部屋"; PhotonNetwork.JoinOrCreateRoom("room", GameRoomProperty.CreateRoomOptions(displayName), TypedLobby.Default);
ゲームの開始時刻を、ルーム参加直後に設定します。ルーム作成時に接続しているマスターサーバーと、ルーム参加時に接続しているゲームサーバーは、別のサーバーなので、サーバー時刻を初期設定に含めると問題が発生しまうためです。 マスタークライアント(Master Client)は、ルームに参加しているプレイヤーの代表で、自身がマスタークライアントかどうかはPhotonNetwork.IsMasterClientで確認できます。プレイヤーの1人だけに実行させたい処理を行う際に便利です。
public override void OnJoinedRoom() { var v = new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f)); PhotonNetwork.Instantiate("GamePlayer", v, Quaternion.identity); // 現在のサーバー時刻を、ゲームの開始時刻に設定する if (PhotonNetwork.IsMasterClient && !PhotonNetwork.CurrentRoom.HasStartTime()) { PhotonNetwork.CurrentRoom.SetStartTime(PhotonNetwork.ServerTimestamp); } }
シーンにテキストを追加して、ゲームの経過時間を表示できるようにしてみましょう。
using Photon.Pun; using TMPro; using UnityEngine; [RequireComponent(typeof(TextMeshProUGUI))] public class GameRoomHUD : MonoBehaviour { private TextMeshProUGUI timeLabel; private void Awake() { timeLabel = GetComponent<TextMeshProUGUI>(); } private void Update() { // まだルームに参加していない時は更新しない if (!PhotonNetwork.InRoom) { return; } // まだゲーム開始時刻が設定されていない時は更新しない if (!PhotonNetwork.CurrentRoom.TryGetStartTime(out int timestamp)) { return; } // ゲーム開始時刻からの経過時間を求めて、テキスト表示する float elapsedTime = Mathf.Max(unchecked(PhotonNetwork.ServerTimestamp - timestamp) / 1000f); timeLabel.text = elapsedTime.ToString("f2"); // 小数点以下2桁表示 } }

カスタムプロパティを更新する場合の注意点

カスタムプロパティは、相対的な値を設定したりすることができないため、並行処理に関連する問題が発生します。 どういうことかというと、例えばルームのカスタムプロパティで、チームスコアが100に設定されていたとします。2人のプレイヤーが、ほぼ同時にチームスコアを10増やそうとした時、それぞれがチームスコアを110に更新することになります。その結果、正しいチームスコアの120になりません。
このような問題を回避するため、誰がカスタムプロパティを更新するのか、方針を決めておくと良いでしょう。 以下の方針はあくまで一例です。
  • プレイヤーのカスタムプロパティは、かならずプレイヤー自身が更新するようにする
  • ルームのカスタムプロパティは、かならずマスタークライアントが更新するようにする

以下の★パートは中級者向けです。わからない部分は読み飛ばして、慣れた頃に読み返すのがオススメです。

★カスタムプロパティのキーについて

ネットワーク上の通信では、文字列は文字数の分だけデータのサイズが増えるという問題があります。カスタムプロパティのキーは文字列なので、可能な限り短い文字列にするのが望ましいです。既にカスタムプロパティのキーの文字列を定数で定義しているなら、その文字列を短くするだけで通信量を削減できます。
private const string ScoreKey = "s"; private const string HueKey = "h";

★効率的なカスタムプロパティの設定

SetCustomProperties()は呼び出すごとにデータの送信が行われます。複数のカスタムプロパティの値を同時に設定する場合は、値を可能な限りHashtableにまとめてからSetCustomProperties()で送信しましょう。通信量の削減に加えて、カスタムプロパティが更新された時に呼ばれるコールバックが無駄に何度も実行されずに済みます。

★任意のデータ型を通信できるようにしよう

Photonがデフォルトでサポートしていないデータ型でも、(データ型からバイト列への)シリアライズ処理と(バイト列からデータ型への)デシリアライズ処理を実装して登録することで、カスタムタイプ(Custom Types)として通信することができます。Vector3型などはカスタムタイプとして登録されていて、以下の場所から実装コードを確認できます。

Color型のシリアライズ処理

Color型をカスタムタイプとして登録してみましょう。Color型のRGBA値はfloat型なので、そのままバイト列に書き込むためには、それぞれ4バイト(合計で16バイト)が必要になります。それに対して、Color32型のRGBA値はbyte型でそれぞれ1バイト(合計で4バイト)で済むため、型変換することによって通信量を削減しています。
using ExitGames.Client.Photon; using UnityEngine; public static class MyCustomTypes { private static readonly byte[] bufferColor = new byte[4]; // カスタムタイプを登録するメソッド(起動時に一度だけ呼び出す) public static void Register() { PhotonPeer.RegisterType(typeof(Color), 1, SerializeColor, DeserializeColor); } // Color型をバイト列に変換して送信データに書き込むメソッド private static short SerializeColor(StreamBuffer outStream, object customObject) { Color32 color = (Color)customObject; lock (bufferColor) { bufferColor[0] = color.r; bufferColor[1] = color.g; bufferColor[2] = color.b; bufferColor[3] = color.a; outStream.Write(bufferColor, 0, 4); } return 4; // 書き込んだバイト数を返す } // 受信データからバイト列を読み込んでColor型に変換するメソッド private static object DeserializeColor(StreamBuffer inStream, short length) { Color32 color = new Color32(); lock (bufferColor) { inStream.Read(bufferColor, 0, 4); color.r = bufferColor[0]; color.g = bufferColor[1]; color.b = bufferColor[2]; color.a = bufferColor[3]; } return (Color)color; } }

Vector2Int型のシリアライズ処理

Vector2Int型もカスタムタイプとして登録してみましょう。xyの値はint型で、それぞれ4バイト(合計で8バイト)になります。Photonでは、int型の値をバイト列に書き込むProtocol.Serialize()が利用できるので、それを使いましょう。
using ExitGames.Client.Photon; using UnityEngine; public static class MyCustomTypes { public static readonly byte[] bufferColor = new byte[4]; public static readonly byte[] bufferVector2Int = new byte[8]; public static void Register() { PhotonPeer.RegisterType(typeof(Color), 1, SerializeColor, DeserializeColor); PhotonPeer.RegisterType(typeof(Vector2Int), 2, SerializeVector2Int, DeserializeVector2Int); } // 省略 private static short SerializeVector2Int(StreamBuffer outStream, object customObject) { Vector2Int v = (Vector2Int)customObject; int index = 0; lock (bufferVector2Int) { Protocol.Serialize(v.x, bufferVector2Int, ref index); Protocol.Serialize(v.y, bufferVector2Int, ref index); outStream.Write(bufferVector2Int, 0, index); } return (short)index; } private static object DeserializeVector2Int(StreamBuffer inStream, short length) { int x, y; int index = 0; lock (bufferVector2Int) { inStream.Read(bufferVector2Int, 0, length); Protocol.Deserialize(out x, bufferVector2Int, ref index); Protocol.Deserialize(out y, bufferVector2Int, ref index); } return new Vector2Int(x, y); } }

(カスタムタイプのシリアライズ処理のための)組み込み型のシリアライズ処理

Protocol.Serialize()で用意されているバイト列に変換できるデータ型は、short型、int型、float型のみです。それ以外のデータ型をバイト列に変換したい場合は、独自にシリアライズ処理を実装する必要があります。
独自の組み込み型のシリアライズ処理を定義するクラス(MyProtocol)を作成して、カスタムタイプのシリアライズ処理で使用できるようにしましょう。テンプレートとしてbyte型のシリアライズ処理のメソッドは、以下のようになります。
public static partial class MyProtocol { public static void Serialize(byte value, byte[] target, ref int offset) { target[offset] = value; offset++; } public static void Deserialize(out byte value, byte[] source, ref int offset) { value = source[offset]; offset++; } }
double型など、2バイト以上のデータ型をバイト列へ変換する際にはBitConverterクラスを利用すると良いでしょう。BitConverterは自身の環境のバイトオーダーでしか処理を行えないため、IPAddressクラスのバイトオーダーを変換するメソッドを使って、異なるバイトオーダーの環境の間でも正しく通信できるようにする必要があります。
using System; using System.Net; public static partial class MyProtocol { private const int SizeDouble = sizeof(double); public static void Serialize(double value, byte[] target, ref int offset) { long host = BitConverter.DoubleToInt64Bits(value); long network = IPAddress.HostToNetworkOrder(host); byte[] bytes = BitConverter.GetBytes(network); Buffer.BlockCopy(bytes, 0, target, offset, SizeDouble); offset += SizeDouble; } public static void Deserialize(out double value, byte[] source, ref int offset) { long host = BitConverter.ToInt64(source, offset); long network = IPAddress.NetworkToHostOrder(host); value = BitConverter.Int64BitsToDouble(network); offset += SizeDouble; } }
string型は、送信する文字数によってサイズが変わります。可変長のデータは、バイト列の最初の1~4バイトにサイズを入れて、バイト列からサイズを取得できるようにしましょう。以下のコードでは、それほど長い文字列は通信しない想定で、最初の1バイトのみにサイズを入れるようにしています。これで最大255バイト分の文字列が通信できます。
using System.Text; using UnityEngine; public static partial class MyProtocol { // UTF-8でエンコード・デコードできない文字は空文字に置き換える設定にしておく private static readonly Encoding encoding = Encoding.GetEncoding( "utf-8", new EncoderReplacementFallback(string.Empty), new DecoderReplacementFallback(string.Empty) ); public static void Serialize(string value, byte[] target, ref int offset) { int byteCount = encoding.GetBytes(value, 0, value.Length, target, offset + 1); byte size = (byte)Mathf.Min(byteCount, byte.MaxValue); target[offset] = size; offset += size + 1; } public static void Deserialize(out string value, byte[] source, ref int offset) { byte size = source[offset]; value = encoding.GetString(source, offset + 1, size); offset += size + 1; } }

★メッセージサイズの最適化

Photonの1メッセージの最大サイズは1200バイトです。複数の小さい送信データは、可能な限り1つのメッセージにまとめられて送信されます。大きい送信データは、複数のメッセージに分割して送信されます。データのサイズを減らすことで、通信のパフォーマンスを改善できます。(もちろん「早すぎる最適化は諸悪の根源である」ことも留意しておきましょう)

小さいデータ型を使おう

  • int型の値が小さい範囲なら、short型かbyte型を使いましょう
  • float型の値の小数点以下を切り捨てて問題がないなら、int型を使いましょう
  • 2DゲームなどでVector3型のzの値が不要なら、Vector2型を使いましょう
  • 座標がグリッドで表現されているなら、グリッド番号を使いましょう (例えば、将棋の盤面は計81マスなので、Vector2型の代わりにbyte型の0~80で座標を指定できます)
  • Quarternion型はオイラー角でも問題がないなら、Quaternion.eulerAnglesVector3型を使いましょう
  • 2Dゲームなどで回転軸が決まっているなら、Quarternion型の代わりにfloat型を使いましょう

カスタムタイプのメリット・デメリット

Photonがデフォルトでサポートしているデータ型の型情報は1バイトに対して、カスタムタイプの型情報は4バイトが必要になります。カスタムタイプでシリアライズする値が少ない場合は、無駄にサイズが増えてしまう可能性があります。 例えば、Photonでカスタムタイプとして登録されているPlayer型は、型情報を含めて8バイトですが、内部的にはint型のプレイヤーIDしかシリアライズしていないので、直接int型でプレイヤーIDを通信すれば5バイトで済みます。
逆にカスタムタイプでシリアライズする値が多い場合は、値の数にかかわらず型情報が4バイトで済むため、データのサイズを減らせる可能性があります。ただし、カスタムタイプを登録して多くの値を通信することを検討する前に、そもそも多くの値を通信せずに済ませる方法がないかを、まず模索してみることをオススメします。

値の精度を落とす

例えば、角度(degree)はfloat型で通信すると、型情報含めて5バイトが必要になります。小数点以下を切り捨てて、short型で通信すれば、3バイトになります。角度を0°~360°の範囲に正規化し、送信時には値を半分にして、受信時には値を倍にして戻すようにすれば、byte型の範囲(0~255)で通信できるようになるため、サイズを2バイトに減らせます。
using UnityEngine; public static partial class MyEncoder { // 角度(degree)をbyte型のコードに変換するメソッド public static byte EncodeAngle(float degree) { // 角度を0°~359°に正規化する int normalized = Mathf.FloorToInt(degree + 36000f) % 360; return (byte)(normalized / 2); } // byte型のコードを角度(degree)に変換するメソッド public static float DecodeAngle(byte code) { return code * 2f; } }

ビット演算によるデータ圧縮

byte型は、要素数8のビット列とみなすことができます。同じようにint型は、要素数4のバイト列(要素数32のビット列)とみなすことができます。ビット演算を使って、1つの値の中に複数の値を詰め込めば、サイズを減らすことができます。
Color型とVector2Int型を、カスタムタイプには登録せずに、Photonでサポートしてるデータ型で通信できるようにしてみましょう。カスタムタイプの型情報に必要だった4バイトが、1バイトのみで済むようになります。
using UnityEngine; public static partial class MyEncoder { // Color型をint型のコードに変換するメソッド public static int EncodeColor(Color color) { Color32 color32 = color; return (color32.r << 24) | (color32.g << 16) | (color32.b << 8) | color32.a; } // int型のコードをColor型に変換するメソッド public static Color DecodeColor(int code) { return new Color32( (byte)((code >> 24) & 0xFF), (byte)((code >> 16) & 0xFF), (byte)((code >> 8) & 0xFF), (byte)(code & 0xFF) ); } // Vector2Int型をlong型のコードに変換するメソッド public static long EncodeVector2Int(Vector2Int vector2) { return ((long)vector2.x << 32) | (long)vector2.y; } // long型のコードをVector2Int型に変換するメソッド public static Vector2Int DecodeVector2Int(long code) { return new Vector2Int( (int)((code >> 32) & 0xFFFFFFFF), (int)(code & 0xFFFFFFFF) ); } }
応用例として、以下のようなプレイヤーのステータスを通信することを考えてみましょう。
  1. 性別(0:男性、1:女性)- 必要なビット数1(範囲 : 0~1)
  2. レベル(最大99)- 必要なビット数7(範囲 : 0~127)
  3. HP(最大9999)- 必要なビット数14(範囲 : 0~16383)
  4. MP(最大999)- 必要なビット数10(範囲 : 0~1023)
各ステータスをそのままint型で通信すると、型情報を含めて20バイトが必要です。性別とレベルをbyte型、HPとMPをshort型で通信するようにすれば、10バイトに減らせます。さらに、各パラメーターが取りうる値の範囲を表現できるビット数を求めると、ちょうど合計32ビットになるので、1つのint型に全部を詰め込むことで、5バイトまで圧縮できます。
public static partial class MyEncoder { // プレイヤーのステータスをint型のコードに変換するメソッド public static int EncodePlayerStatus(int gender, int lv, int hp, int mp) { return (gender << 31) | (lv << 24) | (hp << 10) | mp; } // int型のコードをプレイヤーのステータスに変換するメソッド(戻り値はタプルで返す) public static (int gender, int lv, int hp, int mp) DecodePlayerStatus(int code) { return ( (code >> 31) & 0b_1, (code >> 24) & 0b_11_11111, (code >> 10) & 0b_1111_11111_11111, code & 0b_11111_11111 ); } }

次の記事

作成中です。 PUN2で始めるオンラインゲーム開発入門【その5】
o8que
Ghost - Programmer
4
Comments