Unity新網絡Multiplayer

前言

隨著Unity版本的更新,新版的網絡系統Multiplayer也漸漸地越來越被重視,5.3.4版本測試很好用。使用這套網絡系統可以輕松開發聯機網絡游戲,而且其中封裝的API也針對于開發者的層次作了區分。HighLevelAPI(簡稱HLAPI)針對于簡單的網絡系統搭建,封裝的比較嚴重,只需輕松幾部即可完成網絡環境的搭建;LowLevelAPI(簡稱LLAPI)偏向底層,網絡環境的搭建,需要依靠底層類層層搭建,但較為靈活。根據不同的需求,可以選擇不同的API,當然通常HLAPI和LLAPI是混合起來一起用的。本文會簡單講解Multiplayer的API架構,主要通過項目將所有內容串聯。

  • HLAPI架構圖
    首先給大家看一下UnityAPI中提供的一張Multiplayer的HLAPI架構圖,清晰的了解我們常用的類的層次。


    HLAPI架構圖
    • Transport/Configuration — 底層API類
    • Connection/Reader/Writer — 消息發送類、序列化與反序列化類
    • NetworkClient/NetworkServer — 網絡環境搭建類
    • NetworkIDentity/NetworkBehaviour — 網絡對象狀態同步
    • NetworkManager — 網絡游戲控制(一個組件搞定一個網絡)
    • NetworkLobbyManager — 集成了網絡游戲大廳功能
    • NetworkTransform/NetworkAnimator — 引擎繼承的狀態同步組件
  • 使用基礎類(NetworkServer/NetworkClient)搭建網絡環境

    • 服務器端
      NetworkServer.Listen(7777);//創建服務器監聽本機網卡7777端口

    • 客戶端
      NetworkClient client;//創建客戶端對象
      client.Connect("127.0.0.1",7777);//連接服務器

  • 使用基礎類(NetworkServer/NetworkClient)創建網絡游戲對象


    服務器創建網絡對象卵生到客戶端
    • 客戶端
      ClientScene.Ready(msg.conn); //通知服務器已準備完畢
      ClientScene.RegisterPrefab(playerPrefab); //注冊網絡預設體
      ClientScene.AddPlayer(0); //通知服務器實例化預設體
    • 服務器(只有服務器才能創建網絡對象)
      GameObject player = (GameObject)Instantiate(playerPrefab);
      //給予該客戶端該對象的權限
      NetworkServer.AddPlayerForConnection(netMsg.conn, player, 0);
      //卵生[同步到其他客戶端]
      NetworkServer.Spawn(player);
  • 遠程過程調用(RPC)


    網絡環境下的遠程消息發送
    • Command:由客戶端發送給服務器[在服務器執行方法]

    • ClientRPC:由服務器發送給客戶端[在客戶端執行方法]

    • 客戶端調服務器方法(Command方法名必須以Cmd開頭)
      [Command]
      /// <summary>
      /// 發射炮彈
      /// </summary>
      void CmdFire ()
      {
      GameObject bullet = (GameObject)Instantiate (MyLobbyManager.instance.spawnPrefabs [0],firePoint.position, firePoint.rotation);
      bullet.GetComponent<Rigidbody> ().velocity = bullet.transform.forward * 20;
      NetworkServer.Spawn (bullet);
      }

    • 服務器調客戶端方法(ClientRPC方法必須以Rpc開頭)
      [ClientRpc]
      /// <summary>
      /// 播放特效稍后銷毀
      /// </summary>
      /// <param name="eff">Eff.</param>
      void RpcStopEffect (GameObject eff)
      {
      eff.GetComponent<ParticleSystem> ().Play ();
      Destroy (eff, 1.05f);
      }

當然是用NetworkManager/NetworkLobbyManager組件同樣可以搭建網絡環境,且更為方便,這里不再贅述,詳見項目。

  • 實戰項目坦克大戰


    坦克大戰游戲大廳

    坦克大戰主場景
  • 挑幾個重點腳本看看
    1.大廳管理

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;

public class MyLobbyManager : NetworkLobbyManager
{
    //單例
    public static MyLobbyManager instance;
    //是否開啟切換場景標志位
    public bool beginChange = false;
    //背景音樂
    public GameObject backAud;
    //玩家位置編號
    private int playerPositionIndex = 0;

    void Awake ()
    {
        instance = this;
    }

    void Start ()
    {
        DontDestroyOnLoad (backAud);
    }

    /// <summary>
    /// 當所有玩家都已準備完畢
    /// </summary>
    public override void OnLobbyServerPlayersReady ()
    {
        //啟動協程等待動畫播放完畢
        StartCoroutine (PlayProgress ());
        //遍歷所有客戶端發送播放指令
        foreach (NetworkLobbyPlayer item in lobbySlots) {
            if (item) {
                (item as MyLobbyPlayer).RpcBeginPlay ();
            }
        }
    }

    IEnumerator PlayProgress ()
    {
        //如果還沒有開始切換場景,繼續播放動畫,保持等待
        while (!beginChange) {
            yield return null;
        }
        base.OnLobbyServerPlayersReady ();
    }

    //當服務器添加玩家對象時調用
    public override void OnServerAddPlayer (NetworkConnection conn, short playerControllerId)
    {
        base.OnServerAddPlayer (conn, playerControllerId);
        //判斷是否在游戲場景而非游戲大廳
        if (beginChange) {
            //創建坦克
            GameObject player = Instantiate (gamePlayerPrefab) as GameObject;
            //通過新場景的NetworkStartPosition確定坦克的創建位置
            player.transform.position = startPositions [playerPositionIndex++].position;
            //設置坦克腳本中的網絡變量--坦克編號
            player.GetComponent<MyPlayer> ().tankNum = playerPositionIndex - 1;
            //給予客戶端該坦克的使用權限
            NetworkServer.AddPlayerForConnection (conn, player, playerControllerId);
            //卵生坦克
            NetworkServer.Spawn (player);
        }
    }
}

2.大廳玩家

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.UI;

public class MyLobbyPlayer : NetworkLobbyPlayer
{
    //玩家大廳名稱顯示
    private Transform content;
    //玩家大廳準備按鈕
    private Button readyButton;
    //單例LobbyManager
    private MyLobbyManager manager;
    //倒計時進度條
    private GameObject progress;

    void Awake ()
    {
        manager = MyLobbyManager.instance;
        content = GameController.instance.content.transform;
        readyButton = transform.GetChild (0).GetComponent<Button> ();
    }

    void Start ()
    {
        //設置當前對象到UI中顯示
        GameController.instance.SetParent (this.transform);
        //獲取進度條
        progress = transform.parent.parent.parent.GetChild (3).gameObject;
        //重置縮放
        transform.localScale = Vector3.one;
        //非主機客戶端更新玩家名稱
        if (!isServer) {
            CmdUpdateItemName ();
        }
    }

    [Command]
    public void CmdUpdateItemName ()
    {
        //服務器開始下發指令
        RpcUpdateItemName ();
    }

    [ClientRpc]
    public void RpcUpdateItemName ()
    {
        //非主機客戶端設置字體顏色
        content.GetChild (1).GetComponent<Image> ().color = Color.red;
        //非主機客戶端設置玩家名稱
        content.GetChild (1).GetChild (1).GetComponent<Text> ().text = "Player2";
    }

    /// <summary>
    /// 本地玩家執行
    /// </summary>
    public override void OnStartLocalPlayer ()
    {
        base.OnStartLocalPlayer ();
        //設置準備按鈕可用
        readyButton.interactable = true;
        //移除所有監聽
        readyButton.onClick.RemoveAllListeners ();
        //設置準備按鈕事件監聽
        readyButton.onClick.AddListener (OnReadyButtonClick);
    }

    /// <summary>
    /// 玩家準備按鈕點擊事件
    /// </summary>
    public void OnReadyButtonClick ()
    {
        //向服務器發送準備指令
        SendReadyToBeginMessage ();
        //移除該按鈕所有事件監聽
        readyButton.onClick.RemoveAllListeners ();
        //設置該按鈕取消準備的事件監聽
        readyButton.onClick.AddListener (OnNotReadyButtonClick);
    }

    /// <summary>
    /// 玩家取消準備按鈕點擊事件
    /// </summary>
    public void OnNotReadyButtonClick ()
    {
        //向服務器發送取消準備的指令
        SendNotReadyToBeginMessage ();
        //取消該按鈕的所有事件監聽
        readyButton.onClick.RemoveAllListeners ();
        //添加該按鈕準備的事件監聽
        readyButton.onClick.AddListener (OnReadyButtonClick);
    }

    /// <summary>
    /// 當客戶端主播完畢后調用
    /// </summary>
    /// <param name="readyState">If set to <c>true</c> ready state.</param>
    public override void OnClientReady (bool readyState)
    {
        base.OnClientReady (readyState);
        //如果準備好了
        if (readyState) {
            //按鈕文字顯示為Done
            readyButton.GetComponentInChildren<Text> ().text = "Done";
        } else {
            //否則顯示Ready
            readyButton.GetComponentInChildren<Text> ().text = "Ready";
        }
    }

    [ClientRpc]
    /// <summary>
    /// 客戶端開始播放切換場景動畫
    /// </summary>
    public void RpcBeginPlay ()
    {
        progress.SetActive (true);
    }
}

3.主場景玩家(坦克)

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.UI;

public class MyPlayer : NetworkBehaviour
{
    [SyncVar]
    //坦克編號
    public int tankNum = 0;

    [SyncVar]
    //坦克血量
    public int health = 100;
    //坦克移動速度
    public float tankMoveSpeed = 3f;
    //坦克旋轉速度
    public float tankTurnSpeed = 10f;
    //坦克發射的炮彈飛行速度
    public float fireSpeed = 20f;
    //聲音片段
    public AudioClip idle;
    public AudioClip run;

    private Rigidbody rig;
    //觀察點
    private Transform targetPoint;
    //坦克炮頭
    private Transform gun;
    //發射點
    private Transform firePoint;
    //操縱軸
    private float hor, ver, gunDir;
    //坦克血條顏色
    private Color[] colors = new Color[]{ Color.red, Color.green };
    //坦克血條背景圖片
    private Image healthColor;
    //坦克血條
    private Slider healthSlider;
    //結果UI
    private GameObject resultUI;
    //聲音片段
    private AudioSource aud;

    void Awake ()
    {
        rig = GetComponent<Rigidbody> ();
        aud = GetComponent<AudioSource> ();
        targetPoint = transform.Find ("TargetPoint");
        gun = transform.Find ("TankTurret");
        firePoint = transform.Find ("TankTurret/FirePoint");
        healthColor = transform.Find ("HealthCanvas/Slider/Fill Area/Fill").GetComponent<Image> ();
        healthSlider = transform.Find ("HealthCanvas/Slider").GetComponent<Slider> ();
        resultUI = GameObject.FindWithTag ("UI");
    }

    /// <summary>
    /// 本地玩家Start觸發
    /// </summary>
    public override void OnStartLocalPlayer ()
    {
        //如果是本地玩家
        if (isLocalPlayer) {
            //設置攝像機跟蹤點
            Camera.main.GetComponent<MyCameraFollow> ().SetTarget (targetPoint);
        }
    }

    [ClientCallback]
    void Update ()
    {
        //設置血條背景顏色
        healthColor.color = colors [tankNum];
        //設置血條值
        healthSlider.value = health;
        //如果是本地玩家
        if (isLocalPlayer) {
            //操縱坦克
            hor = Input.GetAxis ("Horizontal");
            ver = Input.GetAxis ("Vertical");
            gunDir = Input.GetAxis ("GunDirection");
            rig.MovePosition (transform.position + transform.forward * ver * Time.deltaTime * tankMoveSpeed);
            transform.eulerAngles += Vector3.up * hor * tankTurnSpeed;
            gun.transform.eulerAngles += Vector3.up * gunDir * tankTurnSpeed;
            //如果坦克移動
            if (hor != 0 || ver != 0) {
                if (aud.clip == idle) {
                    aud.Stop ();
                    aud.clip = run;
                } else {
                    if (!aud.isPlaying) {
                        aud.Play ();
                    }
                }
            } else {
                if (aud.clip == run) {
                    aud.Stop ();
                    aud.clip = idle;
                } else {
                    if (!aud.isPlaying) {
                        aud.Play ();
                    }
                }
            }
            //發射炮彈
            if (Input.GetKeyDown (KeyCode.Space)) {
                CmdFire ();
            }
        }
        //如果血量見底
        if (health <= 0) {
            //本地玩家失敗
            if (isLocalPlayer) {
                resultUI.transform.GetChild (0).gameObject.SetActive (true);
                resultUI.transform.GetChild (0).GetChild (0).GetComponent<Text> ().text = "GameOver";
                resultUI.transform.GetChild (0).GetChild (1).GetComponent<Text> ().text = "GameOver";
            }
            //非本地玩家勝利
            else {
                resultUI.transform.GetChild (0).gameObject.SetActive (true);
                resultUI.transform.GetChild (0).GetChild (0).GetComponent<Text> ().text = "Victory";
                resultUI.transform.GetChild (0).GetChild (1).GetComponent<Text> ().text = "Victory";
            }
        }
    }

    [Command]
    /// <summary>
    /// 發射炮彈
    /// </summary>
    void CmdFire ()
    {
        GameObject bullet = (GameObject)Instantiate (MyLobbyManager.instance.spawnPrefabs [0],
                                firePoint.position, firePoint.rotation);
        bullet.GetComponent<Rigidbody> ().velocity = bullet.transform.forward * 20;
        NetworkServer.Spawn (bullet);
    }
}
玩家準備界面

雙方玩家都已準備完畢倒計時

主場景開炮射擊

結束語

相比從前的老網絡系統,新版網絡解決了很多Bug,也進一步做了優化,沒有出現老網絡的尷尬問題,只是新網絡同步幀速率有些低,有時候會出現延遲較大的情況,這方面還有待改進。新網絡類多內容也多,感興趣的同學還需要多去看API,關于新網絡今后還會有續集喔,敬請期待。本次項目鏈接:https://pan.baidu.com/s/1hRC7C-diHdGeEtFnnMJQ-Q 密碼:rhjh

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,241評論 4 61
  • 前段時間,研究了一下UNet,經過項目實踐,大致整理了下遇到的問題。 UNet常見概念簡介 Spawn:簡單來說,...
    道阻且長_行則將至閱讀 3,324評論 0 10
  • 轉載: 對小女孩玲玲來說,明天有一件天大的事要發生了!晚上睡覺前,玲玲被媽媽嘮叨忘了拿飯盒出來洗、襪子也亂丟,但這...
    蘇夏的后花園閱讀 1,839評論 4 2
  • 2017年6月28日 中午 2018年夏天 二十二歲 一路上,當時羈絆,也是成長,很快……
    HP派派閱讀 159評論 0 2