[Unity6]Photon Fusionのサーバーモードでエアホッケー的な物を作ってみたい(移動〜Unity判定編2/3)
はじめに
前回では、導入から生成を実装してみたので、今回はエアホッケー部分の移動や判定を実装してみます。
前回からの変更点(スクリプト以外)
- サーバークライアントでプロジェクトを分けるのではなく同じプロジェクトにしました。
- サーバー用のシーンとクライアント用シーンで切り分けるようにしています。
ServerScene
GameScene
-
NetworkRunner
のスクリプトを1つにまとめました。
- インスペクターで変更できるisServer
フラグを用意して処理分けをしています。(詳細は後ほど後述します) - 前回作成した壁に
BoxCollider
をアタッチしたものをServerScene
とGameScene
に作りました。
- 左右の壁はトリガーにしています。
-
Canvas
で表示等をしていた所をSprite
で表示するように変更しました。 -
UniRx
をインポートしました。
前回からの変更点(スクリプト)
Scrips配下
今回実装するものも混ざっているので、前回分のNetworkRunnerLauncher
とServerRunnerLogic
部分の変更点です。
Player.cs
に関してはCanvas
からSprite
にしたので全て変更になってます。
NetworkRunnerLauncher.cs
-
NetworkRunner
の部分を統一して、サーバーかクライアントを判定してNetworkRunner
を作成するようにしました。 - 外側から
NetworkRunner
を参照できるようにシングルトン実装にしました。 - コールバックの中身の処理を別の関数として分けました。
- ballPrefabやplayerPrefabは後の解説で作成します。
using UnityEngine;
using Fusion;
using System.Collections.Generic;
using System;
using Fusion.Sockets;
/// <summary>
/// ネットワークランナーランチャー
/// </summary>
public class NetworkRunnerLauncher : MonoBehaviour, INetworkRunnerCallbacks
{
[SerializeField]
[Header("プレイヤーのプレハブ")]
NetworkObject playerPrefab;
[SerializeField]
[Header("ボールのプレハブ")]
NetworkObject ballPrefab;
[SerializeField]
[Header("サーバーかクライアントか")]
bool IsServer;
[Header("ネットワークランナー")]
public NetworkRunner runner;
// インスタンス
private static NetworkRunnerLauncher instance;
// インスタンス
public static NetworkRunnerLauncher Instance
{
get
{
if (instance == null)
{
instance = FindFirstObjectByType<NetworkRunnerLauncher>();
}
return instance;
}
}
/// <summary>
/// 初期化
/// </summary>
async void Start()
{
// コールバックを受け取る
runner.AddCallbacks(this);
// サーバーなら
if (IsServer)
{
// デフォルトのシーン
NetworkSceneInfo sceneInfo = default(NetworkSceneInfo);
// サーバーモードで起動
StartGameArgs startGameArgs = new StartGameArgs()
{
GameMode = GameMode.Server, // サーバーモードで動作
SessionName = "GameServer", // セッション名(任意)
Scene = sceneInfo, // default の NetworkSceneInfo を設定
};
// サーバーdゲームを開始
StartGameResult result = await runner.StartGame(startGameArgs);
// リザルトがOKじゃなかったら
if (!result.Ok)
{
Debug.LogError($"サーバー起動に失敗: {result.ShutdownReason}");
}
// リザルトがOKなら
else
{
Debug.Log("サーバーが正常に起動しました。");
}
}
// クライアントなら
else
{
// クライアント接続する場合
StartGameArgs startGameArgs = new StartGameArgs()
{
GameMode = GameMode.Client, // クライアントモード
SessionName = "GameServer" // サーバーのセッション名と一致させる
};
// クライアントを開始
StartGameResult result = await runner.StartGame(startGameArgs);
// リザルトがOKじゃなかったら
if (!result.Ok)
{
Debug.LogError($"サーバー接続失敗: {result.ShutdownReason}");
}
// リザルトがOKなら
else
{
Debug.Log("サーバーに接続できました。");
}
}
}
/// <summary>
/// プレイヤーが参加した時
/// </summary>
/// <param name="networkRunner">ネットワークランナー</param>
/// <param name="playerRef">接続したプレイヤー</param>
public void OnPlayerJoined(NetworkRunner networkRunner, PlayerRef playerRef)
{
Debug.Log($"Connect Server IsServerFlag:{runner.IsServer}");
// サーバーなら
if (runner.IsServer)
{
// サーバーの接続ロジック
ServerRunnerLogic.JoinplayerLogic(networkRunner, playerPrefab, ballPrefab, playerRef);
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~他のコールバック関数~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
ServerRunnerLogic.cs
-
NetworkRunner
のコールバックから実際に処理をしている部分を実装しています。 - ballPrefabやプレイヤースクリプトは後の解説で作成します。
using UnityEngine;
using Fusion;
using System.Linq;
/// <summary>
/// サーバーのランナーロジック
/// </summary>
public static class ServerRunnerLogic
{
/// <summary>
/// プレイヤー接続ロジック
/// </summary>
/// <param name="networkRunner">ネットワークランナー</param>
/// <param name="playerPrefab">プレイヤープレハブ</param>
/// <param name="playerRef">プレイヤー情報</param>
public static void JoinplayerLogic(NetworkRunner networkRunner, NetworkObject playerPrefab, NetworkObject ballPrefab, PlayerRef playerRef)
{
// 現在のプレイヤー数を取得
int playerCount = networkRunner.ActivePlayers.Count();
Debug.Log("playerCount" + playerCount.ToString());
// プレイヤーのネットワークオブジェクト
NetworkObject player = null;
// プレイヤー生成
player = networkRunner.Spawn(playerPrefab, new Vector3(0.0f, 0.0f, 0.0f), Quaternion.identity, playerRef);
// プレイヤーが生成されていたら
if (player != null)
{
player.GetComponent<Player>().RpcSetPlayerIndex(playerCount);
player.GetComponent<Player>().RpcInit();
}
// 2人目のプレイヤー
if (playerCount == 2)
{
// ボール生成
networkRunner.Spawn(ballPrefab, new Vector3(0.0f, 0.0f, 0.0f), Quaternion.identity);
}
}
}
衝突処理のNetworkRunner事前準備
衝突処理をさせるのには通常通りRigidbody
やCollider
を使いますが、Photon Fusion
ではNetworkRunner
をアタッチしているゲームオブジェクトに対してRunnerSimulatePhysics2D
のアタッチが必要になります。
クライアント側の設定
今回、クライアント側では衝突の計算をさせない処理にしたいのでClientPhysicsSimulation
をSync Transform
にする必要があります、項目が色々あると思いますがSync Transform
は衝突の計算をせずサーバーからの位置を同期させるだけのモードです。
サーバー側の設定
サーバー側では衝突の計算をしたいのでClientPhysicsSimulation
をSimulation Always
にしますSimulation Always
は衝突の計算をし続けるモードです。
プレイヤーPrefab
Sprite
に下記をアタッチします。
-
BoxCollider2D
- 画像に合わせて判定作成 -
Rigidbody2D
-Dynamic
で位置のy以外を固定 -
NetworkRigidbody2D
(そのまま) - プレイヤースクリプト
プレイヤースクリプト全体
using UnityEngine;
using Fusion;
/// <summary>
/// プレイヤー
/// </summary>
public class Player : NetworkBehaviour
{
[SerializeField]
[Header("リジットボディ")]
Rigidbody2D rigidbody2D;
[Networked]
[SerializeField]
[Header("プレイヤーのインデックス")]
int playerIndex { get; set; } = 0;
/// <summary>
/// Photonのスポーン時
/// </summary>
public override void Spawned()
{
// サーバーじゃないなら
if (!NetworkRunnerLauncher.Instance.runner.IsServer)
{
// 物理挙動をシミュレートする
NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), false);
}
// サーバーなら
else
{
// 物理挙動をシミュレートする
NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), true);
}
}
/// <summary>
/// Unityの初期化
/// </summary>
private void Start()
{
// プレイヤーのインデックスが設定されていたら
if (playerIndex != 0)
{
// Playerを見つける
GameObject playerParent = GameObject.Find("Player");
// Playerがあったら
if (playerParent != null)
{
// Playerが見つかった場合、親を設定
transform.SetParent(playerParent.transform);
}
// なかったら
else
{
Debug.LogError("Playerの親が見つかりませんでした。");
}
// プレイヤー1なら
if (playerIndex == 1)
{
// 親の変更で位置とスケールがおかしくなるので修正
transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
transform.position = new Vector3(-8.0f, 0.0f, 0.0f);
}
// プレイヤー2なら
else if (playerIndex == 2)
{
// 親の変更で位置とスケールがおかしくなるので修正
transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
transform.position = new Vector3(8.0f, 0.0f, 0.0f);
}
}
}
/// <summary>
/// RPCの初期化
/// </summary>
[Rpc]
public void RpcInit()
{
// Playerを見つける
GameObject playerParent = GameObject.Find("Player");
// Playerがあったら
if (playerParent != null)
{
// Playerが見つかった場合、親を設定
transform.SetParent(playerParent.transform);
}
// なかったら
else
{
Debug.LogError("Playerの親が見つかりませんでした。");
}
// プレイヤー1なら
if (playerIndex == 1)
{
// 親の変更で位置とスケールがおかしくなるので修正
transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
transform.position = new Vector3(-8.0f, 0.0f, 0.0f);
}
// プレイヤー2なら
else if (playerIndex == 2)
{
// 親の変更で位置とスケールがおかしくなるので修正
transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
transform.position = new Vector3(8.0f, 0.0f, 0.0f);
}
}
/// <summary>
/// 更新
/// </summary>
public override void FixedUpdateNetwork()
{
MovePlayer();
}
/// <summary>
/// プレイヤーの移動
/// </summary>
void MovePlayer()
{
// サーバーなら
if (NetworkRunnerLauncher.Instance.runner.IsServer)
{
// プレイヤーの入力データを取得、できなかったら処理しない
if (NetworkRunnerLauncher.Instance.runner.TryGetInputForPlayer(Object.InputAuthority, out PlayerInputData inputData))
{
// プレイヤーの入力を取得
float moveSpeed = 5f;
// 入力に応じて物理移動
Vector2 moveDir = new Vector2(inputData.horizontal, inputData.vertical);
Vector2 newPos = rigidbody2D.position + moveDir * moveSpeed * NetworkRunnerLauncher.Instance.runner.DeltaTime;
// リジットボディで移動
rigidbody2D.MovePosition(newPos);
}
}
}
/// <summary>
/// プレイヤーのインデックス設定
/// </summary>
/// <param name="index">プレイヤーのインデックス</param>
[Rpc]
public void RpcSetPlayerIndex(int index)
{
// プレイヤーのインデックスが設定されていなかったら
if (playerIndex == 0)
{
playerIndex = index;
}
}
}
プレイヤースクリプト解説
変数宣言
[SerializeField]
[Header("リジットボディ")]
Rigidbody2D rigidbody2D;
[Networked]
[SerializeField]
[Header("プレイヤーのインデックス")]
int playerIndex { get; set; } = 0;
-
rigidbody2D
で移動を行うのでPlayerのrigidbody2D
をアタッチしてください。 - プレイヤーが1なのか2なのかを判定するために
int
の変数を用意します。
-[Networked]
を使用するとそのプロパティがネットワーク上で同期されます。
Photonのスポーン時
/// <summary>
/// Photonのスポーン時
/// </summary>
public override void Spawned()
{
// サーバーじゃないなら
if (!NetworkRunnerLauncher.Instance.runner.IsServer)
{
// 物理挙動をシミュレートする
NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), false);
}
// サーバーなら
else
{
// 物理挙動をシミュレートする
NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), true);
}
}
-
public override void Spawned()
関数を作成するとNetworkRunner
でSpawn
をした時の処理を作成できます。 -
NetworkRunner.IsServer
を使用して自身がサーバーなのかクライアントなのかを判定できます。 -
NetworkRunner.SetIsSimulated(NetworkObject, bool)
を使用するとそのオブジェクトの物理挙動をシミュレートするかしないかを設定できます。
UnityのStart
/// <summary>
/// Unityの初期化
/// </summary>
private void Start()
{
// プレイヤーのインデックスが設定されていたら
if (playerIndex != 0)
{
// Playerを見つける
GameObject playerParent = GameObject.Find("Player");
// Playerがあったら
if (playerParent != null)
{
// Playerが見つかった場合、親を設定
transform.SetParent(playerParent.transform);
}
// なかったら
else
{
Debug.LogError("Playerの親が見つかりませんでした。");
}
// プレイヤー1なら
if (playerIndex == 1)
{
// 親の変更で位置とスケールがおかしくなるので修正
transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
transform.position = new Vector3(-8.0f, 0.0f, 0.0f);
}
// プレイヤー2なら
else if (playerIndex == 2)
{
// 親の変更で位置とスケールがおかしくなるので修正
transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
transform.position = new Vector3(8.0f, 0.0f, 0.0f);
}
}
}
-
UnityStart
ではplayerIndex
が設定されていた場合に位置を初期化する処理を実装しています。
RPC属性のInit
/// <summary>
/// RPCの初期化
/// </summary>
[Rpc]
public void RpcInit()
{
// Playerを見つける
GameObject playerParent = GameObject.Find("Player");
// Playerがあったら
if (playerParent != null)
{
// Playerが見つかった場合、親を設定
transform.SetParent(playerParent.transform);
}
// なかったら
else
{
Debug.LogError("Playerの親が見つかりませんでした。");
}
// プレイヤー1なら
if (playerIndex == 1)
{
// 親の変更で位置とスケールがおかしくなるので修正
transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
transform.position = new Vector3(-8.0f, 0.0f, 0.0f);
}
// プレイヤー2なら
else if (playerIndex == 2)
{
// 親の変更で位置とスケールがおかしくなるので修正
transform.localScale = new Vector3(0.5f, 2.5f, 1.0f);
transform.position = new Vector3(8.0f, 0.0f, 0.0f);
}
}
-
[RPC]
をつける事によってRPC
の実装ができ、プレイヤー1が実行するとサーバーとプレイヤー2に対しても関数の実行ができます。
-RPC
の実装は関数名にRPC
を使用しないとエラーになります。 - 他の処理内容に関しては
UnityStart
と同じです。
PhotonのFixedUpdate
/// <summary>
/// PhotonFixedUpdate
/// </summary>
public override void FixedUpdateNetwork()
{
MovePlayer();
}
/// <summary>
/// プレイヤーの移動
/// </summary>
void MovePlayer()
{
// サーバーなら
if (NetworkRunnerLauncher.Instance.runner.IsServer)
{
// プレイヤーの入力データを取得、できなかったら処理しない
if (NetworkRunnerLauncher.Instance.runner.TryGetInputForPlayer(Object.InputAuthority, out PlayerInputData inputData))
{
// プレイヤーの入力を取得
float moveSpeed = 5f;
// 入力に応じて物理移動
Vector2 moveDir = new Vector2(inputData.horizontal, inputData.vertical);
Vector2 newPos = rigidbody2D.position + moveDir * moveSpeed * NetworkRunnerLauncher.Instance.runner.DeltaTime;
// リジットボディで移動
rigidbody2D.MovePosition(newPos);
}
}
}
-
FixedUpdateNetwork
を使用して更新をします。
- このUpdate
はSimulation Always
やSimulation Forward
の設定時のみ実行されます。 - サーバーで動作をさせてクライアントに反映する形なので
NetworkRunnerLauncher.Instance.runner.IsServer
でサーバーなら処理をする処理になっています。 -
NetworkRunner.TryGetInputForPlayer(Object.InputAuthority, out PlayerInputData inputData)
を使用してプレイヤーの入力を取得しMovePosition
で移動させます。
プレイヤーインデックスの設定
/// <summary>
/// プレイヤーのインデックス設定
/// </summary>
/// <param name="index">プレイヤーのインデックス</param>
[Rpc]
public void RpcSetPlayerIndex(int index)
{
// プレイヤーのインデックスが設定されていなかったら
if (playerIndex == 0)
{
playerIndex = index;
}
}
- プレイヤーのインデックス設定をサーバーの振り分けで行っているので
RPC
属性にして引数のインデックスを受け取り設定します。
ここまでの実装
ここまでの実装でNetworkRunnerLauncher
をアタッチしてplayerPrefab
に作成したプレハブをアタッチしIsServer
フラグでサーバーとクライアントを分けビルドしてサーバー1クライアント2で実行をすると、サーバーに対してプレイヤー1がとプレイヤー2が接続され左右に表示されるようになります。
プレイヤーの入力をサーバーで取得する
入力情報を保存する構造体を作成
using Fusion;
/// <summary>
/// プレイヤー入力のデータ構造体
/// </summary>
public struct PlayerInputData : INetworkInput
{
// 縦方向の入力
public float vertical;
// 横方向の入力
public float horizontal;
}
-
INetworkInput
を継承したクラスを作成、中には入力データを保存できるようにします。
NetworkRunnerLauncherで入力を受け取る
~~~~~~~~~~~~~~~~~~~~~~~~NetworkRunnerLauncherに追加~~~~~~~~~~~~~~~~~~~~~~~~
/// <summary>
/// キー入力したら
/// </summary>
/// <param name="networkRunner"></param>
/// <param name="networkInput"></param>
public void OnInput(NetworkRunner networkRunner, NetworkInput networkInput)
{
// クライアントなら
if (!runner.IsServer)
{
// サーバーの接続ロジック
ClientRunnerLogic.OnInputLogic(networkInput);
}
}
-
NetworkRunnerLauncher
クラスのコールバックの1つであるOnInput
を使用します。
中身の処理に関してはサーバーと同じく切り分けています。 - キー入力等がされると実行される関数でクライアントのみ処理したいので
!runner.IsServer
でクライアントのみ処理しています。
入力伝達処理
using UnityEngine;
using Fusion;
/// <summary>
/// クライアントのランナーロジック
/// </summary>
public static class ClientRunnerLogic
{
/// <summary>
/// 入力ロジック
/// </summary>
/// <param name="networkInput"></param>
public static void OnInputLogic(NetworkInput networkInput)
{
// 入力構造体作成
PlayerInputData data = new PlayerInputData();
// 入力を代入
data.vertical = Input.GetAxis("Vertical");
data.horizontal = Input.GetAxis("Horizontal");
// 入力を送信
networkInput.Set(data);
}
}
-
ClientRunnerLogic.OnInputLogic
では入力情報を保存する構造体を作成で作成した構造体にUnityのInput.GetAxis
から値を取得して保存しています。 -
NetworkInput.Set()
を使用してサーバーに入力データを送信しています。
- これでサーバーの更新処理でここで入力したデータをうけとれるようになります。
壁との当たり判定
壁にCollider
をアタッチすればOKです、左右はトリガーにしています。
玉との当たり判定
玉のプレハブ設定
玉のプレハブもプレイヤーと同じく、NetWorkRigidbody2D
やCollider
等を付けて終わりですが、反射等をさせたいのでPhysicsMaterial2D
を作成して画像のように設定します。
作成し終えたら
玉のスクリプト(全体)
using UnityEngine;
using Fusion;
using UniRx;
/// <summary>
/// ボール
/// </summary>
public class Ball : NetworkBehaviour
{
[SerializeField]
[Header("リジットボディ")]
Rigidbody2D rigidbody2D;
/// <summary>
/// スポーン時
/// </summary>
public override void Spawned()
{
// サーバーじゃないなら
if (!NetworkRunnerLauncher.Instance.runner.IsServer)
{
// 物理挙動をシミュレートする
NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), false);
}
// サーバーなら
else
{
// 物理挙動をシミュレートする
NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), true);
// 右側に押し出す
rigidbody2D.AddForce(Vector2.right * 5.0f, ForceMode2D.Impulse);
}
}
/// <summary>
/// トリガーイベント
/// </summary>
/// <param name="collision"></param>
private void OnTriggerEnter2D(Collider2D collision)
{
// サーバーなら
if (NetworkRunnerLauncher.Instance.runner.IsServer)
{
// 3秒後に処理
Observable.Timer(System.TimeSpan.FromSeconds(3)).Subscribe(_ =>
{
// 初期位置にする
rigidbody2D.position = Vector2.zero;
// 速度をリセット
rigidbody2D.linearVelocity = Vector2.zero;
// 右側に押し出す
rigidbody2D.AddForce(Vector2.right * 5.0f, ForceMode2D.Impulse);
}).AddTo(this);
}
}
}
スポーン時
/// <summary>
/// スポーン時
/// </summary>
public override void Spawned()
{
// サーバーじゃないなら
if (!NetworkRunnerLauncher.Instance.runner.IsServer)
{
// 物理挙動をシミュレートする
NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), false);
}
// サーバーなら
else
{
// 物理挙動をシミュレートする
NetworkRunnerLauncher.Instance.runner.SetIsSimulated(gameObject.GetComponent<NetworkObject>(), true);
// 右側に押し出す
rigidbody2D.AddForce(Vector2.right * 5.0f, ForceMode2D.Impulse);
}
}
- スポーン時にはクライアント側ではプレイヤーと同じく物理挙動のシュミュレーションをoffにしサーバー側ではonにし、右側に押し出す処理をします。
ゴールの当たり判定
/// <summary>
/// トリガーイベント
/// </summary>
/// <param name="collision"></param>
private void OnTriggerEnter2D(Collider2D collision)
{
// サーバーなら
if (NetworkRunnerLauncher.Instance.runner.IsServer)
{
// 3秒後に処理
Observable.Timer(System.TimeSpan.FromSeconds(3)).Subscribe(_ =>
{
// 初期位置にする
rigidbody2D.position = Vector2.zero;
// 速度をリセット
rigidbody2D.linearVelocity = Vector2.zero;
// 右側に押し出す
rigidbody2D.AddForce(Vector2.right * 5.0f, ForceMode2D.Impulse);
}).AddTo(this);
}
}
- 左右の壁に当たるとゴールと見なし、初期位置に戻す処理にを実装します。
- 移動をするのでサーバーのみの処理にし、UniRxで3秒後にリセットしています。
動作確認
ServerScene
とGameScene
をそれぞれビルドします。
ServerScene
GameScene
実行結果
サーバーを起動しクライアントを起動してUnityから実行をすると、矢印キーで上下に移動ができ壁や玉と当たり判定が発生します。
(サーバー+クライアントx2で録画していたので少し重たいです)
ここまででわかった事
- サーバーとクライアントを同じプロジェクトにするとかなりやりやすくなった
- この方法で実行すると入力->移動までラグが体感でわかるぐらいある
-
Rigidbody
とCollider
を使った当たり判定をするのにPhoton Fusion
単体ではできなくて、公式からアドオンをインポートしないと当たり判定周りはできない。
次回はスコアとかマッチング、部屋分け等々をやってみたい。
Discussion