【Unity工具,简单应用】Photon + PUN 2,做一个简单多人在线聊天室

【Unity工具,简单应用】Photon + PUN 2,做一个简单多人聊天室

前置知识,安装,及简单UI

大厅

  • 简单搭建一下大厅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。猜测可以通过转移所有权的方式保留聊天记录。

猜你喜欢

转载自blog.csdn.net/weixin_45775438/article/details/129709284