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阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,074评论 4 62
  • 前段时间,研究了一下UNet,经过项目实践,大致整理了下遇到的问题。 UNet常见概念简介 Spawn:简单来说,...
    道阻且长_行则将至阅读 3,258评论 0 10
  • 转载: 对小女孩玲玲来说,明天有一件天大的事要发生了!晚上睡觉前,玲玲被妈妈唠叨忘了拿饭盒出来洗、袜子也乱丢,但这...
    夏苏的花园阅读 1,783评论 4 2
  • 2017年6月28日 中午 2018年夏天 二十二岁 一路上,当时羁绊,也是成长,很快……
    HP派派阅读 150评论 0 2