• Unity学习笔记6:多人游戏进阶篇


    这篇笔记学习三个问题:多人游戏的level loading,连接测试以及带宽优化。

    一.关卡装载。

    必须了解的函数:


    1.RequireComponent()

    //JavaScript实例
    // Mark the PlayerScript as requiring a rigidbody in the game object.
    //指定此script绑定的对象必须包含rigidbody组件,没有则自动添加
    @script RequireComponent(Rigidbody)
    function FixedUpdate() {
        rigidbody.AddForce(Vector3.up);
    }
    //C#实例
    [RequireComponent (typeof (Rigidbody))]
    public class PlayerScript : MonoBehaviour {
        void FixedUpdate()  {
            rigidbody.AddForce(Vector3.up);
        }
    }


    2.DontDestroyOnLoad()

    当装载一个新的level,旧场景的对象会被销毁,调用此函数可以在装载新level时保留某个对象,如果这个对象是GameObject或者Component那么它的整个transform层都将被保留。


    //C#代码
    public class example : MonoBehaviour {
        void Awake() {
            DontDestroyOnLoad(transform.gameObject);
        }
    }


    3.Network.peerType

    Network类的一个数据成员,返回值是NetworkPeerType的枚举类型,其值有NetworkPeerType.Disconnected//无客户端连接且没有初始化服务器
    NetworkPeerType.Connecting//正在连接服务器
    NetworkPeerType.Server//正在做为服务器运行
    NetworkPeerType.Client//正在作为客户端运行


    4.Network.RemoveRPCsInGroup()

    清除某个组的RPC buffer的所有RPC调用。


    5.Network.SetSendingEnabled (group : int, enabled : boolean)

    开启或关闭某个组的Network View的消息或RPC发送。比如要开始装载某个新level,与旧level相关的消息或RPC就可以停止发送。


    6.Network.IsMessageQueueRunning()

    开启或关闭消息队列的运行,如果关闭则RPC调用不再执行,状态同步机制也不工作。


    7.Network.SetLevelPrefix(prefix: int)

    为所有Network ViewID设置一个前缀。这将有效防止旧level的数据和新levle的数据互相干扰,这一效果不会造成网络带宽负担,但是由于Network ViewID添加了前缀,导致ID池数量有所减少。


    8.Object.FindObjectsOfType(type:Type)

    返回一个活动的对象列表(数组)。不返回assets和非活动的对象。这个函数会导致系统变慢,建议不要在每帧都使用,大多数情况下你都可以使用Singleton模式(参见C#设计模式)作为替代。

    //C#实例
    public class example : MonoBehaviour {
        void OnMouseDown() {
            HingeJoint[] hinges = FindObjectsOfType(typeof(HingeJoint)) as HingeJoint[];
            foreach (HingeJoint hinge in hinges) {
                hinge.useSpring = false;
            }
        }
    }


    9.SendMessage()

    GameObject.SendMessage(methodName : string, value : object = null, options : SendMessageOptions = SendMessageOptions.RequireReceiver)
    Component.SendMessage(methodName : string, value : object = null, options : SendMessageOptions = SendMessageOptions.RequireReceiver)

    调用methodName所指向的方法,若没有传递任何参数被调用的方法可以选择忽略。当SendMessageOptions设置为RequireReceiver时,若没有组件接收该调用消息,则打印一条错误信息。

    //C#实例
    public class example : MonoBehaviour {
        void ApplyDamage(float damage) {
            print(damage);
        }
        void Example() {
            gameObject.SendMessage("ApplyDamage", 5.0F);
        }
    }

    这些函数在下面的例子中会被用到。

    以下是一个简单的多人游戏关卡装载的例子,在关卡装载过程中必须确保不处理其他的network消息,在装载完毕一切就绪之前任何消息不得发送。关卡一旦装载好以后会自动发送一条消息到所有的脚本,通知它们关卡已经装载完毕。SetLevelPrefix()函数保证自动过滤与新装载关卡无关的network数据更新,这些无关数据更新可能是比如说上一个关卡的数据。本例还利用组功能分隔开游戏数据和关卡装载通信数据。组0用于游戏数据交通,而组1用于关卡装载。当关卡正在装载的时候,关闭组0开启组1.

    //Javascript实例
    var supportedNetworkLevels : String[] = [ "mylevel" ];
    var disconnectedLevel : String = "loader";
    private var lastLevelPrefix = 0;
    function Awake ()
    {
        // Network level loading is done in a separate channel.
        DontDestroyOnLoad(this);
        networkView.group = 1;
        Application.LoadLevel(disconnectedLevel);
    }
    function OnGUI ()
    {
         if (Network.peerType != NetworkPeerType.Disconnected)
         {
              GUILayout.BeginArea(Rect(0, Screen.height - 30, Screen.width, 30));
              GUILayout.BeginHorizontal();
              for (var level in supportedNetworkLevels)
              {
                   if (GUILayout.Button(level))
                   {
                        Network.RemoveRPCsInGroup(0);
                        Network.RemoveRPCsInGroup(1);
                        networkView.RPC( "LoadLevel", RPCMode.AllBuffered, level, lastLevelPrefix + 1);
                   }
              }
              GUILayout.FlexibleSpace();
              GUILayout.EndHorizontal();
              GUILayout.EndArea();
         }
    }
    @RPC
    function LoadLevel (level : String, levelPrefix : int)
    {
         lastLevelPrefix = levelPrefix;
              // There is no reason to send any more data over the network on the default channel,
              // because we are about to load the level, thus all those objects will get deleted anyway
              Network.SetSendingEnabled(0, false);
              // We need to stop receiving because first the level must be loaded first.
              // Once the level is loaded, rpc's and other state update attached to objects in the level are allowed to fire
              Network.isMessageQueueRunning = false;
              // All network views loaded from a level will get a prefix into their NetworkViewID.
              // This will prevent old updates from clients leaking into a newly created scene.
              Network.SetLevelPrefix(levelPrefix);
              Application.LoadLevel(level);
              yield;
              yield;
              // Allow receiving data again
              Network.isMessageQueueRunning = true;
              // Now the level has been loaded and we can start sending out data to clients
              Network.SetSendingEnabled(0, true);
              for (var go in FindObjectsOfType(GameObject))
                   go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);
    }
    function OnDisconnectedFromServer ()
    {
         Application.LoadLevel(disconnectedLevel);
    }
    @script RequireComponent(NetworkView)


    二.连接测试。

    Network.TestConnection (forceTest : boolean = false) : ConnectionTesterStatus
    此函数用于测试网络连接状况。
    返回值类型ConnectionTesterStatus是枚举类型,其值如下表所示。

    ConnectionTesterStatus的可能取值
    ConnectionTesterStatus的可能取值

    其中PublicIPIsConnectable,PublicIPPortBlocked以及PublicIPNoServerStarted是测试是否具有公共IP的结果,这个测试主要用于作为服务器运行的情形,因为即使没有Public IP,客户端通过NAT punchthough或者Facilitator等还是可以连接到服务器,但是要作为服务器运行就需要有Public IP.此项测试需要有一个已运行的服务器实例,然后测试服务器尝试连接该服务器的指定IP和端口。

    最下面4项是测试是否具备NAT punchthrough的能力,这个测试既用于服务器也用于客户端,不需要提前进行设置。其中Full cone type和Address-restricted cone type支持完整的NAT punchthrough功能,Port retricted不能连接到Symmetric,但可以连接到另外三种,Symmetric则只能与前两种连接。

    此函数提供的网络连接测试功能具有异步性,上一次调用的测试结果下一次调用才能获得结果。而且为了保证已完成的测试不被重复进行,下一次测试(比如网络状况发生变化时需要进行重新测试)需要给参数forceTest赋值true.

    下面是一个javascript写的测试实例:

    var testStatus = "Testing network connection capabilities.";
    var testMessage = "Test in progress";
    var shouldEnableNatMessage : String = "";
    var doneTesting = false;
    var probingPublicIP = false;
    var serverPort = 9999;
    var connectionTestResult = ConnectionTesterStatus.Undetermined;
    // Indicates if the useNat parameter be enabled when starting a server
    var useNat = false;
    function OnGUI() {
        GUILayout.Label("Current Status: " + testStatus);
        GUILayout.Label("Test result : " + testMessage);
        GUILayout.Label(shouldEnableNatMessage);
        if (!doneTesting)
            TestConnection();
    }
    function TestConnection() {
        // Start/Poll the connection test, report the results in a label and
        // react to the results accordingly
        connectionTestResult = Network.TestConnection();
        switch (connectionTestResult) {
            case ConnectionTesterStatus.Error:
                testMessage = "Problem determining NAT capabilities";
                doneTesting = true;
                break;
            case ConnectionTesterStatus.Undetermined:
                testMessage = "Undetermined NAT capabilities";
                doneTesting = false;
                break;
            case ConnectionTesterStatus.PublicIPIsConnectable:
                testMessage = "Directly connectable public IP address.";
                useNat = false;
                doneTesting = true;
                break;
            // This case is a bit special as we now need to check if we can
            // circumvent the blocking by using NAT punchthrough
            case ConnectionTesterStatus.PublicIPPortBlocked:
                testMessage = "Non-connectable public IP address (port " +
                    serverPort +" blocked), running a server is impossible.";
                useNat = false;
                // If no NAT punchthrough test has been performed on this public
                // IP, force a test
                if (!probingPublicIP) {
                    connectionTestResult = Network.TestConnectionNAT();
                    probingPublicIP = true;
                    testStatus = "Testing if blocked public IP can be circumvented";
                    timer = Time.time + 10;
                }
                // NAT punchthrough test was performed but we still get blocked
                else if (Time.time > timer) {
                    probingPublicIP = false;         // reset
                    useNat = true;
                    doneTesting = true;
                }
                break;
            case ConnectionTesterStatus.PublicIPNoServerStarted:
                testMessage = "Public IP address but server not initialized, "+
                    "it must be started to check server accessibility. Restart "+
                    "connection test when ready.";
                break;
            case ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted:
                testMessage = "Limited NAT punchthrough capabilities. Cannot "+
                    "connect to all types of NAT servers. Running a server "+
                    "is ill advised as not everyone can connect.";
                useNat = true;
                doneTesting = true;
                break;
            case ConnectionTesterStatus.LimitedNATPunchthroughSymmetric:
                testMessage = "Limited NAT punchthrough capabilities. Cannot "+
                    "connect to all types of NAT servers. Running a server "+
                    "is ill advised as not everyone can connect.";
                useNat = true;
                doneTesting = true;
                break;
            case ConnectionTesterStatus.NATpunchthroughAddressRestrictedCone:
            case ConnectionTesterStatus.NATpunchthroughFullCone:
                testMessage = "NAT punchthrough capable. Can connect to all "+
                    "servers and receive connections from all clients. Enabling "+
                    "NAT punchthrough functionality.";
                useNat = true;
                doneTesting = true;
                break;
            default:
                testMessage = "Error in test routine, got " + connectionTestResult;
        }
        if (doneTesting) {
            if (useNat)
                shouldEnableNatMessage = "When starting a server the NAT "+
                    "punchthrough feature should be enabled (useNat parameter)";
            else
                shouldEnableNatMessage = "NAT punchthrough not needed";
            testStatus = "Done testing";
        }
    }

    如果Server和Client的NAT类型可以被提前获知,那么可以不用TestConnection()函数,自己就能写出一个简单的比较函数,只要其中之一不是Symmetric类型,两台机器就可以进行连接。

    javascript的例子如下:

    function CanConnectTo(type1: ConnectionTesterStatus, type2: ConnectionTesterStatus) : boolean
    {
        if (type1 == ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted &&
            type2 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric)
            return false;
        else if (type1 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric &&
            type2 == ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted)
            return false;
        else if (type1 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric &&
            type2 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric)
            return false;
        return true;
    }

    其中用到的一些函数和变量:


    1.Network.TestConnectionNAT(forceTest : boolean = false)

    专用于测试NAT punchthrough,即使机器没有NAT IP(内部IP)地址,而拥有一个外部公共地址。


    2.Time.time

    Time.time的值是游戏开始运行到当前的时间。下面是一个利用Time.time实现子弹按一定时间间隔连续发射的例子:

    public class example : MonoBehaviour {
        public GameObject projectile;
        public float fireRate = 0.5F;
        private float nextFire = 0.0F;
        void Update() {
            if (Input.GetButton("Fire1") && Time.time > nextFire) {
                nextFire = Time.time + fireRate;
                GameObject clone = Instantiate(projectile, transform.position, transform.rotation) as GameObject;
            }
        }
    }


    三.优化带宽。

    带宽的使用量取决于你是否使用Reliable Delta Compressed或者Unreliable类型的状态同步机制。Unreliable模式下,整个被同步的对象在每次同步更新以后都按序传送,同步更新频率取决于Network.sendRate的值,默认情形下这个值为15.Unreliable模式下数据更新非常频繁但是不保证数据包的送达,如果有丢包处理方法是简单的忽略。此模式用于游戏舞台变化非常频繁迅速,并且少量的丢包可忽略的情形。Unreliable的带宽使用量还是比较大的,减少带宽的方法可以是降低数据传送率,15的默认值对一般游戏来说是个合适的取值。

    Reliable Delta Compressed模式下数据按序传输,且保证数据包不丢失,如果有丢包现象,会等待数据包重传,如果数据包没有按序到达,会存入缓冲等待尚未到达的数据。数据等待重传会一定程度的降低数据传输效率,但是因为数据是Delta Compressed,只有更改的数据才会被更新同步,如果数据无变化则没有数据包传送。所以Reliable Delta Compressed模式的效果取决于游戏的场景变化频繁程度。

    另一个问题是,什么样的数据需要被同步?这是一个足够游戏设计者发挥创意的问题,处理的好坏可以决定带宽的优化情况。一个例子是动画数据的同步,如果动画组件被Network View组件监视,动画属性的变化会被严格同步,动画的每一帧在每个客户端都表现相同。虽然这在某些情况下是可取的,但是一般情况下角色只需要知道角色处于走路,跳跃,跑步等等什么样的状态就足够,仅仅发送需要播放哪些动画帧的信息即可完成同步。这比同步整个动画组件要节省带宽得多。

    再者,什么时候需要同步?当两个玩家在分隔在地图两个不同的区域,他们在一段时间内不能见到彼此,那么这时候的同步完全没有多大必要。而在地图一个区域内物理上有机会互相接触的玩家则需要同步。这样可以节省部分带宽。这个概念被称为相关集(Relevant Sets),即在某个特定时间,某个特定的客户端只与整个游戏的一个子集(subset)相关。根据客户端的相关集来进行数据同步可以使用RPC调用,后者可以对同步进行更多的控制。

    在关卡装载的过程中不需要担心数据同步占用带宽的优化问题,因为每个玩家只需等待其他玩家都装载完毕即可,所以在关卡装载时常常涉及到传输大量的数据,比如图像和音频数据等。

  • 相关阅读:
    教你如何在Drcom下使用路由器上校园网(以广东工业大学、极路由1S HC5661A为例)
    selenium跳过webdriver检测并模拟登录淘宝
    无法嵌入互操作类型“Microsoft.Office.Interop.Excel.ApplicationClass”。请改用适用的接口
    使用xib制作界面有时会出现button无法点击,解决办法
    1.开博客的第一篇
    6.关于瀑布模型
    3.Freshman阶段学习内容的确定
    7.Solution的Build、Rebuild和Clean
    4.词法结构JavaScript权威指南笔记
    8.对于.NET的初步理解和介绍
  • 原文地址:https://www.cnblogs.com/definitelymaybe/p/3617778.html
Copyright © 2020-2023  润新知