首先要说明的是,Unity本身的网络功能并不适合做MMO类型的游戏。如果要使用Unity作为MMO游戏的客户端,一般来说都是在C#中通过socket建立自定义的网络通信来实现。Unity本身的网络功能是为多人游戏设计的,这种游戏模式一般来说就是一个玩家建立游戏(这个玩家既是server,又是client),其他玩家连接进来。从带宽考虑一般同时支持的玩家数量小于64个(这个也不一定,要看游戏本身的设计)。这些玩家一般都在一个局域网内互联,但是如果使用MasterServer进行配对,也可以在Internet上相互连接。很多所谓的单机并支持多人连线的游戏基本上都是用这样的网络模式。Unity本身的网络功能是可以跨平台的,可以运行于PC,IOS,Android,并且支持不同平台的客户端通信。
Unity网络协议本身是一个比较高层的网络协议,他并不是独立存在的,而是与引擎的游戏对象结合在一起,为游戏提供网络功能支持。要说明Unity的网络协议,就先要说明一下在网络模式下游戏对象的管理。
在Unity网络模式下,创建一个游戏对象实例,是通过Network.Instantiate()函数来实现的。这个函数与Object.Instantiate()类似,会在本地创建一个游戏对象实例,并且还会在所有互联的主机上也同时创建同样的游戏对象实例,这些游戏对象具有相同的NetworkView ID。每个被创建出来的游戏对象都使用NetworkView.isMine来标识自己的身份。如果isMine为true,就表示此游戏对象是本机创建的;false则表示是其他主机创建,并同步到本机的。游戏对象的isMine身份与网络底层的server和client身份没有任何关系,在server和client上都可以有任何身份的游戏对象存在,这主要取决于游戏对象是谁创建的。如果要销毁游戏对象,应该调用Network.Destroy()来代替Object.Destroy(),他会在所有主机上销毁这个对象。
在Unity中不存在单纯的“发送给服务器”或“发送给客户端的”网络消息,Unity的网络协议是基于一个具体的游戏对象的通信协议,这就意味着所有的通信上下文都与一个具体的游戏对象有关。一旦在网络环境下创建了游戏对象,这些在各个主机上被创建的游戏对象之间就已经建立好了通信关系,任何一个主机上的游戏对象都可以与其他主机上的“自己”进行通信。不同的游戏对象之间的通信是独立的,就好像每个游戏对象都有一个独立的通信管道。
游戏对象之间的通信在形式上分为两种:
游戏对象中经常发生变化,并且可以被描述成对象属性的数据。状态同步的方向只能是isMine到非isMine。比如一个玩家角色对象hp属性,如果需要其他玩家能够看到,就可以使用状态同步来完成。当isMine的玩家角色的hp变化后,会自动更新这个属性给其他的主机上的同一个玩家角色对象。状态同步还支持保证和非保证模式。
在其他主机的相同游戏对象上执行一个远程函数调用。RPC一般用来通过网络通知一次性的游戏事件,比如”游戏对象A受到了30点伤害“等等。RPC的传输方向非常自由,可以在一个主机向任何其他主机,包括自己,发送RPC调用。
网络游戏对象管理+状态同步+RPC组成了Unity的网络协议,所有需要实现的多人游戏的网络功能都是构建在这套Unity网络协议的基础之上的。
在构建多人游戏过程中,除了需要通过网络传递一些基本数据类型的数据(int, string...),有时候还需要传递一个游戏对象的引用。比如游戏对象A通过RPC通知给其他主机他消灭了游戏对象B,这里B就是游戏对象的引用。类似的需要还有很多,那么我们如何传递这个引用呢?这就需要NetworkView.viewID了。NetworkView.viewID是一个NetworkViewID类型的对象,RPC本身支持传输这种数据类型。在发送方只要将B的NetworkView.viewID当作RPC的参数传递给接收方,接收方再通过NetworkView.Find()在本地将viewID转换成对应的游戏对象引用就可以了。
在单机游戏中,场景中一般会创建一个游戏的全局对象,来处理整体的游戏状态。比如关卡的完成情况,全局的事件处理等等。在多人游戏中,同样也需要这样的功能。一般来说,这个全局对象可以在server端创建成网络游戏对象,并复制给其他的client。这个全局对象一般用来处理以下功能:
多人游戏一般带有单人模式,而且他们很多游戏逻辑是相同的。在编写游戏逻辑代码时,除了必须的网络功能代码外,应该尽量掩盖单人与多人游戏的区别,使代码更好理解和维护。我们可以把单人游戏看成是只有一个本地客户端的多人游戏,并且屏蔽掉网络功能而已。
使用NetworkViewWrapper代替直接使用NetworkView,使isMine在单机模式下也有效,并且屏蔽掉单机模式的RPC功能。
using UnityEngine;
using System.Collections;
/** 包装NetworkView,使之能够兼容非网络模式. */
[RequireComponent(typeof(NetworkView))]
public class NetworkViewWrapper : MonoBehaviour
{
public int group
{
get { return networkView.group; }
set { networkView.group = value; }
}
public bool isMine
{
get
{
if(Network.peerType == NetworkPeerType.Disconnected)
return true;
return networkView.isMine;
}
}
public Component observed
{
get { return networkView.observed; }
set { networkView.observed = value; }
}
public NetworkPlayer owner
{
get { return networkView.owner; }
}
public NetworkStateSynchronization stateSynchronization
{
get { return networkView.stateSynchronization; }
set { networkView.stateSynchronization = value; }
}
public NetworkViewID viewID
{
get { return networkView.viewID; }
set { networkView.viewID = value; }
}
public static NetworkViewWrapper Find(NetworkViewID viewID)
{
NetworkView nvw = NetworkView.Find(viewID);
return (nvw == null) ? null : nvw.GetComponent();
}
public void RPC(string name, NetworkPlayer target, params object[] args)
{
if (Network.peerType == NetworkPeerType.Disconnected)
return;
networkView.RPC(name, target, args);
}
public void RPC(string name, RPCMode mode, params object[] args)
{
if (Network.peerType == NetworkPeerType.Disconnected)
return;
networkView.RPC(name, mode, args);
}
}
using UnityEngine;
using System.Collections;
public static class NetObject
{
public static Object instantiate(Object prefab, Vector3 position, Quaternion rotation)
{
if (Network.peerType == NetworkPeerType.Disconnected)
return Object.Instantiate(prefab, position, rotation);
else
return Network.Instantiate(prefab, position, rotation, 0);
}
public static void destroy(GameObject gameObject)
{
if (Network.peerType == NetworkPeerType.Disconnected)
Object.Destroy(gameObject);
else
Network.Destroy(gameObject);
}
}