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

这篇博客探讨了Unity多人游戏中如何进行关卡装载,包括关键函数如RequireComponent(),DontDestroyOnLoad()等;连接测试方法,如Network.TestConnectionNAT();以及带宽优化策略,讲解了不同状态同步机制和相关集的概念。" 88541382,7362451,蓝桥杯Java决赛:最大平方十位数解题思路,"['算法竞赛', '深搜', '数学问题', 'Java编程', '编程竞赛']

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这篇笔记学习三个问题:多人游戏的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)
function FixedUpdate() {
     rigidbody.AddForce(Vector3.up);
}
?
1
2
3
4
5
6
7
//C#实例
[RequireComponent ( typeof (Rigidbody))]
public class PlayerScript : MonoBehaviour {
     void FixedUpdate()  {
         rigidbody.AddForce(Vector3.up);
     }
}


2.DontDestroyOnLoad()

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


?
1
2
3
4
5
6
//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#设计模式)作为替代。

?
1
2
3
4
5
6
7
8
9
//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时,若没有组件接收该调用消息,则打印一条错误信息。

?
1
2
3
4
5
6
7
8
9
//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.

?
1
2
3
4
//Javascript实例
var supportedNetworkLevels : String[] = [ "mylevel" ];
var disconnectedLevel : String = "loader" ;
private var lastLevelPrefix = 0;
?
1
2
3
4
5
6
7
function Awake ()
{
     // 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
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();
      }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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);
}
?
1
2
3
4
5
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写的测试实例:

?
1
2
3
4
5
6
7
8
9
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 ;
?
1
2
3
4
5
6
7
function OnGUI() {
     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
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的例子如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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实现子弹按一定时间间隔连续发射的例子:

?
1
2
3
4
5
6
7
8
9
10
11
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调用,后者可以对同步进行更多的控制。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值