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是枚举类型,其值如下表所示。

Unity学习系列笔记6:多人游戏进阶篇_第1张图片

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调用,后者可以对同步进行更多的控制。

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





你可能感兴趣的:(游戏,function,server,测试,服务器,NetWork)