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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

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