前置知识,安装,及简单UI
- 【Unity工具,简单学习】PUN 2,多人在线游戏开发,初步使用
- 需要有一定 UNITY 使用经验的开发者可以顺利阅读。
大厅
- 简单搭建一下大厅UI。
给Laucher
节点一个Launcher
脚本
-
Launcher
脚本如下,具体功能看注释
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun;
using Photon.Realtime;
public class Launcher : MonoBehaviourPunCallbacks
{
public string RoomName = "Room"; // 设置房间名,确保每次连接到同一个房间
private string gameVersion = "1";
[SerializeField]
private const byte maxPlayersPerRoom = 4; // 设置单房间最多玩家数
[SerializeField]
private GameObject controlPanel;
[SerializeField]
private GameObject progressLabel;
void Awake()
{
// Then let master server can use PhotonNetwork.LoadLevel()
// Everyone will see the same level
PhotonNetwork.AutomaticallySyncScene = true; // 确保该变量为 true,否则无法同步
progressLabel.SetActive(false);
controlPanel.SetActive(true);
}
public void Connect()
{
progressLabel.SetActive(true);
controlPanel.SetActive(false);
if (PhotonNetwork.IsConnected) // 若已连接,则直接加入到房间
{
PhotonNetwork.JoinOrCreateRoom(RoomName, new RoomOptions() { MaxPlayers = maxPlayersPerRoom }, default);
}
else
{
PhotonNetwork.ConnectUsingSettings(); // 用 PhotonServerSettings 来连接
PhotonNetwork.GameVersion = gameVersion;
}
}
public override void OnJoinRandomFailed(short returnCode, string message) // 若加入随机房间失败
{
Debug.Log("PUN Basics Tutorial/Launcher:OnJoinRandomFailed() was called by PUN. No random room available, so we create one.\nCalling: PhotonNetwork.CreateRoom");
// #Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room.
PhotonNetwork.CreateRoom(RoomName, new RoomOptions() { MaxPlayers = maxPlayersPerRoom });
}
public override void OnJoinedRoom() // 若加入了某房间,则加载聊天室场景,不要使用 UNITY的加载场景方法
{
Debug.Log("PUN Basics Tutorial/Launcher: OnJoinedRoom() called by PUN. Now this client is in a room.");
PhotonNetwork.LoadLevel("ChatingRoom");
}
public override void OnConnectedToMaster() // 运行ConnectUsingSettings()后会先连接到 Master节点,再创建或加载房间
{
Debug.Log("PUN Basics Tutorial/Launcher: OnConnectedToMaster() was called by PUN");
PhotonNetwork.JoinOrCreateRoom(RoomName, new RoomOptions() { MaxPlayers = maxPlayersPerRoom }, default);
}
public override void OnDisconnected(DisconnectCause cause) // 若失去连接后
{
progressLabel.SetActive(false);
controlPanel.SetActive(true);
Debug.LogWarningFormat("PUN Basics Tutorial/Launcher: OnDisconnected() was called by PUN with reason {0}", cause);
}
}
- 需要注意的是
PhotonNetwork.JoinOrCreateRoom(RoomName, new RoomOptions() { MaxPlayers = maxPlayersPerRoom }, default);
方法,若该房间名的房间不存在则创建,否则加载该房间。 - 在输入框中,把该字符串赋值给
PhotonNetwork.NickName
即表示该玩家的名称,需非空PhotonNetwork.NickName = defaultName;
聊天室
- 聊天室的UI如下
添加一个GameManagerPUN
的物体并给予它对应的脚本组件
-
GameManagerPUN
脚本如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Photon.Pun;
using Photon.Realtime;
public class GameManagerPUN : MonoBehaviourPunCallbacks
{
public string DialogueText = "DialogueText";
[SerializeField]
private InputField _inputField;
public GameObject Content;
void Start()
{
GameObject go = PhotonNetwork.Instantiate("Sphere", Vector3.zero, Quaternion.identity);
go.GetComponent<ChangeRandomPosition>().change(); // 用 PhotonNetwork.Instantiate 创建一个物体,让它的位置随机变一下,代表显示该房间内的玩家个数
}
public override void OnLeftRoom()
{
SceneManager.LoadScene("LobbyScene");
}
public void LeaveRoom() // 退出按钮的监听器直接调用该代码即可调用 OnLeftRoom() 方法,退出大厅
{
PhotonNetwork.LeaveRoom();
}
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
}
}
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
}
}
public void SendMessage() // 发送消息,记录发送消息者名称,与它发送的消息,然后创建一个UI物体,加载到滚动content中
{
string res = PhotonNetwork.NickName + " : " + _inputField.text;
GameObject obj = PhotonNetwork.Instantiate(DialogueText, Vector3.zero, Quaternion.identity);
obj.GetComponent<Text>().text = res;
obj.GetComponent<SentenceAsync>().str = res;
obj.transform.SetParent(Content.transform);
_inputField.text = ""; // 发送玩后清空输入框
}
}
-
同步文本信息脚本
SentenceAsync
如下,需要实现IPunObservable
接口的OnPhotonSerializeView
方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using UnityEngine.UI;
public class SentenceAsync : MonoBehaviourPunCallbacks, IPunObservable
{
public string str;
public GameManagerPUN gm;
public GameObject pa;
void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
stream.SendNext(str);
}
else
{
str = (string)stream.ReceiveNext();
if (pa == null) // 用于在不同客户端的同步处理
{
pa = GameObject.Find("Content");
this.gameObject.transform.SetParent(pa.transform);
this.GetComponent<Text>().text = str;
}
}
}
}
- 没错,该同步脚本纠结了我好几个小时,才成功处理对。为什么这么麻烦呢?
简单同步
- 同步的基本原理比较复杂, 所幸该
Photon + Pun2
给我们封装的差不多了,我们只需要知道基础的内容即可 - 简单来说,假设有客户端
A,B,C
,他们都连接服务器X
在客户端A
处,玩家创建了一个球,若只是普通创建,则只有A
处的玩家能看到该球。
若希望每个客户端都能看到该球,首先需要使用PhotonNetwork.Instantiate(str, Vector3, Quaternion)
方法进行同步创建。注意创建物体通过给定它的字符串str
,该预设体的路径必须在该PhotonUnityNetworking / Resources
文件夹下:
- 第二步,指定该物体需要同步什么信息。对于该球,我们只需要同步它的位置信息即可。
物体若需要同步信息,则必须创建Photon View
脚本。
物体若需要同步信息,则必须创建Photon View
脚本。
物体若需要同步信息,则必须创建Photon View
脚本。
重要的信息重复三次
物体若需要同步transform
信息,则直接给它Photon Transform View
脚本即可
- 这里,
Ownership
表示该物体的所有权是否转移,比如A
创建了该物体,是否允许B
更改它的信息。这里还设计到主机与从机的区别。但这里我们不更改别的信息,设置 fixed` 即可 - 这里
Synchronization
选择Unreliable On Change
即可,Observable Search
选择同步的观察信息,可以直接选择Auto Find Active
或者设置Manual
然后手动给予它需要同步信息的脚本即可。 - 能被观察到的脚本必须实现
IPunObservable
接口,实现IPunObservable.OnPhotonSerializeView
方法。
较复杂同步
- 我们搜索一下已经有的脚本,发现官方直接支持我们如下的同步脚本:
- 也就是说,若需要同步的观察信息只有
Animator, Rigidbody, Transform
那么可以直接挂载相应的脚本进行同步。 - 那如果我们需要同步某数值信息,比如
int, float, string
呢?该三种信息同步相应来说比较容易
仍然,我们需要实现IPunObservable
接口,实现IPunObservable.OnPhotonSerializeView
方法
void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting) // 若流正在写,我们发送接下来要同步的信息 string str
{
stream.SendNext(str);
}
else
{
str = (string)stream.ReceiveNext(); // 接受流的下一个输出,并转化为 (string) 类型
if (pa == null) // (1)
{
pa = GameObject.Find("Content");
this.gameObject.transform.SetParent(pa.transform);
this.GetComponent<Text>().text = str;
}
}
}
- 我们发现,其他脚本都很好懂,那么
(1)
处是什么意思呢?
这里每行对话物体,由于挂载在Canvas
中,且挂载Text
组件,所以必须为Rect Transform
组件
尽管Rect Transform
组件是继承自Transform
组件,但是它的信息与Transform
组件截然不同,所以官方提供的Photon Transform View
不能使用(你挂载就会出bug,可以F12查看该脚本原码) - 难道我们去实现能同步
RectTransform
或者Text
信息的脚本?写起来有点麻烦…
但是我们转念一想,我们需要同步的信息只有 发送信息的字符串而已
所以我们只同步字符串。由于PhotonNetwork.Instantiate()
方法已经为我们创建了该同步物体,所以别的组件都是创建出来的,只是其中的信息没有同步而已 - 我们还需要同步哪个信息?
RectTransform?
不需要,由于我们使用ScrollView
,自动处理它的各种坐标信息Text?
我们只需给予其Text.text = str
即可,其他的字体啥信息都默认即可
还有一个隐藏的需要同步的信息 —— 该物体的父对象
。父对象由于我们没有同步,所以它默认会创建在世界根节点的下面,不会显示在ScrollView
下。
由于该ScrollView
下面的那些默认物体是每个玩家一开始就是一样的,所以我们只需要单纯通过(1)
指定,把自己挂载到该父对象上即可。
自定义同步
- 若需要自定义同步,由于
stream.ReceiveNext()
强转类型只支持默认的那三个int, float, string
,所以你想同步比如说Rect Transform
或者XXXscript
脚本,就需要去里面注册某物体类型 - 很麻烦,不怎么推荐,相当于指定某个类型的序列化和反序列化的规则,可以自己去搜集信息。
最终效果
- 我和室友,使用的是两台笔记本的两个客户端,在里面进行聊天
- 经过测试发现,若某玩家退出后,由该
owner
通过PhotonNetwork.Instantiate
创建的同步物体会自动Destroy
。猜测可以通过转移所有权的方式保留聊天记录。
文章来源地址https://www.toymoban.com/news/detail-472372.html
文章来源:https://www.toymoban.com/news/detail-472372.html
到了这里,关于【Unity工具,简单应用】Photon + PUN 2,做一个简单多人在线聊天室的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!