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

来源:互联网 发布:网络电视盒子刷机软件 编辑:程序博客网 时间:2024/06/05 07:44

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

一.关卡装载。

必须了解的函数:


1.RequireComponent()

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


2.DontDestroyOnLoad()

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


?
1
2
3
4
5
6
//C#代码
public class example : MonoBehaviour {
    voidAwake() {
        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#设计模式)作为替代。

?
1
2
3
4
5
6
7
8
9
//C#实例
public class example : MonoBehaviour {
    voidOnMouseDown() {
        HingeJoint[] hinges = FindObjectsOfType(typeof(HingeJoint))as HingeJoint[];
        foreach(HingeJoint hinge inhinges) {
            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时,若没有组件接收该调用消息,则打印一条错误信息。

?
1
2
3
4
5
6
7
8
9
//C#实例
public class example : MonoBehaviour {
    voidApplyDamage(float damage) {
        print(damage);
    }
    voidExample() {
        gameObject.SendMessage("ApplyDamage", 5.0F);
    }
}

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

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

?
1
2
3
4
//Javascript实例
varsupportedNetworkLevels : String[] = [ "mylevel"];
vardisconnectedLevel : String = "loader";
privatevar lastLevelPrefix = 0;
?
1
2
3
4
5
6
7
functionAwake ()
{
    // Network level loading is done in a separate channel.
    DontDestroyOnLoad(this);
    networkView.group = 1;
    Application.LoadLevel(disconnectedLevel);
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
functionOnGUI ()
{
     if(Network.peerType != NetworkPeerType.Disconnected)
     {
          GUILayout.BeginArea(Rect(0, Screen.height - 30, Screen.width, 30));
          GUILayout.BeginHorizontal();
          for(varlevel insupportedNetworkLevels)
          {
               if(GUILayout.Button(level))
               {
                    Network.RemoveRPCsInGroup(0);
                    Network.RemoveRPCsInGroup(1);
                    networkView.RPC("LoadLevel", RPCMode.AllBuffered, level, lastLevelPrefix + 1);
               }
          }
          GUILayout.FlexibleSpace();
          GUILayout.EndHorizontal();
          GUILayout.EndArea();
     }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RPC
functionLoadLevel (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(vargo inFindObjectsOfType(GameObject))
               go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);
}
?
1
2
3
4
5
functionOnDisconnectedFromServer ()
{
     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写的测试实例:

?
1
2
3
4
5
6
7
8
9
vartestStatus = "Testing network connection capabilities.";
vartestMessage = "Test in progress";
varshouldEnableNatMessage : String = "";
vardoneTesting = false;
varprobingPublicIP = false;
varserverPort = 9999;
varconnectionTestResult = ConnectionTesterStatus.Undetermined;
// Indicates if the useNat parameter be enabled when starting a server
varuseNat = false;
?
1
2
3
4
5
6
7
functionOnGUI() {
    GUILayout.Label("Current Status: "+ testStatus);
    GUILayout.Label("Test result : "+ testMessage);
    GUILayout.Label(shouldEnableNatMessage);
    if(!doneTesting)
        TestConnection();
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
functionTestConnection() {
    // Start/Poll the connection test, report the results in a label and
    // react to the results accordingly
    connectionTestResult = Network.TestConnection();
    switch(connectionTestResult) {
        caseConnectionTesterStatus.Error:
            testMessage ="Problem determining NAT capabilities";
            doneTesting =true;
            break;
        caseConnectionTesterStatus.Undetermined:
            testMessage ="Undetermined NAT capabilities";
            doneTesting =false;
            break;
        caseConnectionTesterStatus.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
        caseConnectionTesterStatus.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
            elseif (Time.time > timer) {
                probingPublicIP =false;        // reset
                useNat =true;
                doneTesting =true;
            }
            break;
        caseConnectionTesterStatus.PublicIPNoServerStarted:
            testMessage ="Public IP address but server not initialized, "+
                "it must be started to check server accessibility. Restart "+
                "connection test when ready.";
            break;
        caseConnectionTesterStatus.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;
        caseConnectionTesterStatus.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;
        caseConnectionTesterStatus.NATpunchthroughAddressRestrictedCone:
        caseConnectionTesterStatus.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的例子如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
functionCanConnectTo(type1: ConnectionTesterStatus, type2: ConnectionTesterStatus) : boolean
{
    if(type1 == ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted &&
        type2 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric)
        returnfalse;
    elseif (type1 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric &&
        type2 == ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted)
        returnfalse;
    elseif (type1 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric &&
        type2 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric)
        returnfalse;
    returntrue;
}

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


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

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


2.Time.time

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

?
1
2
3
4
5
6
7
8
9
10
11
public class example : MonoBehaviour {
    publicGameObject projectile;
    publicfloat fireRate = 0.5F;
    privatefloat nextFire = 0.0F;
    voidUpdate() {
        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调用,后者可以对同步进行更多的控制。

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

0 0
原创粉丝点击