9 - プレイヤーUIプレハブ

このセクションでは、プレイヤーUIシステムを作成する方法を説明します。 プレイヤー名と現在の体力を表示する必要があります。 UIの位置も管理してプレイヤーを追従するようにします。

このセクションはネットワーキングとは関係がありませんが、 ネットワーキングに関するいくつかの高度な機能を提供し、また開発時の制約について説明するため重要な設計パターンをいくつか記載しています。

UIは単純に必要がないのでネットワーキング化されませんが、トラフィックを使用せずに進める方法は他にいくつもあります。 これは常に追及すべきことです。機能をネットワーク化せずにすむのは良いことです。

次に考えるべきことは、ネットワーク化された各プレイヤーにどのようにUIを用意するかです。

専用のPlayerUIスクリプトを使用したUIプレハブがあります。 PlayerManagerスクリプトはこのUIプレハブの参照を保持し、PlayerManagerが開始されるとこのUIプレハブをインスタンス化し、プレハブに担当のプレイヤーを追従するように指示します。

目次

UIプレハブの作成

  1. UI Canvasのある任意のシーンを開きます。
  2. キャンバスにスライダーUI GameObjectを追加してPlayer UIと名付けます。
  3. Rect Transform の垂直アンカーをMiddleに、水平アンカーをCenterに設定します。
  4. RectTransformの幅を80に、高さを15に設定します。
  5. Backgroundの子を選択し、Imageコンポーネントの色をRedにします。
  6. 「Fill Area/Fill」の子を選択し、Imageの色をgreenにします。
  7. Text UI GameObjectをPlayer UIの子として追加し、Player Name Textと名付けます。
  8. CanvasGroupコンポーネントをPlayer UIに追加します。
  9. その CanvasGroupコンポーネントでInteractableBlocks Raycastプロパティをfalseに設定します。
  10. Player UIを階層からアセットのプレハブフォルダーにドラッグします。これでプレハブができました。
  11. 今後は不要なため、シーン内のインスタンスを削除します。

トップに戻る

PlayerUIスクリプトの基本

  1. 新しいC#スクリプトを作成し、PlayerUIと名付けます。
  2. 以下がスクリプトの基本の構成です。これに応じて編集し、PlayerUIスクリプトを保存します。
    using UnityEngine;
    using UnityEngine.UI;

    using System.Collections;

    namespace Com.MyCompany.MyGame
    {
        public class PlayerUI : MonoBehaviour
        {
            #region Private Fields

            [Tooltip("UI Text to display Player's Name")]
            [SerializeField]
            private Text playerNameText;

            [Tooltip("UI Slider to display Player's Health")]
            [SerializeField]
            private Slider playerHealthSlider;

            #endregion

            #region MonoBehaviour Callbacks

            #endregion

            #region Public Methods

            #endregion

        }
    }
  1. PlayerUIスクリプトを保存します。

ではプレハブ自体を作成してみましょう。

  1. プレハブPlayerUIPlayerUIスクリプトを追加します。
  2. パブリックフィールドPlayerNameTextに子のGameObjectである「Player Name Text」をドラッグアンドドロップします。
  3. パブリックフィールドPlayerHealthSliderにSlider Componentをドラッグアンドドロップします。

トップに戻る

インスタンス化とプレイヤーとの結合

PlayerUIをプレイヤーと結合する

PlayerUIスクリプトは、何よりもまず、体力とプレイヤー名を表示するため、どのプレイヤーを代表しているのか把握していなければなりません。 その状態と名前を表示できるようにするためです。このバインディングを可能にするパブリックメソッドを作成しましょう。

  1. スクリプトPlayerUIを開きます。
  2. 「プライベートフィールド」リージョンでプライベートプロパティを追加します。  
private PlayerManager target;

定期的に体力を確認するので、PlayerManagerの参照をキャッシュすると効率的です。

  1. このパブリックメソッドを"Public Methods"領域に追加します。
public void SetTarget(PlayerManager _target)
{
    if (_target == null)
    {
        Debug.LogError("<Color=Red><a>Missing</a></Color> PlayMakerManager target for PlayerUI.SetTarget.", this);
        return;
    }
    // Cache references for efficiency
    target = _target;
    if (playerNameText != null)
    {
        playerNameText.text = target.photonView.Owner.NickName;
    }
}
  1. MonoBehaviour CallBacksリージョンにこのメソッドを追加します。

        void Update()
    {
        // Reflect the Player Health
        if (playerHealthSlider != null)
        {
            playerHealthSlider.value = target.Health;
        }
    }
  2. PlayerUIスクリプトを保存します。

これで、UIが対象のプレイヤー名と体力を表示するようになります。

トップに戻る

インスタンス化

プレイヤープレハブをインスタンス化するたびに、このプレハブをインスタンスする必要があることがわかりました。 初期化中にPlayerManagerで行うのがインスタンス化の最良の方法です。

  1. スクリプトPlayerManagerを開きます。
  2. 参照を保持するため、以下のようにパブリックフィールドをPlayerUI参照に追加します。

        [Tooltip("The Player's UI GameObject Prefab")]
    [SerializeField]
    private GameObject playerUiPrefab;
  3. Start()メソッドにこのコードを追加します。

        if (playerUiPrefab != null)
    {
        GameObject _uiGo =  Instantiate(playerUiPrefab);
        _uiGo.SendMessage ("SetTarget", this, SendMessageOptions.RequireReceiver);
    }
    else
    {
        Debug.LogWarning("<Color=Red><a>Missing</a></Color> PlayerUiPrefab reference on player Prefab.", this);
    }
  4. PlayerManagerスクリプトを保存します。

これらは全てUnityの標準コードですが、メッセージの送信先が作成したばかりのインスタンスである点に留意してください。 SetTargetがそれに反応するコンポーネントを見つけられない場合、警告がでて通知されるよう、受信者が必要です。 もう一つの方法としては、PlayerUIコンポーネントをインスタンスから取得し、直接SetTargetを呼び出します。 一般的に、直接コンポーネント使用することを推奨しますが、他にも取得する方法があることを知っておくのも良いことです。

ですが、これだけではまだ十分ではありません。プレイヤーの削除も処理する必要があります。シーンの中にorphanのUIインスタンスが分散するのを避けるため、割り当てられた対象が無くなったことを確認し次第、UIインスタンスを破棄する必要があります。

  1. PlayerUIスクリプトを開きます。
  2. Update()機能にこれを追加します。

        // Destroy itself if the target is null, It's a fail safe when Photon is destroying Instances of a Player over the network
    if (target == null)
    {
        Destroy(this.gameObject);
        return;
    }
  3. PlayerUIスクリプトを保存します。

このコードは簡単であり、かつ、とても使い勝手のいいものです。対象の参照がnullになった際、Photonのネットワーク化されたインスタンスの削除の方法を理由として、UIインスタンスが自分自身を破棄する方が簡単です。 このやり方をすると、起こり得る多くの問題を避けることができます。そしてとても安全で対象が名¥いあなくなった理由によらず、関連するUIは自動的に自分で破棄するので、速く処理でき便利です。

ただしこのままでは、新しいレベルが読み込まれたとき、UIは破棄されますがプレイヤーはとどまります。このため、レベルの読み込みを把握したらプレイヤーもインスタンス化する必要があります。以下のように処理します。

  1. スクリプトPlayerManagerを開きます。
  2. このコードをCalledOnLevelWasLoaded()メソッドに追加します。

        GameObject _uiGo = Instantiate(this.playerUiPrefab);
    _uiGo.SendMessage("SetTarget", this, SendMessageOptions.RequireReceiver);
  3. PlayerManagerスクリプトを保存します。

この処理を行うにはより複雑で強力な方法もあり、UIはシングルトンから作成することも可能です。ただし、ルームに入室したり退室したりしている他のプレイヤーも動揺にのUIを処理する必要があるので、たちまち複雑になってしまいます。今回の実装では、UIプレハブのインスタンス化による重複で、明快な処理になっています。簡潔に実行するため、「SetTarget」メッセージをインスタンス化して送信するプライベートメソッドを作成し、コードを複製する代わりに、様々な場所からこのメソッドを呼び出すことも可能です。

トップに戻る

UIキャンバスのペアレンティング

Unity UIシステムの重要な制約の1つは、UI要素は全てCanvas GameObjectに配置しなければいけないということです。そのため、PlayerUIプレハブがインスタンス化される時に処理をする必要があります。これは、PlayerUIのインスタンス化の際に行います。

  1. スクリプトPlayerUIを開きます。
  2. 「MonoBehaviour CallBacks」リージョンにこのメソッドを追加します。

        void Awake()
    {
        this.transform.SetParent(GameObject.Find("Canvas").GetComponent<Transform>(), false);
    }
  3. PlayerUIスクリプトを保存します。

なぜこのように力ずくでキャンバスを見つけるのでしょうか?シーンが読み込まれたりアンロードされる際は、プレハブと同様キャンバスも毎回異なるからです。 これ以上複雑なコード構成を避け、一番迅速な方法を使います。ただし「Find」の使用はお勧めしません。このオペレーションは遅いからです。 このようなケースで複雑な処理を実装することは、このチュートリアルの趣旨とは離れますが、Unityやスクリプトに慣れてきたら試してみてください。読み込みやアンロードを考慮に入れた、キャンバス要素参照のより良い管理方法をコーディングでき  ます。

トップに戻る

対象のプレイヤーを追従する

これは興味深いパートです。Player UIが対象のプレイヤーをスクリーンで追従するようにする必要があります。 以下のような細かい処理が必要です。

  • UIは2D要素ですが、プレイヤーは3D要素です。このようなケースで、どのようにして位置を一致させられるでしょうか?
  • UIがプレイヤーよりわずかに上に位置することは好ましくありません。プレイヤーの位置から画面上でオフセットするには、どうすればよいでしょうか?

    1. PlayerUIスクリプトを開きます。
    2. 「Public Fields」リージョンにこのパブリックプロパティを追加します。

          [Tooltip("Pixel offset from the player target")]
      [SerializeField]
      private Vector3 screenOffset = new Vector3(0f,30f,0f);
    3. これら2つのプライベートフィールドを「Private Fields」リージョンに追加します。

float characterControllerHeight = 0f;
Transform targetTransform;
Renderer targetRenderer;
CanvasGroup _canvasGroup;
Vector3 targetPosition;
  1. これをAwakeメソッド領域の中に追加します。
  _canvasGroup = this.GetComponent<CanvasGroup>();
  1. _targetが設定された後、次のコードをSetTarget()メソッドに追加します。
targetTransform = this.target.GetComponent<Transform>();
targetRenderer = this.target.GetComponent<Renderer>();
CharacterController characterController = _target.GetComponent<CharacterController> ();
// Get data from the Player that won't change during the lifetime of this Component
if (characterController != null)
{
    characterControllerHeight = characterController.height;
}

プレイヤーは、 Heightプロパティを持つCharacterControllerに基づいています。これは、プレイヤー上のUI要素を適切にオフセットするために必要です。

  1. 「MonoBehaviour Callbacks」リージョンにこのパブリックメソッドを追加します。
void LateUpdate()
{
    // Do not show the UI if we are not visible to the camera, thus avoid potential bugs with seeing the UI, but not the player itself.
  if (targetRenderer!=null)
  {
    this._canvasGroup.alpha = targetRenderer.isVisible ? 1f : 0f;
  }

    // #Critical
    // Follow the Target GameObject on screen.
    if (targetTransform != null)
    {
        targetPosition = targetTransform.position;
        targetPosition.y += characterControllerHeight;
        this.transform.position = Camera.main.WorldToScreenPoint (targetPosition) + screenOffset;
    }
}
  1. PlayerUIスクリプトを保存します。

2Dの位置と3Dの位置を合わせるためのポイントは、カメラのWorldToScreenPoint機能を使用することです。このゲームでは1つしかないので、Unityシーンのデフォルト設定であるメインカメラにアクセスします。

複数のステップでオフセットを設定しました。最初に対象の実際の位置を取得して、characterControllerHeightを追加し、最後にプレイヤーのトップ画面位置を推定した後、最後に画面オフセットを追加します。

前に戻る.

ドキュメントのトップへ戻る