UnityとPhotonで対戦型ボードゲーム「犬猫将棋」を作成したい(14):ターン制のタイマーの作成をデモから学ぶ

犬猫将棋を作る上で、ターン制のゲームというところを取り入れたいところですが、なかなか検索してもそれらしいことをやっている人を見つけられませんでした。

 

PhotonにはそもそもPUN TurnManagerというものがついていて、ターン制のゲームに対応しているはずなのです。

 

下のリンクを確認するとPhotonのアセットにはもともとジャンケンのターン制のゲームTurnBasedDemoが組み込まれていることが分かります。

 

Photon公式:ターンベースゲームデモ

 

実際にデモを起動してみると、(起動方法は下記リンク参照下さい)黒い画面から選んでいろんなデモを体験できるのですが、その中の一つに、ジャンケンマシーンがあります。

   

Photon公式: Demos And Tutorials  概要 (デモ開始手順)

   

   

これは5秒以内にジャンケンを出さないといけないゲームです。

 

そして、対戦相手も同じく5秒以内に同時進行でジャンケンを出さなければなりません。

 

ジャンケンの手が両方決まった段階で勝敗処理が決まるといった形のゲームです。

   

インスペクターを見てみるとどうやら、PUN Tun Managerがコンポーネントとしてくっついているのが分かります。

 

これを利用すれば、犬猫将棋にもターン制を取り入れることができるのではないかと考えました。

   

そこで、まずはソースコードを読みました。

 

一回目はチンプンカンプンで、5回ぐらいは読みました。そこで、分かったことを日本語化して注釈をつけたところ何となくこのプログラムの全貌が見えてきています。

   

取り急ぎ、主要な部分のソースに 分かる部分だけ日本語の注釈をつけた ものを貼ります。

 

私の理解の範囲内でしか書けていないので多々間違っている解釈もあると思いますが、何となくゲームのロジックが、どこに書かれているか分かると思います。

   

    private PunTurnManager turnManager;

    public Hand randomHand;    // used to show remote player's "hand" while local player didn't select anything

	// keep track of when we show the results to handle game logic.
	private bool IsShowingResults;
	
    public enum Hand
    {
        None = 0,
        Rock,
        Paper,
        Scissors
    }

    public enum ResultType
    {
        None = 0,
        Draw,
        LocalWin,
        LocalLoss
    }

    public void Start() //スタートのタイミングで呼ばれます。
    {
		this.turnManager = this.gameObject.AddComponent<PunTurnManager>();//PunTurnManagerをコンポーネントとして呼びます。
        this.turnManager.TurnManagerListener = this;//リスナーを??
        this.turnManager.TurnDuration = 5f;//5秒でターンを回します
        

        this.localSelectionImage.gameObject.SetActive(false);
        this.remoteSelectionImage.gameObject.SetActive(false);
        this.StartCoroutine("CycleRemoteHandCoroutine");

		RefreshUIViews();
    }

    public void Update()
    {
        // Check if we are out of context, which means we likely got back to the demo hub.//デモに戻る時の処理
        if (this.DisconnectedPanel ==null)
		{
			Destroy(this.gameObject);
        }

        // for debugging, it's useful to have a few actions tied to keys://デバッグ用の処理
        if (Input.GetKeyUp(KeyCode.L))
        {
            PhotonNetwork.LeaveRoom();
        }
        if (Input.GetKeyUp(KeyCode.C))
        {
            PhotonNetwork.ConnectUsingSettings(null);
            PhotonHandler.StopFallbackSendAckThread();
        }

	
        if ( ! PhotonNetwork.inRoom)
        {
			return;
		}

        // disable the "reconnect panel" if PUN is connected or connecting//通信切れましたの画面が出るようにする処理
        if (PhotonNetwork.connected && this.DisconnectedPanel.gameObject.GetActive())
		{
			this.DisconnectedPanel.gameObject.SetActive(false);
		}
		if (!PhotonNetwork.connected && !PhotonNetwork.connecting && !this.DisconnectedPanel.gameObject.GetActive())
		{
			this.DisconnectedPanel.gameObject.SetActive(true);
		}


		if (PhotonNetwork.room.PlayerCount>1)
		{
			if (this.turnManager.IsOver)//ターンが終わっているかどうかの真偽値。
			{
				return;//終わっていればリターン。
            }

			/*
			// check if we ran out of time, in which case we loose
			if (turnEnd<0f && !IsShowingResults)
			{
					Debug.Log("Calling OnTurnCompleted with turnEnd ="+turnEnd);
					OnTurnCompleted(-1);
					return;
			}
		*/

            if (this.TurnText != null)
            {
                this.TurnText.text = this.turnManager.Turn.ToString();//何ターン目かを表示してくれる
            }

			if (this.turnManager.Turn > 0 && this.TimeText != null && ! IsShowingResults)//ターンが0以上、TimeTextがnullでない、結果が見えていない場合。
            {
                
				this.TimeText.text = this.turnManager.RemainingSecondsInTurn.ToString("F1") + " SECONDS";//小数点以下1桁の残り時間を表示。

				TimerFillImage.anchorMax = new Vector2(1f- this.turnManager.RemainingSecondsInTurn/this.turnManager.TurnDuration,1f);//残り時間のバーの表示。
            }

            
		}

		this.UpdatePlayerTexts();

        // show local player's selected hand//自分の手を見せる
        Sprite selected = SelectionToSprite(this.localSelection);
        if (selected != null)
        {
            this.localSelectionImage.gameObject.SetActive(true);
            this.localSelectionImage.sprite = selected;
        }

        // remote player's selection is only shown, when the turn is complete (finished by both)
        if (this.turnManager.IsCompletedByAll) //両方のプレイヤーがターンを終了しているか
        {
            selected = SelectionToSprite(this.remoteSelection);
            if (selected != null)
            {
                this.remoteSelectionImage.color = new Color(1,1,1,1); //不透明にします。
                this.remoteSelectionImage.sprite = selected; //相手の手を見せます。
            }
        }
        else
        {
			ButtonCanvasGroup.interactable = PhotonNetwork.room.PlayerCount > 1;

            if (PhotonNetwork.room.PlayerCount < 2) //対戦相手がいない場合?
            {
                this.remoteSelectionImage.color = new Color(1, 1, 1, 0);//完全に透明にします。
            }

            // if the turn is not completed by all, we use a random image for the remote hand
            else if (this.turnManager.Turn > 0 && !this.turnManager.IsCompletedByAll)//ターンが0以上で、ターンが終わってない場合
            {
                // alpha of the remote hand is used as indicator if the remote player "is active" and "made a turn"
                PhotonPlayer remote = PhotonNetwork.player.GetNext(); //??
                float alpha = 0.5f; //半透明にします
                if (this.turnManager.GetPlayerFinishedTurn(remote))//相手の(remote)プレイヤーがターンを終了しているかどうか。
                {
                    alpha = 1;//不透明
                }
                if (remote != null && remote.IsInactive)//remoteがnullの場合で、かつ、remoteアクティブでない場合。
                {
                    alpha = 0.1f;//ほぼ透明
                }

                this.remoteSelectionImage.color = new Color(1, 1, 1, alpha);//前に定義したアルファ値で透明度が決まる
                this.remoteSelectionImage.sprite = SelectionToSprite(randomHand);
            }
        }

    }

    #region TurnManager Callbacks

    /// <summary>Called when a turn begins (Master Client set a new Turn number).</summary>
    public void OnTurnBegins(int turn)//ターンが開始すると呼ばれるコールバックメソッド、マスタークライアントがターン番号を決める
    {
        Debug.Log("OnTurnBegins() turn: "+ turn);
        this.localSelection = Hand.None;//自分のハンドを初期化
        this.remoteSelection = Hand.None;//相手のハンドを初期化

        this.WinOrLossImage.gameObject.SetActive(false);//勝敗イメージを消す

        this.localSelectionImage.gameObject.SetActive(false);//自分の選択した手は消す
        this.remoteSelectionImage.gameObject.SetActive(true);//相手のは残しておく??

		IsShowingResults = false;//結果はまだ見えてないよ
		ButtonCanvasGroup.interactable = true;//ボタンを押せるよ
    }


    public void OnTurnCompleted(int obj)//ターン終了時に呼ばれるメソッド
    {
        Debug.Log("OnTurnCompleted: " + obj);

        this.CalculateWinAndLoss();//勝敗判定をして
        this.UpdateScores();//スコアを足して
        this.OnEndTurn();//エンドターンに必要な処理をします
    }


    // when a player moved (but did not finish the turn)
    public void OnPlayerMove(PhotonPlayer photonPlayer, int turn, object move)//手を決めたけど、ターンが終わらない場合??
    {
        Debug.Log("OnPlayerMove: " + photonPlayer + " turn: " + turn + " action: " + move); //??
        throw new NotImplementedException(); //要求されたメソッドまたは操作が実装されない場合にスローされる例外。
    }


    // when a player made the last/final move in a turn
    public void OnPlayerFinished(PhotonPlayer photonPlayer, int turn, object move)//ターン内に最後の手を決めた場合
    {
        Debug.Log("OnTurnFinished: " + photonPlayer + " turn: " + turn + " action: " + move);

        if (photonPlayer.IsLocal)
        {
            this.localSelection = (Hand)(byte)move;
        }
        else
        {
            this.remoteSelection = (Hand)(byte)move;
        }
    }



    public void OnTurnTimeEnds(int obj)//時間が終わった時に呼ばれるメソッド
    {
		if (!IsShowingResults)//結果が見えていなければ
		{
			Debug.Log("OnTurnTimeEnds: Calling OnTurnCompleted");
			OnTurnCompleted(-1);//ターンとしてカウントしない??
		}
	}

    private void UpdateScores()//スコアの計算
    {
        if (this.result == ResultType.LocalWin)
        {
            PhotonNetwork.player.AddScore(1);   // this is an extension method for PhotonPlayer. you can see it's implementation
        }
    }

    #endregion

    #region Core Gameplay Methods

    
    /// <summary>Call to start the turn (only the Master Client will send this).</summary>
    public void StartTurn()//ターン開始メソッド(マスタークライアントのみ呼べる)
    {
        if (PhotonNetwork.isMasterClient)
        {
            this.turnManager.BeginTurn();//turnmanagerに新しいターンを始めさせる
        }
    }
	
    public void MakeTurn(Hand selection)//手を決めるメソッド
    {
        this.turnManager.SendMove((byte)selection, true);//アクションを送るときに呼ぶ(アクション,ターンを終了するかどうか(true))
    }
	
    public void OnEndTurn()//エンドターンのメソッド
    {
        this.StartCoroutine("ShowResultsBeginNextTurnCoroutine");//コルーチン呼び出し
    }

    public IEnumerator ShowResultsBeginNextTurnCoroutine()//結果を見せて次のターンに行くコルーチン
    {
		ButtonCanvasGroup.interactable = false;//手を選択できないようにして
		IsShowingResults = true;//結果が見えている
       // yield return new WaitForSeconds(1.5f);

        if (this.result == ResultType.Draw)//引き分けの場合
        {
            this.WinOrLossImage.sprite = this.SpriteDraw;//引き分けのスプライトにする
        }
        else//それ以外は
        {
            this.WinOrLossImage.sprite = this.result == ResultType.LocalWin ? this.SpriteWin : SpriteLose;//勝ち負けのスプライトにする
        }
        this.WinOrLossImage.gameObject.SetActive(true);//スプライトイメージをアクティブにする

        yield return new WaitForSeconds(2.0f);//二秒表示する

        this.StartTurn();//ターンを開始する
    }


    public void EndGame()
    {
		Debug.Log("EndGame");
    }

    private void CalculateWinAndLoss()//計算のメソッド
    {
        this.result = ResultType.Draw;
        if (this.localSelection == this.remoteSelection)
        {
            return;
        }

		if (this.localSelection == Hand.None)
		{
			this.result = ResultType.LocalLoss;
			return;
		}

		if (this.remoteSelection == Hand.None)
		{
			this.result = ResultType.LocalWin;
		}
        
        if (this.localSelection == Hand.Rock)
        {
            this.result = (this.remoteSelection == Hand.Scissors) ? ResultType.LocalWin : ResultType.LocalLoss;
        }
        if (this.localSelection == Hand.Paper)
        {
            this.result = (this.remoteSelection == Hand.Rock) ? ResultType.LocalWin : ResultType.LocalLoss;
        }

        if (this.localSelection == Hand.Scissors)
        {
            this.result = (this.remoteSelection == Hand.Paper) ? ResultType.LocalWin : ResultType.LocalLoss;
        }
    }

    private Sprite SelectionToSprite(Hand hand)//手を決めるスプライトの何か??
    {
        switch (hand)
        {
            case Hand.None:
                break;
            case Hand.Rock:
                return this.SelectedRock;
            case Hand.Paper:
                return this.SelectedPaper;
            case Hand.Scissors:
                return this.SelectedScissors;
        }

        return null;
    }

    private void UpdatePlayerTexts()//プレイヤーの勝った回数に関するメソッド??
    {
        PhotonPlayer remote = PhotonNetwork.player.GetNext();
        PhotonPlayer local = PhotonNetwork.player;

        if (remote != null)
        {
            // should be this format: "name        00"
            this.RemotePlayerText.text = remote.NickName + "        " + remote.GetScore().ToString("D2");
        }
        else
        {

			TimerFillImage.anchorMax = new Vector2(0f,1f);
			this.TimeText.text = "";
            this.RemotePlayerText.text = "waiting for another player        00";
        }
        
        if (local != null)
        {
            // should be this format: "YOU   00"
            this.LocalPlayerText.text = "YOU   " + local.GetScore().ToString("D2");
        }
    }

    public IEnumerator CycleRemoteHandCoroutine()//STARTで呼ばれるコルーチンの内容以下
    {
        while (true)
        {
            // cycle through available images
            this.randomHand = (Hand)Random.Range(1, 4);//ランダムに手を出す
            yield return new WaitForSeconds(0.5f);//0.5秒ごとに行う
        }
    }

    #endregion

これで大体の全体の流れと使えるコールバックメソッドが分かったので、これのパーツを切り貼りして、新たなスクリプトを作ろうと思いました。

   

そこで、まっさらなスクリプト(INSCore.cs)に必要最低限のものだけを書いた状態が下記です。

 

正確にいうと、IPunTurnManagerCallbacksを入れたところで、エラーが出てしまい、無理やり1,2,3,4,5、のコールバックメソッドを追加されたので、最低限必要だということだと思います。

   

using System; // 注意
using System.Collections;
using Photon; // 注意
using UnityEngine;
using UnityEngine.UI; //注意

public class INSCore : PunBehaviour, IPunTurnManagerCallbacks// このコールバックを使用する際は1,2,3,4,5を実装しなければならない
{

    private PunTurnManager turnManager;

    public void Start()
    {
        this.turnManager = this.gameObject.AddComponent<PunTurnManager>();//PunTurnManagerをコンポーネントに追加
        this.turnManager.TurnManagerListener = this;//リスナーを?
        this.turnManager.TurnDuration = 5f;//ターンは5秒にする

    }


    public void OnPlayerFinished(PhotonPlayer player, int turn, object move)//1
    {
        throw new NotImplementedException();
    }

    public void OnPlayerMove(PhotonPlayer player, int turn, object move)//2
    {
        throw new NotImplementedException();
    }

    public void OnTurnBegins(int turn)//3
    {
        throw new NotImplementedException();
    }

    public void OnTurnCompleted(int turn)//4
    {
        throw new NotImplementedException();
    }

    public void OnTurnTimeEnds(int turn)//5
    {
        throw new NotImplementedException();
    }
        
    void Update()
    {
        
    }
}

これで、一応エラーは出ていない状態になっていて、かつ、起動すると、PunTurnManagerがコンポーネントに追加されることが確認できます。ここから、このスクリプトに情報を足していけば良さそうな感じがします。

   

まずは、ターンのタイマーがきちんと動くようにしてあげたいと思い、必要な部分だけを書いてあげると、下記のようにタイマーとターンのカウントが動くようになりました。

   

これで、タイマーの使用に関して、必要な部分がどこであるかということが特定できたということになります。

 

これを利用して今後は交代制のターンを取り入れたいと思います。以下、タイマー部分のソースです。

   

using System; // 注意
using System.Collections;
using Photon; // 注意
using UnityEngine;
using UnityEngine.UI; //注意

public class INSCore : PunBehaviour, IPunTurnManagerCallbacks// このコールバックを使用する際は1,2,3,4,5を実装しなければならない
{

    [SerializeField]
    private RectTransform TimerFillImage;//タイマーの赤い部分

    [SerializeField]
    private Text TurnText;//ターン数の表示テキスト

    [SerializeField]
    private Text TimeText;//残り時間の表示テキスト

    private bool IsShowingResults;//真偽値




    private PunTurnManager turnManager;

    public void Awake()// StartをAwakeにする。
    {
        this.turnManager = this.gameObject.AddComponent<PunTurnManager>();//PunTurnManagerをコンポーネントに追加
        this.turnManager.TurnManagerListener = this;//リスナーを?
        this.turnManager.TurnDuration = 5f;//ターンは5秒にする

    }


    void Update()
    {

        if (this.TurnText != null)
        {
            this.TurnText.text = this.turnManager.Turn.ToString();//何ターン目かを表示してくれる
        }

        if (this.turnManager.Turn > 0 || this.TimeText != null && !IsShowingResults)//ターンが0以上、TimeTextがnullでない、結果が見えていない場合。
        {

            this.TimeText.text = this.turnManager.RemainingSecondsInTurn.ToString("F1") + " SECONDS";//小数点以下1桁の残り時間を表示。

            TimerFillImage.anchorMax = new Vector2(1f - this.turnManager.RemainingSecondsInTurn / this.turnManager.TurnDuration, 1f);//残り時間のバーの表示。
        }
    }




    public void OnPlayerFinished(PhotonPlayer photonPlayer, int turn, object move)//1
    {
        Debug.Log("OnTurnFinished: " + photonPlayer + " turn: " + turn + " action: " + move);
    }

    public void OnPlayerMove(PhotonPlayer photonPlayer, int turn, object move)//2
    {
        Debug.Log("OnPlayerMove: " + photonPlayer + " turn: " + turn + " action: " + move);
       
    }

    public void OnTurnBegins(int turn)//3 ターンが開始した場合
    {
        Debug.Log("OnTurnBegins() turn: " + turn);

        IsShowingResults = false;
    }

    public void OnTurnCompleted(int obj)//4
    {
        Debug.Log("OnTurnCompleted: " + obj);
        
    }

    public void OnTurnTimeEnds(int turn)//5 タイマーが終了した場合
    {
        this.StartTurn();
    }

    public void StartTurn()//ターン開始メソッド(シーン開始時にRPCから呼ばれる呼ばれるようにしてあります。)
    {
        if (PhotonNetwork.isMasterClient)
        {
            this.turnManager.BeginTurn();//turnmanagerに新しいターンを始めさせる
        }
    }

}

それから、必要な テキストとグラフィックを適宜インスペクター上に流し込んであげています。

   

もしよろしければ、Photonについて私が知りえる限りの情報を徹底的にまとめてみたので、下記の記事も読んで下さいね!
 
おすすめの記事