界面设计
初始界面
背景暂时放一张糖豆人的海报,界面这部分是和小组其他成员一起制作的。其他成员的博客可以在小组的综述里找到。
登录注册
请忽略后面的黄色字体和充满违和感的素材。还没有到GUI美化阶段。
大厅界面
创建房间界面
见下下下面↓
GUI与PUN代码结构(调用关系)
AppManager.cs负责各种监听函数、GUI显示逻辑等。
Launcher.cs继承PunCallbacks,实现Pun相关的内容。
这两部分代码搭配使用。
GUI操作→调用pun;
pun结果→GUI反馈。
项目代码
下面进行大厅功能的开发。
当加入主服务器后,我们需要的功能是加入大厅并且显示房间列表而不是直接随机加入到某个房间中。
而且如果要进行创建房间,必须要先在大厅/主服务器中才可以。
修改OnConnectedToMaster
回调:
public override void OnConnectedToMaster()
{
Debug.Log("连接到主服务器! this msg is from `OnConnectedToMaster()`");
//GUI提示--message:欢迎上线!
//UI.Show("欢迎上线");
PhotonNetwork.JoinLobby();//连接大厅
lobbyPanel.gameObject.SetActive(true);//开启大厅面板
startPanel.gameObject.SetActive(false);//关闭登录面板
}
lobbyPanel:(粗略的制作了一下,GUI美化在后期)
中间的列表区域是一个Unity的UI对象ScrollView。(已删除水平滚动条)
房间会被加入到Content中显示,别忘了给Content加一个GridLayoutGroup组件。并且调整内部元素的大小。
房间列表的显示可以通过pun官方的回调函数OnRoomListUpdate
来写。
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
for (int i = 0; i < gridLayoutGroup_content.childCount; i++)
{
if (gridLayoutGroup_content.GetChild(i).gameObject.GetComponentInChildren<Text>().text == roomList[i].Name)//防止重复的房间显示
{
Destroy(gridLayoutGroup_content.GetChild(i).gameObject);
}
if (roomList[i].PlayerCount == 0)//剔除房间人数为0的房间显示
{
roomList.Remove(roomList[i]);
}
}
foreach (var room in roomList)//显示在list中的每个房间
{
//这里在学习官方demo后有改动。主要是房间预制体更详细了。
//此处大致略过即可。
GameObject newRoom = Instantiate(roomPrefab, gridLayoutGroup_content.position, Quaternion.identity);
newRoom.GetComponentInChildren<Text>().text = room.Name + "(" + room.PlayerCount + "/" + room.MaxPlayers + ")";
//UI设置父物体,添加在画布上。
newRoom.transform.SetParent(gridLayoutGroup_content);
}
}
点击创建房间后会弹出创建面板。
确定按钮的监听。
public void SureCreateBtn()
{
AudioManager.instance.Click();
if (roomNameInputField.text.Length<2)
{
//UI提示房间名不规范
return;
}
Launcher.instance.CreateRoom(roomNameInputField.text,4);
//第二个参数是byte类型,暂时默认为4
}
现在已经可以建立房间和显示房间了,但是还没有制作加入房间等功能。
另外提示一下,unity中导入Pun2插件后,可以找到官方的demo,里面具有游戏同步和房间大厅的代码,如下图
当然再Scenes里可以找到场景并且进行多人游玩。(前提是配置好pun的AppID等)
官方demo学习结果
通过上述官方demo的学习。
先讲一下大致框架:
创建房间后仍然调用Photon的官方API。
这里因为房间需要复用。把房间作为一个预制体。
这里只显示最基本的房间信息以及一个按钮。
有其他需要可以扩展。不过要结合pun框架哦。
在之前的OnRoomListUpdate
回调里,需要改一下了:
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
//...这些代码没变
//变动↓
foreach (var room in roomList)//显示在list中的每个房间
{
GameObject entry = Instantiate(roomEntryPrefab, gridLayoutGroup_content.position, Quaternion.identity);
entry.transform.SetParent(gridLayoutGroup_content);
//entry.transform.localScale = Vector3.one;
//调用entry(预制体)上挂的脚本的初始化方法,把房间名等信息传到UI。
entry.GetComponent<RoomEntry>().Initialize(room.Name, (byte)room.PlayerCount, room.MaxPlayers);
}
}
然后这样列表里会逐一显示“房间名称”、“在线人数/最大人数”、“加入按钮”的房间。
具体RoomPrefab内部布局这里不提及。UI按照个人意愿做就好了。可以使用UI的一些布局组件“Horizontal Layout Group”。
然后看一下挂在RoomPrefab上的脚本:
RoomEntry.cs:
//省略命名空间引用和类名
//预制体上的三个元素
public Text RoomNameText;
public Text RoomPlayersText;
public Button JoinRoomButton;
//私有变量存储房间名
private string roomName;
//unity的回调
public void Start()
{
//给按钮添加监听
JoinRoomButton.onClick.AddListener(() =>
{
if (PhotonNetwork.InLobby)
{
PhotonNetwork.LeaveLobby();//加入房间时离开大厅
}
//加入这个房间,roomName来自Initialize方法
//可回看OnRoomListUpdate回调的改动
PhotonNetwork.JoinRoom(roomName);
});
}
//由外部实例化一个房间的时候调用,更新这个预制体的信息
public void Initialize(string name, byte currentPlayers, byte maxPlayers)
{
roomName = name;
RoomNameText.text = name;
RoomPlayersText.text = currentPlayers + " / " + maxPlayers;
}
现在可以生成房间并且显示房间了。
那么创建房间或点击加入房间之后该显示什么呢?
下一步制作RoomPanel。
这个panel不需要做成预制体,根本不需要复用,用SetActive(bool)控制它的显示与否就可以了。
预制体包含:
- 房间名称的文本(可不要)
- 离开房间按钮
- 开始房间按钮(默认不显示,通过代码实现当全部玩家准备好后在房主那边显示)。
另外最重要的就是显示玩家了,而且还要有准备功能!
为此创建一个PlayerEntryPrefab。
共四个子UI:
一个用于区分玩家的颜色显示块。
玩家昵称。
准备与取消准备按钮。
准备状态图片。
但是代码就复杂起来了。也需要用到更高深的API。
如何知道玩家有没有准备?如何同步准备信息?如何给定当前房间的玩家一个永久对应的颜色?
官方demo是这么做的(有少许改动):
PlayerEntry脚本,挂在预制体上:
//PlayerEntry.cs
public void Start()
{
if (PhotonNetwork.LocalPlayer.ActorNumber != ownerId)
{
Debug.Log("这不是我,隐藏准备按钮,PlayerEntry.Start-ActorNumber!=ownerId");
PlayerReadyButton.gameObject.SetActive(false);
}
else
{
Hashtable initialProps = new Hashtable() { { Const.Player_Ready, isPlayerReady }, { Const.PLAYER_LIVES, Const.PLAYER_MAX_LIVES } };
PhotonNetwork.LocalPlayer.SetCustomProperties(initialProps);
PhotonNetwork.LocalPlayer.SetScore(0);
PlayerReadyButton.onClick.AddListener(() =>
{
isPlayerReady = !isPlayerReady;
SetPlayerReady(isPlayerReady);
//把个人信息准备情况的信息写到CustomProperties里
Hashtable props = new Hashtable() { { Const.Player_Ready, isPlayerReady } };
PhotonNetwork.LocalPlayer.SetCustomProperties(props);
if (PhotonNetwork.IsMasterClient)
{
//我是房主的话,执行一下OnePlayerReady,检测一下是不是全准备了
//全准备了就显示开始游戏按钮
FindObjectOfType<Launcher>().OnePlayerReady();
//Launcher.instance.OnePlayerReady();
}
});
}
}
public void SetPlayerReady(bool playerReady)
{
PlayerReadyButton.GetComponentInChildren<Text>().text = playerReady ? "Ready!" : "Ready?";
PlayerReadyImage.enabled = playerReady;
}
传递每个客户端的消息(准备情况),用的是SetCustomProperties
,这都是pun封装好的方法,用来修改这个玩家对应的一些属性,有监听修改的回调函数。玩家属性有改动则会执行这个回调。可以检测有没有都准备。
OnPlayerPropertiesUpdate()
:
public override void OnPlayerPropertiesUpdate(Photon.Realtime.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(Const.Player_Ready, out isPlayerReady))
{
//更新本地UI上这个玩家准备状态
entry.GetComponent<PlayerEntry>().SetPlayerReady((bool)isPlayerReady);
}
}
//检测一下是不是全准备了,是的话房主显示启动按钮
AMinstance.startGameBtn.gameObject.SetActive(CheckPlayersReady());
}
由以上代码,当全部玩家准备后,房主那边才可以点击开始游戏。
开始游戏按钮的监听里可以调用PhotonNetwork.LoadLevel("SceneName");
,启动游戏场景。按照前几篇博文,只要开启了同步加载场景,那么其他玩家会同步加载游戏。
官方文档学习笔记–退出房间(与实际需求有偏差,可不看)
首先要明确不同人数的游戏房间,地形大小也是不同的,软件应该为用户带来良好体验,而游戏中有足够大的空间会增强这一点。
还要实现一个退出房间的功能,先做一个GameManager预制体并附上一个脚本(不同场景中会复用GameManager),脚本包含点击事件对应的函数LeaveRoom
及退出房间的回调函数OnLeftRoom
。
同时做一个按钮预制体。为其监听器添加LeaveRoom
。
按钮制作这里可以修改RectTransform的锚点,按shift和alt键设置为top和stretch。RectTransform是UI特有组件。这里涉及到锚点/锚框的知识点,说白了就是相对布局与绝对布局的概念。
然后我们需要玩家加入或退出房间后加载场景,这用到两个回调函数OnPlayerEnteredRoom
和OnPlayerLeftRoom
。代码如下
public override void OnPlayerEnteredRoom(Player other)
{
Debug.LogFormat("OnPlayerEnteredRoom() {0}", other.NickName); // not seen if you're the player connecting
if (PhotonNetwork.IsMasterClient)
{
Debug.LogFormat("OnPlayerEnteredRoom IsMasterClient {0}", PhotonNetwork.IsMasterClient); // called before OnPlayerLeftRoom
LoadArena();
}
}
public override void OnPlayerLeftRoom(Player other)
{
Debug.LogFormat("OnPlayerLeftRoom() {0}", other.NickName); // seen when other disconnects
if (PhotonNetwork.IsMasterClient)
{
Debug.LogFormat("OnPlayerLeftRoom IsMasterClient {0}", PhotonNetwork.IsMasterClient); // called before OnPlayerLeftRoom
LoadArena();
}
}
#region Private Methods
void LoadArena()
{
if (!PhotonNetwork.IsMasterClient)
{
Debug.LogError("PhotonNetwork : Trying to Load a level but we are not the master Client");
}
Debug.LogFormat("PhotonNetwork : Loading Level : {0}", PhotonNetwork.CurrentRoom.PlayerCount);
PhotonNetwork.LoadLevel("Room for " + PhotonNetwork.CurrentRoom.PlayerCount);
}
#endregion
要注意PhotonNetwork.LoadLevel
是只有房主才能调用的方法。当所有客户端开启了同步加载场景后会自动跟随加载场景。
官方文档到此之后的代码与要完成的功能有点区别。
有一点要注意的是必要时可以用一个bool变量存储用户的连接状态并作为后续代码执行条件。避免用户在退出房间时执行了并不需要的功能。
bool isConnecting;
//...
isConnecting = PhotonNetwork.ConnectUsingSettings();
待更新点及总结
至于玩家颜色方面
通过Photon.Realtime.Player.GetPlayerNumber()
玩家编号来获得不同颜色。
//PlayerEntry.cs
public void OnEnable()
{
PlayerNumbering.OnPlayerNumberingChanged += OnPlayerNumberingChanged;
}
public void OnDisable()
{
PlayerNumbering.OnPlayerNumberingChanged -= OnPlayerNumberingChanged;
}
//委托对应的方法,每当玩家编号发生变化时调用?
//ownerId是私有变量。
private void OnPlayerNumberingChanged()
{
foreach (Photon.Realtime.Player p in PhotonNetwork.PlayerList)
{
if (p.ActorNumber == ownerId)
{
//修改UI上的颜色
PlayerColorImage.color = Launcher.GetColor(p.GetPlayerNumber());
}
}
}
但是好像涉及到玩家编号改变的情况(玩家进入退出房间),demo用了委托,还额外有一个"PlayerNumbering.cs"脚本,暂时还没看懂有什么用。
后续有时间再看一下吧。
下一步就可以学习游戏的同步了。这里才是重头戏。将会新开一篇博文。