はじめに
前回はコインの生成と削除を実装しました。
今回はコインの取得数を表示していきたいと思います。
今回の最終形はこんな感じになります!
コインを取得すると頭上の数値が増えていますね。
この数値もちゃんと同期されているのが分かると思います。
コインカウンター用のプレハブを作成
プレイヤーの上に表示するカウンター用のプレハブを作ります。
画像のようにTextMeshProを付けたUIオブジェクトを作成してください。
そしてCoinCountというスクリプトを作成します。
CoinCount.csは次のようになっています。
public class CoinCount : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI m_text;
private Transform m_target;
private void Update()
{
//ターゲットがいなくなっていたら削除する
if(m_target == null)
{
Destroy(gameObject);
return;
}
//ワールド座標をスクリーン座標に変換
transform.position = RectTransformUtility.WorldToScreenPoint(Camera.main, m_target.position + Vector3.up);
}
//数値設定
public void SetNumber(int count)
{
m_text.text = count.ToString();
}
//表示する対象(プレイヤー)を設定
public void SetTarget(Transform target)
{
m_target = target;
}
}
ターゲットの少し上の位置をスクリーン座標に変換して数値を表示しています。
このコードに関してはNetcodeの機能を使っていないので特段説明はしません。
スクリプトが作成出来たらアタッチしておきましょう。
プレイヤー生成時にカウンターを生成
プレイヤーがスポーンされた際にカウンターも一緒に生成するようにしましょう。
UIを生成するのでGameシーンにCanvasを配置してください。
そしてプレイヤースクリプトを以下のように変更します。
(Start、Update、SetMoveInputServerRpc、ServerUpdate関数は変更がありません)
public class Player : NetworkBehaviour
{
[SerializeField] float m_moveSpeed = 1;
private Rigidbody m_rigidBody;
private Vector2 m_moveInput = Vector2.zero;
//コインのプレハブ
[SerializeField] private GameObject m_coinCountPrefabs;
private CoinCount m_coinCount;
//コイン取得数
private NetworkVariable<int> m_coinNum;
void Awake()
{
m_coinNum = new NetworkVariable<int>(0);
}
void Start()
{
// Rigidbody を取得
m_rigidBody = GetComponent<Rigidbody>();
}
public override void OnNetworkSpawn()
{
//コイン取得数変化通知
m_coinNum.OnValueChanged += OnCoinNumChanged;
//コインカウントUI生成
var canvas = GameObject.Find("Canvas").transform;
m_coinCount = Instantiate(m_coinCountPrefabs, canvas).GetComponent<CoinCount>();
m_coinCount.SetTarget(transform);
m_coinCount.SetNumber(m_coinNum.Value);
}
//コインUI更新
void OnCoinNumChanged(int prevValue, int newValue)
{
m_coinCount.SetNumber(newValue);
}
private void Update()
{
//ownerの場合
if (IsOwner)
{
// 移動入力を設定
SetMoveInputServerRpc(
Input.GetAxisRaw("Horizontal"),
Input.GetAxisRaw("Vertical"));
}
//サーバー(ホスト)の場合
if (IsServer)
{
ServerUpdate();
}
}
//=================================================================
//RPC
//=================================================================
// 移動入力をセットするRPC
[ServerRpc]
private void SetMoveInputServerRpc(float x, float y)
{
m_moveInput = new Vector2(x, y);
}
//=================================================================
//サーバー側で行う処理
//=================================================================
// サーバー側で呼ばれるUpdate
private void ServerUpdate()
{
//移動
var velocity = Vector3.zero;
velocity.x = m_moveSpeed * m_moveInput.normalized.x;
velocity.z = m_moveSpeed * m_moveInput.normalized.y;
//移動処理
m_rigidBody.AddForce(velocity * Time.deltaTime);
}
void OnTriggerEnter(Collider other)
{
if (IsServer == false) { return; }
if (other.gameObject.CompareTag("Coin"))
{
//取得処理
m_coinNum.Value += 1;
//コイン削除処理(CoinManagerの処理を呼ぶ)
CoinManager.Instance.DeleteCoin(other.gameObject);
}
}
}
Playerのプレハブを開きPlayerScriptのCoinCountPrefabsに先ほど作ったプレハブをアタッチします。
UI生成
プレイヤーがスポーンされたタイミングで呼び出されるOnNetworkSpawnコールバックでUIを生成します。
//コインカウントUI生成
var canvas = GameObject.Find("Canvas").transform;
m_coinCount = Instantiate(m_coinCountPrefabs, canvas).GetComponent<CoinCount>();
m_coinCount.SetTarget(transform);
m_coinCount.SetNumber(m_coinNum.Value);
同期変数を作成
Netcodeの機能にNetworkVariableというものがあります。
これを使うと同期する変数を作ることができます。
今回はコイン数を同期したいためint型の同期変数が必要です。
その場合は以下のように定義します。
private NetworkVariable<int> m_coinNum;
次にOnNetworkSpawn値が変更された時のコールバック関数を設定します。
m_coinNum.OnValueChanged += OnCoinNumChanged;
これでサーバー側でm_coinNumの値が変更されると、登録したOnCoinNumChangedコールバックが呼び出されます。
void OnCoinNumChanged(int prevValue, int newValue)
{
//コインの値を更新
m_coinCount.SetNumber(newValue);
}
そしてコインに当たったときにm_coinNumを+1するようしましょう。
void OnTriggerEnter(Collider other)
{
if (IsServer == false) { return; }
if (other.gameObject.CompareTag("Coin"))
{
//取得処理
m_coinNum.Value += 1;
//コイン削除処理(CoinManagerの処理を呼ぶ)
CoinManager.Instance.DeleteCoin(other.gameObject);
}
}
これでコインを取得したときに、カウンターが連動して増える仕組みができました。
NetworkVariableの値を変更する際は以下の2点に注意してください。
- 値を変更は.Valueに対し行う
- 値の変更はサーバーで行う
プレイヤーの生成方法を変更
ここまででコイン取得数を表示する部分の作成は完了しました。
しかし、現状で実行すると意図したとおりに動きません。
左がホストですが、ホストの画面でだけ自身の操作キャラの頭上にUIが表示されていません。
これはプレイヤーが生成されるタイミングに原因があります。
現状プレイヤーオブジェクトはホストに接続した時に自動的に生成されます。
そのためホスト自身はGameシーンに切り替わる前にオブジェクトが生成されてしまいます。
なのでTitleシーンにコインカウントUIが生成され、シーンが切り変わったときに破棄されています。
逆にクライアントがサーバーに接続する際はすでにサーバー側のシーンがGameに切り替わっている状態なので、問題なく生成されています。
この問題を解決するために、プレイヤーの生成タイミングを変えましょう。
プレイヤーの自動生成を止める
今のままだと自動的にプレイヤーオブジェクトが生成されるので、その設定をやめましょう。
まず、タイトルシーンのNetworkMangerを選択し、PlayerPrefabの設定をNoneに変更します。
そして後で手動で生成するためにNetworkPrefabsの方に登録してください。
次にTitle.csのApprovalCheck関数内の一部を以下のように変更します。
//PlayerObjectを生成するかどうか
response.CreatePlayerObject = false;
//PlayerObjectをスポーンする位置(nullの場合Vector3.zero)
response.Position = Vector3.zero;
これで自動的に生成されなくなりました。
プレイヤーを任意のタイミングで生成する
今度はGameシーンに切り替わったタイミングでプレイヤーを生成するようにしましょう。
方法はGameシーンにネットワークオブジェクトを置き、そのオブジェクトのOnNetworkSpawnコールバック内でプレイヤーを生成します。
これでGameシーンに切り替わったタイミングでプレイヤーを生成することができます。
Game.csという名前のスクリプトを作成します。
public class Game : NetworkBehaviour
{
//プレイヤーのプレハブ
[SerializeField] private NetworkObject m_playerPrefab;
public override void OnNetworkSpawn()
{
//ホスト以外の場合
if (IsHost == false){ return; }
//クライアント接続時
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
//すでに存在するクライアント用に関数呼び出す
foreach (var client in NetworkManager.Singleton.ConnectedClientsList)
{
OnClientConnected(client.ClientId);
}
}
public void OnClientConnected(ulong clientId)
{
//プレイヤーオブジェクト生成
var generatePos = new Vector3(0, 1, -8);
generatePos.x = -5 + 5 * (NetworkManager.Singleton.ConnectedClients.Count % 3);
NetworkObject playerObject = Instantiate(m_playerPrefab, generatePos, Quaternion.identity);
//接続クライアントをOwnerにしてPlayerObjectとしてスポーン
playerObject.SpawnAsPlayerObject(clientId);
}
}
そしてGameシーンにこのスクリプトをアタッチするオブジェクトを作ります。
今回はInstanceという名前のオブジェクトにNetworkObjectとGame.csを取り付けました。
PlayerPrefabの部分にプレハブをアタッチし忘れないように注意しましょう。
プレイヤーオブジェクトのスポーン
ネットワークオブジェクトのスポーン方法はコインを生成する時に学びました。
NetworkObject型のSpawn関数をサーバーで呼び出せばいいのでしたね。
今回は少しだけ違っていて、プレイヤーオブジェクトを生成します。
プレイヤーオブジェクトとはNetcodeにおいて特別な存在で、NetworkManger経由で直接取得することができます。
//クライアントが自分自身のレイヤーオブジェクトを取得する場合
NetworkManager.Singleton.LocalClient.PlayerObject;
//サーバーが特定のクライアントのプレイヤーオブジェクトを取得する場合
NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject;
自動でプレイヤーを生成していた時は、勝手にPlayerObjectとして生成してくれていました。
これを手動で生成する場合はNetworkObject型のSpawnAsPlayerObjectという関数を使います。
//接続クライアントをOwnerにしてPlayerObjectとしてスポーン
playerObject.SpawnAsPlayerObject(clientId);
引数のclientIdはそのプレイヤーを操作するクライアントのIDになります。
クライアント接続コールバック
OnClientConnectedCallbackはクライアントが接続した際に呼び出されるコールバックです。
これでクライアントが接続するたびにOnClientConnected関数が呼び出されます。
//クライアント接続時
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
OnClientConnectedの引数はクライアントのIDです。
この関数内で先ほど説明したプレイヤーオブジェクトの生成を行います。
public void OnClientConnected(ulong clientId)
{
//プレイヤーオブジェクト生成
var generatePos = new Vector3(0, 1, -8);
generatePos.x = -5 + 5 * (NetworkManager.Singleton.ConnectedClients.Count % 3);
NetworkObject playerObject = Instantiate(m_playerPrefab, generatePos, Quaternion.identity);
//接続クライアントをOwnerにしてPlayerObjectとしてスポーン
playerObject.SpawnAsPlayerObject(clientId);
}
しかしこれだけでは問題があり、OnClientConnectedCallbackはすでに接続されているクライアント(ホスト含め)に対してはコールバックが呼び出されません。
なので、すでに存在するクライアントようにはOnClientConnected関数を手動で呼び出します。
//すでに存在するクライアント用に関数呼び出す
foreach (var client in NetworkManager.Singleton.ConnectedClientsList)
{
OnClientConnected(client.ClientId);
}
動作確認してみよう
ここまで出来たらビルドして動作確認してみましょう。
全てのプレイヤーの上にコイン数が表示されるようになったと思います。
まとめ
今回は変数の同期とプレイヤーオブジェクトの生成について学びました。
Netcodeの学習部分は今回で終わりです。お疲れさまでした。
あとはSteamの機能を使ってインターネット越しで通信ができるようにするだけです。
あと少しですので頑張りましょう。
👇あなたにお勧めの書籍
次回↓
コメント