PhotonUnity技術ブログ

UnityPhotonのデモDemo2DJumpAndRunWithPhysicsを改造してみる。

今回は前回の「よくわかる??Photonの仕組み」から続いて、

作成したサンプルゲームの解説をおこなっていきたいと思います。

今回のサンプルゲームは、PhotonのデモゲームのDemo2DJumpAndRunWithPhysicsを改造して見ました。

・改造点は以下の通り

・部屋一覧画面に遷移して部屋の作成や部屋一覧から部屋を選択できるようにしました。(部屋一覧画面の追加)

・各プレイヤーにScoreやHPなどの固有のパラメーターを持たせました。(PlayerにCustomPropertyを持たせる)


・ジャンプして相手プレイヤーを踏んづけた時に相手にダメージを与えて自分の得点が上昇するようにしました。

・部屋に残り時間を持たせて(RoomにCustomPropertyを持たせる)後から来たプレイヤーもその残り時間を共有できるようにしました。

・残り時間が0になった時に、ゲームオーバー画面へ遷移するようにしました。

・ゲームを実際に遊んでみる

・Unityを開き新規プロジェクトを作成します。

・Photonをインポートします。

Photonのインポートと設定方法についてはこちらの記事をご参照ください

・今回パッケージを準備しましたのでパッケージをプロジェクトにインポートします。

ファイルをダウンロード
 


パッケージダウンロード後にファイルをダブルクリックしてデータをインポートします。

・プロジェクトの内容

Images配下には本ゲーム中で使用しているImageが格納されています。
Prefabs配下にはGameのControll用のGameObjectのPrefabと部屋表示用のPrefabが格納されています。
Resources配下には、Playerのアバター用のキャラクターPrefabが格納されています。
Resources配下に格納している理由としては、Photon特有のInstantiateをする際にResources配下でなければPrefabを読み込めない為です。
Scenes配下に本サンプルゲームのSceneが格納されています。

Scripts配下に本サンプルゲームで追加したScriptファイルが格納されています。
各スクリプトの詳細は下記に記載しています。

・実際にサンプルプロジェクトを遊んでみる

Assets/FriendsHirataSoft/Scenes/FriendsJumpAndRun.unity

を開きます。

こちらのFriendsJumpAndRun.unityをダブルクリック

ゲームシーンが開きますのでUnityエディタ中の再生ボタン「▶︎」のマークをクリックします。

ゲームタイトルへ遷移します。

画面タップ後に読み込み画面へ遷移

部屋一覧画面になるので、Inputに任意の文字を入力後にCreateRoomを押下します。

もしくは、部屋がある場合は、任意の部屋をクリックします。

読み込み後にゲームが開始されます。

相手を踏むとScoreが加算されます。

また踏まれた相手のHPが減少します。

HPが0になるとGameOverになり部屋から強制的に退出します。

 

・各種追加したスクリプトの紹介

下記に今回追加したScriptの解説を記載しています。

FHConnectAndJoinLobby.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace FriendsHirataSoft
{
    public class FHConnectAndJoinLobby : MonoBehaviour
    {
        //任意のバージョン識別用の文字列を入れます。
        const string Version = "1";

        void Start()
        {
            //今回は、Lobbyへ遷移後に部屋一覧を確認したいので autoJoinLobbyをtrueにします。
            //部屋一覧に飛ばず直接部屋に遷移したい場合などにこのフラグをfalseにします。
            PhotonNetwork.autoJoinLobby = true;
        }

        public void ConnectToPhoton(){
            if (PhotonNetwork.connecting) return;
            PhotonNetwork.ConnectUsingSettings(Version + "");
        }

    }
}

Photon接続用のScriptです。
ConnectToPhoton()が実行された時にPhotonに接続されます。

ゲーム中では最初の画面タップ時にこの処理が行われています。

/FHGameControllObjects/GameTitleCanvas/GameStartButton

にアタッチされているButtonのOnClickに処理が記載されています。

FHDeadZone.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace FriendsHirataSoft
{
    public class FHDeadZone : MonoBehaviour
    {

        private void OnCollisionEnter2D(Collision2D collision)
        {
            var photonView = collision.gameObject.GetComponent();
            if (photonView == null) return;
            var targetPlayer = photonView.owner;
            if (targetPlayer == null) return;
            targetPlayer.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { PlayerCustomPropertyKey.HP.ToString(), 0 } });
        }
    }
}

こちらはゲームのステージ外に出てしまった時の死亡判定用のスクリプトです。

ステージ外に飛び出したプレイヤーのHPを0にします。

/FHGameControllObjects/LevelColliders/DeadZoneCollider

にアタッチされています。

FHGameManager.cs


using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public enum PlayerCustomPropertyKey{
    HP,
    SCORE,
}
public enum RoomCustomPropertyKey
{
    REMAINING_TIME,
}

namespace FriendsHirataSoft{
    public class FHGameManager : MonoBehaviour
    {
        enum GameState{
            NONE,
            ROOM_LIST,
            GAME_TITLE,
            GAME_END,
        }
        [SerializeField]
        GameObject gameTitleCanvas;
        [SerializeField]
        GameObject roomListCanvas;
        [SerializeField]
        GameObject gameEndCanvas;

        [SerializeField]
        FHRoomListItem fHRoomListItemPrefab;
        [SerializeField]
        Transform roomListItemContent;
        List fHRoomItemList = new List();
        [SerializeField]
        InputField inputField;
        PhotonView photonView;
        bool isGameEnd;

		void Start()
		{
            if (PhotonNetwork.connected)
            {
                PhotonNetwork.Disconnect();
            }
            gameTitleCanvas.SetActive(true);
            roomListCanvas.SetActive(true);
            photonView = GetComponent();
		}

		void Update()
		{
            if (
                PhotonNetwork.inRoom 
                && PhotonNetwork.player.isMasterClient 
                && PhotonNetwork.room.customProperties.ContainsKey(RoomCustomPropertyKey.REMAINING_TIME.ToString())
               )
            {
                float remainingTime = (float)PhotonNetwork.room.customProperties[RoomCustomPropertyKey.REMAINING_TIME.ToString()];
                remainingTime -= Time.deltaTime;
                PhotonNetwork.room.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { RoomCustomPropertyKey.REMAINING_TIME.ToString(), remainingTime } });
                if(remainingTime < 0 && !isGameEnd){
                    isGameEnd = true;
                    photonView.RPC("ShowGameEndCanvas", PhotonTargets.All, null);
                }
            }
		}

		public void CreatePhotonRoom(){
            PhotonNetwork.CreateRoom(inputField.text);
        }
        void JoineRoom(RoomInfo roomInfo){
            PhotonNetwork.JoinRoom(roomInfo.Name);
        }

        const int DEFALT_HP = 100;
        const int DEFALT_SCORE = 0;
        const float DEFALT_REMAINING_TIME = 180.0f;

        bool initializedPlayerStates = false;

        public void InitializePlayerStates(){
            PhotonNetwork.player.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { PlayerCustomPropertyKey.HP.ToString(), DEFALT_HP } });
            PhotonNetwork.player.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { PlayerCustomPropertyKey.SCORE.ToString(), DEFALT_SCORE } });
            PhotonNetwork.player.name = "Player" + PhotonNetwork.player.ID;
            initializedPlayerStates = true;
        }


        public void InitializeRoomStates(){
            if(PhotonNetwork.player.isMasterClient){
                PhotonNetwork.room.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { RoomCustomPropertyKey.REMAINING_TIME.ToString(), DEFALT_REMAINING_TIME } });
            }
        }

        void ShowSceneUI(GameState state){
            gameTitleCanvas.SetActive(false);
            roomListCanvas.SetActive(false);
            gameEndCanvas.SetActive(false);
            switch (state)
            {
                case GameState.NONE:
                    break;
                case GameState.GAME_TITLE:
                    gameTitleCanvas.SetActive(true);
                    break;
                case GameState.ROOM_LIST:
                    roomListCanvas.SetActive(true);
                    break;
                case GameState.GAME_END:
                    gameEndCanvas.SetActive(true);
                    break;
            }
        }

        public void HideUI(){
            ShowSceneUI(GameState.NONE);
        }

        public void ShowGameTitleCanvas(){

            ShowSceneUI(GameState.GAME_TITLE);
        }
        public void ShowRoomListCanvas()
        {
            ShowSceneUI(GameState.ROOM_LIST);
        }

        [PunRPC]
        void ShowGameEndCanvas()
        {
            ShowSceneUI(GameState.GAME_END);
        }

        public void CarryOutTheDeath(){
            if (!PhotonNetwork.player.customProperties.ContainsKey(PlayerCustomPropertyKey.HP.ToString()) || !initializedPlayerStates) return;

            bool isDead = 0>=(int)PhotonNetwork.player.customProperties[PlayerCustomPropertyKey.HP.ToString()];
            if(isDead && PhotonNetwork.inRoom){
                GameOver();
            }
        }

        public void GameOver(){
            PhotonNetwork.Disconnect();
            SceneManager.LoadScene(SceneManager.GetActiveScene().name);
        }

		public void UpdateRoomItems(){
            fHRoomItemList.ForEach(item => Destroy(item.gameObject));
            fHRoomItemList.Clear();
            foreach (var roomInfo in PhotonNetwork.GetRoomList())
            {
                CreateRoomItem(roomInfo);
            }
        }

        void CreateRoomItem(RoomInfo roomInfo){
            var item = Instantiate(fHRoomListItemPrefab) as FHRoomListItem;
            item.gameObject.transform.SetParent(roomListItemContent);
            SetRoomItemInfo(item, roomInfo);
            fHRoomItemList.Add(item);
        }

        void SetRoomItemInfo(FHRoomListItem item,RoomInfo roomInfo){
            item.UpdateDisplay(roomInfo, (_roomInfo) => {
                JoineRoom(_roomInfo);
            });
        }

    }
}



 

こちらのスクリプトは

/FHGameControllObjects/FHGameControllManagersにアタッチされています。

こちら、ゲーム全体を管理してコントロールするScriptになります。

機能が詰め込まれている為、ややファッティになってます。

[SerializeField] GameObject gameTitleCanvas;

[SerializeField] GameObject roomListCanvas;

[SerializeField] GameObject gameEndCanvas;

あたりでゲームのCanvas表示を切り替えたり

CreateRoomItem(RoomInfo roomInfo)で部屋の表示アイテムの生成

GameOver()でPhoton切断後にTitleに遷移

また、InitializePlayerStates()やInitializeRoomStates()

の処理により部屋やプレイヤー情報の初期化をおこなっています。

また、これらの処理は次に記載するFHPhotonCallBackReceiver.cs中でPhotonの各種状態変化時のコールバックにフックさせる形で実行されています。

FHPhotonCallBackReceiver.cs


using System.Collections.Generic;
using ExitGames.Client.Photon;
using UnityEngine;
using UnityEngine.Events;

namespace FriendsHirataSoft
{
    public class FHPhotonCallBackReceiver : MonoBehaviour
    {
        void OnConnectedToPhoton()
        {
            onConnectedToPhotonEvent.Invoke();
        }
        [SerializeField]
        UnityEvent onConnectedToPhotonEvent;
        void OnLeftRoom()
        {
            onLeftRoomEvent.Invoke();
        }
        [SerializeField]
        UnityEvent onLeftRoomEvent;
・
・
・
・
以下Phtonの各種コールバックが続きます。
長いので割愛しています。

こちらPhotnのコールバックをUnityのInspector上で設定できるようにしたScriptになります。

/FHGameControllObjects/FHGameControllManagersにアタッチされています。

このようにInspectorから各種イベントをフックさせることができます。

本ゲーム中では、

OnJoinedLobbyのタイミングで(Lobbyにjoinした時)

FHGameManager.csのShowRoomListCanvas()を実行しています。

(部屋一覧の表示処理)

OnReceivedRoomListUpdateのタイミングで(部屋一覧が更新された時)

FHGameManager.csのUpdateRoomItems()を実行

(部屋一覧を更新する処理)

OnJoinedRoomのタイミングで(部屋にJoinedした時)

FHGameManager.csのHideUI

(UIを非表示にする処理)

FHGameManager.csのInitializePlayerStates

(PlayerのStatesを初期化する処理)

FHGameManager.csのInitializeRoomStates

(RoomのStatesを初期化する処理)

が行われています。

 

FHJumpAttack.cs


using System;
using UnityEngine;
namespace FriendsHirataSoft
{
    public class FHJumpAttack : MonoBehaviour
    {
        Action atkActionCallBack;
        public void Initialize(Action atkActionCallBack){
            this.atkActionCallBack = atkActionCallBack;
        }

		void OnTriggerEnter2D(Collider2D collision)
		{
            if (collision.gameObject.tag == "Player")
            {
                var player = collision.transform.GetComponent().owner;
                if (atkActionCallBack != null) atkActionCallBack(player);
            }
		}
    }
}

こちらジャンプ攻撃用の判定用のスクリプトになります。

/Assets/FriendsHirataSoft/Resources/FHRobot Kyle 2D.prefab/JumpAttackにアタッチされています。

BoxCollider2DにTriggerEnter2Dしたオブジェクトのタグをみて、

他のプレイヤーだった場合は設定したコールバックが発火するようになっています。

 

FHPlayerController.cs


using UnityEngine;
namespace FriendsHirataSoft
{
    public class FHPlayerController : MonoBehaviour
    {

        int atk = 10;
        int addScore = 5;
        [SerializeField]
        float jumpForce = 500;
        Rigidbody2D body;
        PhotonView photonView;
        [SerializeField]
        FHJumpAttack fHJumpAttack;
        Animator animator;

        [SerializeField]
        ParticleSystem attackEffect;

        void Start()
        {
            fHJumpAttack.Initialize(JumpAttack);
            body = GetComponent();
            photonView = GetComponent();
            animator = GetComponent();
            animator.SetBool("IsGrounded", true);
        }


        void JumpAttack(PhotonPlayer target)
        {
            if (target.IsLocal || animator.GetBool("IsGrounded")) return;
            body.AddForce(Vector2.up * jumpForce);
            photonView.RPC("FHDoJump", PhotonTargets.All);
            var targetHp = (int)target.CustomProperties[PlayerCustomPropertyKey.HP.ToString()];
            targetHp = targetHp - atk;
            target.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { PlayerCustomPropertyKey.HP.ToString(), targetHp } });
            var score = (int)PhotonNetwork.player.CustomProperties[PlayerCustomPropertyKey.SCORE.ToString()];
            PhotonNetwork.player.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { PlayerCustomPropertyKey.SCORE.ToString(), score + addScore } });
        }

        void HideAttackEffect()
        {
            attackEffect.gameObject.SetActive(false);
        }

        [PunRPC]
        void FHDoJump()
        {
            //相手を踏んだ時のエフェクトを発火させる
            attackEffect.gameObject.SetActive(true);
            Invoke("HideAttackEffect", 1.0f);

        }
    }
}

こちらプレイヤーアバターのコントロールを行う為のスクリプトになります。

JumpAttack(PhotonPlayer target)で相手にJump攻撃した際の処理が定義されています。

特筆すべきところで

var targetHp = (int)target.CustomProperties[PlayerCustomPropertyKey.HP.ToString()];

targetHp = targetHp – atk;
 target.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { PlayerCustomPropertyKey.HP.ToString(), targetHp } } );

によって対象プレイヤーのCustomProperties中のHPを減らしています。

また、JumpAttack(PhotonPlayer target)中でRPCも実行しており、

FHDoJump()がRPCとして各プレイヤーで実行されます。

 

/Assets/FriendsHirataSoft/Resources/FHRobot Kyle 2D.prefabにアタッチされています。

Inspector上では、攻撃時のエフェクトや、相手を踏みつけた際のJumpの強さが定義されてます。

FHRoomListItem.cs


using UnityEngine;
using UnityEngine.UI;

namespace FriendsHirataSoft
{
    public class FHRoomListItem : MonoBehaviour
    {
        [SerializeField]
        Text roomNameText;
        [SerializeField]
        Text roomPlayerCountText;
        [SerializeField]
        Button button;
        RoomInfo roomInfo;
        public RoomInfo RoomInfo{
            get{
                return roomInfo;
            }
        }
        public void UpdateDisplay(RoomInfo info,Action onClick){
            roomInfo = info;
            roomNameText.text = info.Name;
            roomPlayerCountText.text = "PlayerCount:" + info.playerCount + "人";
            //UpdateDisplayを複数回呼ぶ想定なので、一旦AddListenerする前にRemoveAllListenerしている
            button.onClick.RemoveAllListeners();
            button.onClick.AddListener(()=>{
                onClick(roomInfo);
            });
        }
    }
}

こちらは部屋一覧の部屋表示用のスクリプトとなっています。

/Assets/FriendsHirataSoft/Prefab/RoomListItem.prefabにアタッチされています。

PhotonのRoomInfoモデルデータ(部屋情報)を受け取り、部屋名や部屋の人数などを表示しています。

 

FHShowStatusWhenConnecting.cs


using UnityEngine;

namespace FriendsHirataSoft
{
    public class FHShowStatusWhenConnecting : MonoBehaviour
    {

        public GUISkin Skin;
        [SerializeField]
        GameObject loadingShield;

        ClientState prevState = ClientState.Leaving;

        void OnGUI()
        {
            if (Skin != null)
            {
                GUI.skin = Skin;
            }

            float width = 400;
            float height = 100;

            if (prevState != PhotonNetwork.connectionStateDetailed)
            {
                prevState = PhotonNetwork.connectionStateDetailed;
                Debug.Log(PhotonNetwork.connectionStateDetailed);
            }

            if (PhotonNetwork.connectionStateDetailed == ClientState.JoinedLobby || PhotonNetwork.connectionStateDetailed == ClientState.PeerCreated)
            {
                if (loadingShield.active)
                {
                    loadingShield.SetActive(false);
                }
                return;
            }
            if (!loadingShield.active)
            {
                loadingShield.SetActive(true);
            }
            Rect centeredRect = new Rect((Screen.width - width) / 2, (Screen.height - height) / 2, width, height);

            GUILayout.BeginArea(centeredRect, GUI.skin.box);
            {
                GUILayout.Label("Connecting" + GetConnectingDots(), GUI.skin.customStyles[0]);
                GUILayout.Label("Status: " + PhotonNetwork.connectionStateDetailed);
            }
            GUILayout.EndArea();

            if (PhotonNetwork.inRoom)
            {
                loadingShield.SetActive(false);
                enabled = false;
            }
        }

        string GetConnectingDots()
        {
            string str = "";
            int numberOfDots = Mathf.FloorToInt(Time.timeSinceLevelLoad * 3f % 4);

            for (int i = 0; i < numberOfDots; ++i)
            {
                str += " .";
            }

            return str;
        }
    }
}

こちらはゲーム中でPhotonの接続状況を表示する為のスクリプトになります。

/FHGameControllObjects/FHGameControllManagersにアタッチされています。

GUILayout.Label(“Status: ” + PhotonNetwork.connectionStateDetailed);

中で、PhotonNetwork.connectionStateDetailedから現在のPhotonへの接続状況を受け取り、

表示をおこなっています。

・まとめ

このサンプルゲーム上で、Photonの状態遷移や部屋一覧の取得、
RoomやPlayerのCustomPropertyなどの使用例などを記載していますので
参考になれば幸いです。