Unityでは、個人でもオンライン対戦ゲームを作ることができます。
特に、ここでは”Photon”というサーバーを用いる方法を紹介します。
原理をしっかり学ぶ前に、
まずは実装したい!という方の助けになるかと思います。
1.作るモノ
サーバーにログインし、ルーム作成とルーム一覧をみる機能を実装する方法です。
触って試してみてください↓
このようなログインとマッチングシステムは、
オンライン対戦ゲームでは、なくてはならないものですよね?
わざわざシーン移動せず、Panelを切り替えて行います。
ゲームの流れは次のような形です。
1 ニックネームを決めてログイン
特定のデータを保存しながら遊ぶゲームもありますが、
ここでは、毎回ニックネームを決めて遊ぶ、シンプルなゲームを想定します。
ニックネームを決めて、ログインボタンをクリック。
2 遊ぶルームを決める
ルームとは、ゲームシーンを共有するグループです。
ルームを決めるのに、ここで3つの選択肢を与えます。
●ルーム作成
●ランダムプレイ(ルームがなければ、自動ルーム作成)
とにかくすぐに遊びたい人向け(俗にいうクイックプレイ)
●ルーム一覧
すでに知り合いがルームを作っている場合などは、ここからルームを探します。
3 ゲームをはじめる
ルームでは、その部屋に入るのが早い人がルームホストとなります。
ルームホストがゲームをスタートさせることができます。
それでは、Scriptの内容を中心に、
ロビー・ルーム・ゲームスタートまでを作っていきます。
2.宣言と必要なUIを作る
第一に、Projectファイルにて、LobbySceneという名のSceneを作ります。
「LobbyManager.cs」のC#scriptを作成し、空のGameObjectにアタッチします。
Scriptでは、まず下図のように必要なモノを宣言するので、
それを参考に必要なUIオブジェクト(TextやPanelなど)をシーンに作成します。
*Prefabは別途作成したテンプレのようなオブジェクトです。
次の要素をもつオブジェクトを作り、Resourcesフォルダに入れておきます。
シーンに配置するオブジェクト、LobbyManagerのInspectorは次のようになります。
3.処理をひたすらコーディング
シーンに必要なオブジェクトを作成したら、
Scriptに各種処理をコーディングしていきます。
(#region~#endregionで区切ります)
Unity Methods
まずは、通常のUnity Methodsです。
Start()で初めの画面を表示、
その時点のルームやプレイヤー情報をDictionaryというListに格納させてます。
AutomaticallySyncSceneで、MasterClient(ルームホスト)のロードするゲームシーンをルーム内の他Client(ユーザー)に共有されるようになります。
UI Callbacks
UI Callbacksでは基本的に、UIのButtonで呼ぶメソッドを記載してます。
その際に、どのような処理をさせるかを記載します。
OnLoginButtonClicked()は、「ログイン」ボタンで呼び出します。
入力されたニックネームをPhotonNetworkに送り、設定します。
入力がない場合、エラー(invalid)を返します。
OnCreateRoomButtonClicked()は、「ルーム作成」ボタンで呼び出します。
ルーム名が入力されなければ、Room●●●(1000~10000の番号が入る)として、
最大プレイ人数MaxPlayers(数字のみ)をroomOptionsに送信。
PhotonNetworkサーバー上に、roomNameとroomOptionsをもったルームを作成します。
OnCancelButtonClicked()は、「キャンセル」ボタンで呼び出します。
1つ前のGameOptions(3つの選択肢のある)画面のみをActiveにして、戻ります。
OnShowRoomListButtonClicked()は「ルームリスト」ボタンで呼び出します。
ロビーにいなければロビーに入り、ルームリストの画面をActiveにします。
OnBackButtonClicked()は「戻る」ボタンで呼び出します。
ロビーから出て、前の画面をActiveにします。
OnLeaveGameButtonClicked()は「退室」ボタンで呼び出します。
ルームを出ます。
OnJoinRandomGameButtonClicked()は「ランダムプレイ」ボタンで呼び出します。
画面を切り替え、ランダムに取得されたルームに入る、
または、自動でルーム作成の処理(PhotonNetwork.JoinRandomRoom)が行われます。
OnStartGameButtonClicked()は「ゲームスタート」ボタンで呼び出します。
押した人がルームホスト(MasterClient)であること確認し、
設定の”GemeScene”をロードします。
Photon Callbacks
本丸といえようPhoton Callbacksです!
サーバーに情報を送信すると、自動的に実行されるメソッドであり、
そのときに処理してほしい内容を記載します。
Debug.Logを置くことで、そのメソッドの実行有無を確認でき、
エラーの原因を知れるようになります。
(ゲーム開発が終わったら、デバッグは除きましょう)
少し長くなりますが、テンプレみたいなものだと思って、
意味を理解して、必要な箇所のみアレンジして使えばよいでしょう^^
OnConnected()でネットへの接続、
OnConnectedToMaster()でPhotonサーバーへの接続がなされたことを確認できます。
(Debug.Logが読まれたならば)
LocalPlayer.NickNameは、自分が入力したニックネームの情報です。
サーバー接続と同時に、GameOptionsの画面を表示させます。
OnCreatedRoom()でルームが作られたことを確認します。
OnJoinedRoom()は、ルームに入ったときに読まれるメソッドです。
このときにルーム内のPanelをActiveにし、
自身がルームホスト(MasterClient)であれば、startGameButton(「ゲームスタート」ボタン)が表示されるようになります。
roomInfoText.textに、"ルーム名"、"現在のプレイ人数&最大プレイ人数"という情報を渡し、表示させます。
次に、ルーム内にいる各プレイヤーに対して
playerListPrefab(プレイヤーカードみたいなもの)を生成して
playerGameObjectという変数に入れ、
playerListContentを配下に並べられ、表示されます。
さらに、各プレイヤーのActorNumber(ルームに入ると割り当てられるID)が読まれ、
自分(LocalPlayer)であるかどうかを判別し、PlayerIndicatorの表示を判断させます(この機能は別になくても困らない^^;)。
ルームにいるプレイヤーをActorNumberという番号と紐付けて、記憶します。
最後にplayerGameObjectsというListに、これらActorNumerとplayerGameObjectをセットで格納しておきます。
OnPlayerEnterRoom()は、すでに自分がルームにいて、他のプレイヤーが入室したときに呼ばれます。
OnJoinedRoom()と同様、
roomInfo.textの更新、playerListObjectの生成と整列、ActorNumberによる自分の識別、Listへの格納を行います。
後からプレイヤー入室しても、ルームホスト(MasterClient)は変わらないので、「ゲームスタート」ボタンの表示有無に関する処理はありません。
OnPlayerLeftRoom()は、ルームから自分以外のプレイヤーが退室したときに呼ばれます。
roomInfo.textの更新、退室したプレイヤー(otherplayer)をListから削除します。
ここで、自分がルームホストであるかのチェックをさせます。
OnLeftRoom()は、自分が退室したときに呼ばれます。
GameOptionsの画面(Panel)をアクティブに、
playerListGameObjectsという配列データは削除され、空になります(どこかに入室したときにまた更新される)。
OnRoomListUpdate()は、ルームを一望できるロビーに入ったとき(JoinLobby)に呼ばれます。
現存するルーム、つまり
(RoomInfoというクラス型のroomListという名のListに格納されているroom)を
1つずつ検証して有効なルームを精査し、
cachedRoomListというListに格納されていきます
さらに、cachedRoomListに格納されている各roomに対して、
roomListEntryPrefab(これまたカードみたいなもの)を生成、roomListEntryGameObjectという変数に入れ、
roomListParentGameObjectを配下に、並べられます。
それから、roomEntryGameObjectに、roomがもつ情報を反映させていきます。
最後に、roomListGameObjectsというListに格納されます。
Listが3つも登場するので、混乱するかもしれません^^;
イメージは次のようなものです。
全roomをもつList(roomList)
→ 有効なroomのList(cachedRoomList)
→ roomの情報が反映されたオブジェクトのList(roomListGameObjects)
OnLeftLobby()は、自分がロビーを出たときに呼ばれます。
ルーム一覧の情報は破棄され、
cachedRoomListに格納された情報も削除されます。
OnJoinRandomFailed()は、現存するルームに入室できなかったときに呼ばれます。
「ルーム作成」ボタンで呼び出したときと同じようにルーム作成の処理が行われますが、
ここでは設定画面をとばし、
コードの内容(ルーム名:Room ●●●●、最大プレイ人数:20)でルームが即作成されるようにしています。
Private Methods
サーバーの情報をもってきて、ローカルで呼ぶメソッドです。
OnJoinRoomButtonClicked()は、「Join」ボタンで呼び出します。
Photonoサーバーにおけるロビーからルームに移動する一連のメソッドを呼びます。
ClearRoomListView()は、Photon CallbacsのOnLeftLobby()で呼ばれます。
ルームを一覧に表示させていたroomListGameObjectsの中身を削除します。
(OnLeftLobby()では、cachedRoomListは削除される)
Public Methods
各種コードより、飛ばれるメソッドをここに記載します。
ActivePanel()は、画面切替を操作させるものです。
呼び出す際は、ActivatePanel(~.name)というように記載します。
初期画面(Login_UI_Panel.name)
ログイン後画面(GameOptions_UI_Panel.name)
ルーム作成画面(CreateRoom_UI_Panel.name)
ルーム内画面(InsideRoom_UI_Panel.name)
ロビー画面(RoomList_UI_Panel.name)
ランダムルーム検索中画面(JoinRandomRoom_UI_Panel.name)
以上
これはあくまで、私個人の理解に基づくものであることをご留意ください。
私がプロフェッショナルでないからこそ、
初心者にもわかりやすい説明になったかな?と思いたいのですが・・・^^;
サーバーに関して独学は、とても難しいですね。
4.コピペ用コード
以下は、コピペして使うのにご活用ください↓
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using Photon.Pun; using Photon.Realtime; public class LobbyManager : MonoBehaviourPunCallbacks { [Header("Connection Status")] public Text connectionStatusText; [Header("Login UI Panel")] public InputField playerNameInput; public GameObject Login_UI_Panel; [Header("Game Options UI Panel")] public GameObject GameOptions_UI_Panel; [Header("Create Room UI Panel")] public GameObject CreateRoom_UI_Panel; public InputField roomNameInputField; public InputField maxPlayerInputField; [Header("Inside Room UI Panel")] public GameObject InsideRoom_UI_Panel; public Text roomInfoText; public GameObject playerListPrefab; public GameObject playerListContent; public GameObject startGameButton; [Header("Room List UI Panel")] public GameObject RoomList_UI_Panel; public GameObject roomListEntryPrefab; public GameObject roomListParentGameObject; [Header("Join Random Room UI Panel")] public GameObject JoinRandomRoom_UI_Panel; private Dictionary<string, RoomInfo> cachedRoomList; private Dictionary<string, GameObject> roomListGameObjects; private Dictionary<int, GameObject> playerListGameObjects; #region Unity Methods // Start is called before the first frame update private void Start() { // Login_UI_Panel.SetActive(true); // GameOptions_UI_Panel.SetActive(false); ActivatePanel(Login_UI_Panel.name); cachedRoomList = new Dictionary<string, RoomInfo>(); roomListGameObjects = new Dictionary<string, GameObject>(); PhotonNetwork.AutomaticallySyncScene = true; } // Update is called once per frame private void Update() { connectionStatusText.text = "Connection status: " + PhotonNetwork.NetworkClientState; } #endregion #region UI Callbacks public void OnLoginButtonClicked() { string playerName = playerNameInput.text; if(!string.IsNullOrEmpty(playerName)) { PhotonNetwork.LocalPlayer.NickName = playerName; PhotonNetwork.ConnectUsingSettings(); } else { Debug.Log("PlayerName is invalid!"); } } public void OnCreateRoomButtonClicked() { string roomName = roomNameInputField.text; if(string.IsNullOrEmpty(roomName)) { roomName = "Room " + Random.Range(1000,10000); } RoomOptions roomOptions = new RoomOptions(); roomOptions.MaxPlayers = (byte)int.Parse(maxPlayerInputField.text); PhotonNetwork.CreateRoom(roomName,roomOptions); } public void OnCancelButtonClicked() { ActivatePanel(GameOptions_UI_Panel.name); } public void OnShowRoomListButtonClicked() { if(!PhotonNetwork.InLobby) { PhotonNetwork.JoinLobby(); } ActivatePanel(RoomList_UI_Panel.name); } public void OnBackButtonClicked() { if(PhotonNetwork.InLobby) { PhotonNetwork.LeaveLobby(); } ActivatePanel(GameOptions_UI_Panel.name); } public void OnLeaveGameButtonClicked() { PhotonNetwork.LeaveRoom(); } public void OnJoinRandomRoomButtonClicked() { ActivatePanel(JoinRandomRoom_UI_Panel.name); PhotonNetwork.JoinRandomRoom(); } public void OnStartGameButtonClicked() { if(PhotonNetwork.IsMasterClient) { PhotonNetwork.LoadLevel("GameScene"); } } #endregion #region Photon Callbacks public override void OnConnected() { Debug.Log("Connected to Internet"); } public override void OnConnectedToMaster() { Debug.Log(PhotonNetwork.LocalPlayer.NickName + "is connected to Photon"); ActivatePanel(GameOptions_UI_Panel.name); } public override void OnCreatedRoom() { Debug.Log(PhotonNetwork.CurrentRoom.Name + " is created."); } public override void OnJoinedRoom() { Debug.Log(PhotonNetwork.LocalPlayer.NickName + " joined to " + PhotonNetwork.CurrentRoom.Name); ActivatePanel(InsideRoom_UI_Panel.name); if(PhotonNetwork.LocalPlayer.IsMasterClient) { startGameButton.SetActive(true); } else { startGameButton.SetActive(false); } roomInfoText.text = "Room name: " + PhotonNetwork.CurrentRoom.Name + " " + "Players/Max.players:" + PhotonNetwork.CurrentRoom.PlayerCount + "/" + PhotonNetwork.CurrentRoom.MaxPlayers; if(playerListGameObjects == null) { playerListGameObjects = new Dictionary<int, GameObject>(); } foreach(Player player in PhotonNetwork.PlayerList) { GameObject playerListGameObject = Instantiate(playerListPrefab); playerListGameObject.transform.SetParent(playerListContent.transform); playerListGameObject.transform.localScale = Vector3.one; playerListGameObject.transform.Find("PlayerNameText").GetComponent<Text>().text = player.NickName; if(player.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber) { playerListGameObject.transform.Find("PlayerIndicator").gameObject.SetActive(true); } else { playerListGameObject.transform.Find("PlayerIndicator").gameObject.SetActive(false); } playerListGameObjects.Add(player.ActorNumber, playerListGameObject); } } public override void OnPlayerEnteredRoom(Player newPlayer) { roomInfoText.text = "Room name: " + PhotonNetwork.CurrentRoom.Name + " " + "Players/Max.players:" + PhotonNetwork.CurrentRoom.PlayerCount + "/" + PhotonNetwork.CurrentRoom.MaxPlayers; GameObject playerListGameObject = Instantiate(playerListPrefab); playerListGameObject.transform.SetParent(playerListContent.transform); playerListGameObject.transform.localScale = Vector3.one; playerListGameObject.transform.Find("PlayerNameText").GetComponent<Text>().text = newPlayer.NickName; if(newPlayer.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber) //ActorNumber=出入りで変わる識別番号 { playerListGameObject.transform.Find("PlayerIndicator").gameObject.SetActive(true); } else { playerListGameObject.transform.Find("PlayerIndicator").gameObject.SetActive(false); } playerListGameObjects.Add(newPlayer.ActorNumber, playerListGameObject); } public override void OnPlayerLeftRoom(Player otherPlayer) { roomInfoText.text = "Room name: " + PhotonNetwork.CurrentRoom.Name + " " + "Players/Max.players:" + PhotonNetwork.CurrentRoom.PlayerCount + "/" + PhotonNetwork.CurrentRoom.MaxPlayers; Destroy(playerListGameObjects[otherPlayer.ActorNumber].gameObject); playerListGameObjects.Remove(otherPlayer.ActorNumber); if(PhotonNetwork.LocalPlayer.IsMasterClient) { startGameButton.SetActive(true); } } public override void OnLeftRoom() { ActivatePanel(GameOptions_UI_Panel.name); foreach(GameObject playerListGameObject in playerListGameObjects.Values) { Destroy(playerListGameObject); } playerListGameObjects.Clear(); playerListGameObjects = null; } public override void OnRoomListUpdate(List<RoomInfo> roomList) { ClearRoomListView(); foreach(RoomInfo room in roomList) { Debug.Log(room.Name); if(!room.IsOpen || !room.IsVisible || room.RemovedFromList) { if(cachedRoomList.ContainsKey(room.Name)) { cachedRoomList.Remove(room.Name); } } else { if(cachedRoomList.ContainsKey(room.Name)) { cachedRoomList[room.Name] = room; } else { cachedRoomList.Add(room.Name, room); } } } foreach (RoomInfo room in cachedRoomList.Values) { GameObject roomListEntryGameObject = Instantiate(roomListEntryPrefab); roomListEntryGameObject.transform.SetParent(roomListParentGameObject.transform); roomListEntryGameObject.transform.localScale = Vector3.one; roomListEntryGameObject.transform.Find("RoomNameText").GetComponent<Text>().text = room.Name; roomListEntryGameObject.transform.Find("RoomPlayersText").GetComponent<Text>().text = room.PlayerCount + " / " + room.MaxPlayers; roomListEntryGameObject.transform.Find("JoinRoomButton").GetComponent<Button>().onClick.AddListener(()=> OnJoinRoomButtonClicked(room.Name)); roomListGameObjects.Add(room.Name, roomListEntryGameObject); } } public override void OnLeftLobby() { ClearRoomListView(); cachedRoomList.Clear(); } public override void OnJoinRandomFailed(short returnCode, string message) { Debug.Log(message); string roomName = "Room " + Random.Range(1000,10000); RoomOptions roomOptions = new RoomOptions(); roomOptions.MaxPlayers = 20; PhotonNetwork.CreateRoom(roomName,roomOptions); } #endregion #region Private Methods void OnJoinRoomButtonClicked(string _roomName) { if(PhotonNetwork.InLobby) { PhotonNetwork.LeaveLobby(); } PhotonNetwork.JoinRoom(_roomName); } void ClearRoomListView() { foreach(var roomListGameObject in roomListGameObjects.Values) { Destroy(roomListGameObject); } roomListGameObjects.Clear(); } #endregion #region Public Methods public void ActivatePanel(string panelToBeActivated) { Login_UI_Panel.SetActive(panelToBeActivated.Equals(Login_UI_Panel.name)); GameOptions_UI_Panel.SetActive(panelToBeActivated.Equals(GameOptions_UI_Panel.name)); CreateRoom_UI_Panel.SetActive(panelToBeActivated.Equals(CreateRoom_UI_Panel.name)); InsideRoom_UI_Panel.SetActive(panelToBeActivated.Equals(InsideRoom_UI_Panel.name)); RoomList_UI_Panel.SetActive(panelToBeActivated.Equals(RoomList_UI_Panel.name)); JoinRandomRoom_UI_Panel.SetActive(panelToBeActivated.Equals(JoinRandomRoom_UI_Panel.name)); } #endregion }
InputFieldで、
ニックネームやルームの日本語入力
プレイ人数設定での半角数字のみを受け付けるようにする方法は
次の記事で紹介しております。
特にWebGLでゲームを出力した場合、
日本語に関してさまざまな問題が発生するのです。
ま、日本語をそもそも使わなければよい話なんですけどね^^;