🟥 实现目标
Demo:DemoAsteroids大厅的解析
🟧 大厅思路
Awake:设置同步场景的方式 登陆按钮:同步本地昵称、连接到服务器 连接到服务器回调:关闭登陆界面,打开 创建房间 / 加入房间 / 显示房间列表 的面板选择界面 选择创建房间界面的Btn:打开创建房间界面 创建房间界面:输入房间名、最大人数、拥有创建房间、返回的按钮 返回按钮:返回到功能选择界面 创建房间Btn:根据房间名、最大人数,创建服务器房间 创建/加入房间回调:打开房间内面板、实例化当前所有玩家的条形信息预制体,并将(昵称、是否准备)信息初始化到该预制体上的脚本上↓ 条形信息物体:上有脚本:保存了该玩家的 ID、昵称、准备信息 根据初始化的ID==本地玩家ID?不等于则不显示该预制体的准备按钮(即咱们不显示别人电脑的ready,咱们只能控制咱们的ready) 若等于,即代表着这个预制体是我们自己的。则显示准备按钮。且将准备信息等做为自定义的同步信息 根据本房间内,该玩家的Number,决定这个预制体是什么颜色。(demo设定最多8人,因此这有8种对应的case)【用PUN的 Number更新回调实现】 注意,这儿要用到PUN自带的脚本:PlayerNumbering,要将其挂在场景中 上有准备按钮: 每次点击,改变自身状态(是否关闭等)、同步自身是否准备信息 若自己是主服务器,则还可根据当前玩家是否都已准备,显示开始游戏按钮(检查是否都已准备,就是foreach所有玩家的准备信息,进行判断) 开始游戏按钮: 设置当前房间状态:不可再加入、大厅列表不可见(隐身) PUN同步加载场景 加入随机房间按钮:加入随机房间,显示服务器房间界面 返回按钮:退出服务器房间,返回到功能选择界面 显示房间列表按钮:使用加入大厅API,使PUN调用 刷新大厅列表 回调,在该回调中完成相关逻辑(该回调会传入所有房间列表缓存): 清空、删掉房间预制体、 根据从网络获得的缓存列表,判断房间是否可加入、可见性、标记性,删除不需要的房间,将需要的房间添加到本地房间列表。 更新实例化本地房间列表 开始游戏按钮状态:只有主客户端进行检测判断。(其他客户端没有开游戏的资格,自然不用检测) 主客户端点击准备时、 本地玩家进入房间时、(新玩家进来了,当然关闭按钮了) 其他玩家进入房间时、(新玩家进来了,当然关闭按钮了) 其他玩家离开房间时、 主客户端切换给别人时、 玩家属性更新时、(PUN回调) 房间列表更新时机: 显示房间列表信息按钮、 本地玩家退出大厅回调、 本地玩家离开房间回调、 其他玩家加入房间回调、 其他玩家离开房间回调、
🟨 主要脚本
该场景主要由这两个脚本实现功能
PlayerNumbering作为PUN实用脚本,挂载到场景中,配合我们写的代码。
LobbyMainPanel
using ExitGames.Client.Photon; using Photon.Realtime; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace Photon.Pun.Demo.Asteroids { public class LobbyMainPanel : MonoBehaviourPunCallbacks { #region Public Parameters [Header("Login Panel")] public GameObject LoginPanel; [Tooltip("玩家昵称输入框")] public InputField PlayerNameInput; [Header("房间操作界面")] public GameObject SelectionPanel; [Header("Create Room Panel")] public GameObject CreateRoomPanel; public InputField RoomNameInputField; public InputField MaxPlayersInputField; [Header("Join Random Room Panel")] public GameObject JoinRandomRoomPanel; [Header("Room List Panel")] public GameObject RoomListPanel; public GameObject RoomListContent; public GameObject RoomListEntryPrefab; [Header("Inside Room Panel")] public GameObject InsideRoomPanel; public Button StartGameButton; public GameObject PlayerListEntryPrefab; #endregion #region Private Parameters Dictionary<string, RoomInfo> cachedRoomList = new Dictionary<string, RoomInfo>(); Dictionary<string, GameObject> roomListEntries = new Dictionary<string, GameObject>(); Dictionary<int, GameObject> playerListEntries; #endregion #region Mono CallBacks public void Awake() { PhotonNetwork.AutomaticallySyncScene = true; PlayerNameInput.text = "Player " + Random.Range(1000, 10000); } #endregion #region PUN CallBacks public override void OnConnectedToMaster() { SetActivePanel(SelectionPanel.name); } //本地玩家进入房间时 public override void OnJoinedRoom() { print("OnJoinedRoom"); SetActivePanel(InsideRoomPanel.name); if (playerListEntries == null) playerListEntries = new Dictionary<int, GameObject>(); foreach (Player p in PhotonNetwork.PlayerList) { GameObject entry = Instantiate(PlayerListEntryPrefab); entry.transform.SetParent(InsideRoomPanel.transform); entry.transform.localScale = Vector3.one; entry.GetComponent<PlayerListEntry>().Initialize(p.ActorNumber, p.NickName); object isPlayerReady; if (p.CustomProperties.TryGetValue(AsteroidsGame.PLAYER_READY, out isPlayerReady)) { entry.GetComponent<PlayerListEntry>().SetPlayerReady((bool)isPlayerReady); } playerListEntries.Add(p.ActorNumber, entry); } StartGameButton.gameObject.SetActive(CheckPlayersReady()); Hashtable props = new Hashtable { {AsteroidsGame.PLAYER_LOADED_LEVEL, false} }; PhotonNetwork.LocalPlayer.SetCustomProperties(props); } //刷新列表回调 public override void OnRoomListUpdate(List<RoomInfo> roomList) { ClearRoomListView(); UpdateCachedRoomList(roomList); UpdateRoomListView(); } public override void OnLeftLobby() { cachedRoomList.Clear(); ClearRoomListView(); } public override void OnCreateRoomFailed(short returnCode, string message) { SetActivePanel(SelectionPanel.name); } public override void OnJoinRoomFailed(short returnCode, string message) { SetActivePanel(SelectionPanel.name); } public override void OnJoinRandomFailed(short returnCode, string message) { string roomName = "Room " + Random.Range(1000, 10000); RoomOptions options = new RoomOptions { MaxPlayers = 8 }; PhotonNetwork.CreateRoom(roomName, options, null); } public override void OnLeftRoom() { SetActivePanel(SelectionPanel.name); foreach (GameObject entry in playerListEntries.Values) { Destroy(entry.gameObject); } playerListEntries.Clear(); playerListEntries = null; } //其他玩家进入房间时 public override void OnPlayerEnteredRoom(Player newPlayer) { GameObject entry = Instantiate(PlayerListEntryPrefab); entry.transform.SetParent(InsideRoomPanel.transform); entry.transform.localScale = Vector3.one; entry.GetComponent<PlayerListEntry>().Initialize(newPlayer.ActorNumber, newPlayer.NickName); playerListEntries.Add(newPlayer.ActorNumber, entry); StartGameButton.gameObject.SetActive(CheckPlayersReady()); } public override void OnPlayerLeftRoom(Player otherPlayer) { Destroy(playerListEntries[otherPlayer.ActorNumber].gameObject); playerListEntries.Remove(otherPlayer.ActorNumber); StartGameButton.gameObject.SetActive(CheckPlayersReady()); } public override void OnMasterClientSwitched(Player newMasterClient) { if (PhotonNetwork.LocalPlayer.ActorNumber == newMasterClient.ActorNumber) { StartGameButton.gameObject.SetActive(CheckPlayersReady()); } } public override void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps) { if (playerListEntries == null) playerListEntries = new Dictionary<int, GameObject>(); GameObject entry; if (playerListEntries.TryGetValue(targetPlayer.ActorNumber, out entry)) { object isPlayerReady; if (changedProps.TryGetValue(AsteroidsGame.PLAYER_READY, out isPlayerReady)) { entry.GetComponent<PlayerListEntry>().SetPlayerReady((bool)isPlayerReady); } } StartGameButton.gameObject.SetActive(CheckPlayersReady()); } #endregion #region Public Methods //绑定到登陆按钮 public void OnLoginButtonClicked() { string playerName = PlayerNameInput.text; if (!playerName.Equals("")) { PhotonNetwork.LocalPlayer.NickName = playerName; PhotonNetwork.ConnectUsingSettings(); } else { Debug.LogError("Player Name is invalid."); } } //绑定到创建服务器房间按钮 public void OnCreateRoomButtonClicked() { string roomName = RoomNameInputField.text; roomName = (roomName.Equals(string.Empty)) ? "Room " + Random.Range(1000, 10000) : roomName; byte maxPlayers; byte.TryParse(MaxPlayersInputField.text, out maxPlayers); maxPlayers = (byte)Mathf.Clamp(maxPlayers, 2, 8); RoomOptions options = new RoomOptions { MaxPlayers = maxPlayers }; PhotonNetwork.CreateRoom(roomName, options, null); } //给玩家信息条的准备按钮使用。当本地玩家是主客户端时,执行。 public void LocalPlayerPropertiesUpdated() { StartGameButton.gameObject.SetActive(CheckPlayersReady()); } //绑定到开始游戏按钮 public void OnStartGameButtonClicked() { //当前房间不可再加入 PhotonNetwork.CurrentRoom.IsOpen = false; //让当前房间不可见:在大厅的列表中搜不到。(并且当你创建这个房间时,也可设为隐身的房间) PhotonNetwork.CurrentRoom.IsVisible = false; PhotonNetwork.LoadLevel("DemoAsteroids-GameScene"); } //绑定到加入随机房间按钮 public void OnJoinRandomRoomButtonClicked() { SetActivePanel(JoinRandomRoomPanel.name); PhotonNetwork.JoinRandomRoom(); } //绑定到显示列表按钮 public void OnRoomListButtonClicked() { if (!PhotonNetwork.InLobby) { //使用该API,使PUN调用 刷新大厅房间列表 回调,并在该回调完成相关逻辑。 PhotonNetwork.JoinLobby(); } SetActivePanel(RoomListPanel.name); } //绑定到创建房间界面的返回按钮 public void OnBackButtonClicked() { //这并不会执行,因为没加入大厅 if (PhotonNetwork.InLobby) { PhotonNetwork.LeaveLobby(); } SetActivePanel(SelectionPanel.name); } //绑定到加入随机房间界面的返回按钮上 public void OnLeaveGameButtonClicked() { PhotonNetwork.LeaveRoom(); } #endregion #region Private Methods void SetActivePanel(string activePanel) { LoginPanel.SetActive(activePanel.Equals(LoginPanel.name)); SelectionPanel.SetActive(activePanel.Equals(SelectionPanel.name)); CreateRoomPanel.SetActive(activePanel.Equals(CreateRoomPanel.name)); JoinRandomRoomPanel.SetActive(activePanel.Equals(JoinRandomRoomPanel.name)); RoomListPanel.SetActive(activePanel.Equals(RoomListPanel.name)); // UI should call OnRoomListButtonClicked() to activate this InsideRoomPanel.SetActive(activePanel.Equals(InsideRoomPanel.name)); } //检查所有玩家是否已经准备 bool CheckPlayersReady() { if (!PhotonNetwork.IsMasterClient) { return false; } foreach (Player p in PhotonNetwork.PlayerList) { object isPlayerReady; if (p.CustomProperties.TryGetValue(AsteroidsGame.PLAYER_READY, out isPlayerReady)) { if (!(bool)isPlayerReady) { return false; } } else { return false; } } return true; } //清空本地房间数据列表、删除房间预制体 void ClearRoomListView() { foreach (GameObject entry in roomListEntries.Values) { Destroy(entry.gameObject); } roomListEntries.Clear(); } //更新本地房间数据列表 void UpdateCachedRoomList(List<RoomInfo> roomList) { foreach (RoomInfo info in roomList) { // 如果缓存房间中该房间为关闭状态、不可见或被标记为已删除,则从缓存房间列表中删除该房间 if (!info.IsOpen || !info.IsVisible || info.RemovedFromList) { if (cachedRoomList.ContainsKey(info.Name)) { cachedRoomList.Remove(info.Name); } continue; } // Update cached room info if (cachedRoomList.ContainsKey(info.Name)) { cachedRoomList[info.Name] = info; } // Add new room info to cache else { cachedRoomList.Add(info.Name, info); } } } //实例化房间预制体 void UpdateRoomListView() { foreach (RoomInfo info in cachedRoomList.Values) { GameObject entry = Instantiate(RoomListEntryPrefab); entry.transform.SetParent(RoomListContent.transform); entry.transform.localScale = Vector3.one; entry.GetComponent<RoomListEntry>().Initialize(info.Name, (byte)info.PlayerCount, info.MaxPlayers); roomListEntries.Add(info.Name, entry); } } #endregion } }
PlayerListEntry
// -------------------------------------------------------------------------------------------------------------------- // <copyright file="PlayerListEntry.cs" company="Exit Games GmbH"> // Part of: Asteroid Demo, // </copyright> // <summary> // Player List Entry // </summary> // <author>developer@exitgames.com</author> // -------------------------------------------------------------------------------------------------------------------- using UnityEngine; using UnityEngine.UI; using ExitGames.Client.Photon; using Photon.Realtime; using Photon.Pun.UtilityScripts; namespace Photon.Pun.Demo.Asteroids { public class PlayerListEntry : MonoBehaviour { [Tooltip("玩家昵称")] public Text PlayerNameText; public Image PlayerColorImage; public Button PlayerReadyButton; public Image PlayerReadyImage; int ownerId; bool isPlayerReady; #region Mono CallBacks public void OnEnable() { //每次房间索引更新时调用 PlayerNumbering.OnPlayerNumberingChanged += OnPlayerNumberingChanged; } public void Start() { if (PhotonNetwork.LocalPlayer.ActorNumber != ownerId) { PlayerReadyButton.gameObject.SetActive(false); } else { Hashtable initialProps = new Hashtable() {{AsteroidsGame.PLAYER_READY, isPlayerReady}, {AsteroidsGame.PLAYER_LIVES, AsteroidsGame.PLAYER_MAX_LIVES}}; PhotonNetwork.LocalPlayer.SetCustomProperties(initialProps); //这将在本地设置分数,并将同步它在游戏中尽快。 PhotonNetwork.LocalPlayer.SetScore(0); PlayerReadyButton.onClick.AddListener(() => { isPlayerReady = !isPlayerReady; SetPlayerReady(isPlayerReady); Hashtable props = new Hashtable() {{AsteroidsGame.PLAYER_READY, isPlayerReady}}; PhotonNetwork.LocalPlayer.SetCustomProperties(props); //检测是否全员准备,是则显示开始游戏按钮 if (PhotonNetwork.IsMasterClient) { FindObjectOfType<LobbyMainPanel>().LocalPlayerPropertiesUpdated(); } }); } } public void OnDisable() { PlayerNumbering.OnPlayerNumberingChanged -= OnPlayerNumberingChanged; } #endregion public void Initialize(int playerId, string playerName) { ownerId = playerId; PlayerNameText.text = playerName; } private void OnPlayerNumberingChanged() { foreach (Player p in PhotonNetwork.PlayerList) { if (p.ActorNumber == ownerId) { PlayerColorImage.color = AsteroidsGame.GetColor(p.GetPlayerNumber()); } } } public void SetPlayerReady(bool playerReady) { PlayerReadyButton.GetComponentInChildren<Text>().text = playerReady ? "Ready!" : "Ready?"; PlayerReadyImage.enabled = playerReady; } } }