Unity3d网络教程(旧版networkView开发模式/附带项目源程序)

原文:

Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改

英文原版资源下载:

unity3d网络教程(英文PDF+项目源码)


释义文章:

Unity3D RPC(远程过程调用)细节__让你调用一个远程计算机的函数

unity3D-Network网络基础学习

unity圣典-network

unity圣典-networkView

unity圣典-MasterServer 主服务器

unity圣典-HostData 主机数据



个人提醒:Unity5.0之后,弃用了networkView组件,此教程较老(),部分组件或代码可能不再使用,但unity会兼容运行,适合入门,学通后建议继续学习5.x新版的network开发模式或socket开发等。部分不太理解的语句少做了修改,有疑问请查看英文版自行理解,用2.x以下版本可以完美运行


From:http://game.ceeger.com/forum/read.php?tid=428&page=3

 Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改

一、教程简介

        我一直认为unity需要一个好一点的多人网络的教程。当我开始用unity网络功能的时候,我感觉unity自带的例子太混乱了;一个好的网络功能的例子应该包括源文件,这样你可以迅速找到你需要的资料。由于这个想法,我决定参加UniKnowledge比赛并且终于完成了一个网络功能的教程,我希望这个教程包括了你所需要的所有的内容。
        这个教程介绍了很多案例;从最小的细节一直到真正的FPS游戏。我建议你从头到尾看一遍这个教程,不过如果你学东西很快的话,也可以自己看一下这些案例,如果需要更多细节,再回过头来看一下这个文档。




二、关于作者
        这个教程由M2H的Mike Hergaarden(Leepo)所写。我们已经使用unity两年多了,不过我们真正用unity进行正规开发只有最近的几个月。我们在最开始就在关注多人游戏的功能。实际上我们的第一个游戏就是多人在线游戏;其实很简单!我们的多人游戏有:Crashdrive 3D, Cratemania, Surrounded by Death, Verdun Online 还有最近我们正在搞的Hyberon。
        希望你能够享受这个教程。如果你你搞出什么名堂来,记得和我们联络哦。




三、如何使用这个教程
        和文档一起的还有一个压缩包,里面是教程中用到的案例的源文件。我们假设你已经知道怎么用unity编辑器和脚本,如果你不熟悉这些,请先去看unity的视频教程。
        多人游戏的debug很麻烦,因为你有两个机器在跑(服务器和客户端)这个项目。所以我们建议你在学习这个教程的时候,在编辑器里跑服务器端,在web里跑客户端。
        如果你想把教程中的源文件用在自己的项目里,注意这些文件已经针对教程进行了设置。在你自己的项目里,要确保Run in background选项被选中,这可以让你把服务器端在后端激活,避免进入睡眠状态。这样的话你就可以再后端跑服务器。不然的话你就没办法在跑客户端的时候同时在后端跑服务器。你可以打开这个选项在:Edit-Project settings-Player.

Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改




四、Tutorial 1:Connect &Disconnect

(教程1:连接于断开连接)

让我们开始吧
1.打开教程的第一个场景:这个场景在:Tutorial1/Tutorial_1. 这个场景包括了一个摄像机,一Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改个游戏物体和它的脚本,还有另一个物体用来显示场景标题。
2.Build一个webplayer然后运行
3.在编辑器里也开始跑同一个场景,然后点击:Start a server(用默认的IP和端口)
4.在webplayer里点击:Connect as client
5.你应该可以在你的两个项目里都看到:Connection status:Client! 还有:Connection status:Server! 恭喜啦,连接上了!


        简单吧;幸运的是这个脚本一点都不难。看一下脚本:Tutorial 1/Connect.js . 这个例子里用到的所有的代码都在OnGUI()函数里,看下这个函数,然后确定你明白这个函数是怎么工作的。这段代码挺简单的(如果你看的懂代码的话,嘿嘿),不过我们还是大致看一下这部分代码。

var connectToIP : String = "127.0.0.1";
var connectPort : int = 25001;

//Obviously the GUI is for both client&servers (mixed!)
function OnGUI ()
{
  if (Network.peerType == NetworkPeerType.Disconnected)
  {
       //We are currently disconnected: Not a client or host
      GUILayout.Label("Connection status: Disconnected");
  
      connectToIP = GUILayout.TextField(connectToIP, GUILayout.MinWidth(100));
      connectPort = parseInt(GUILayout.TextField(connectPort.ToString()));
  
      GUILayout.BeginVertical();
      if (GUILayout.Button ("Connect as client"))
      {
            //Connect to the "connectToIP" and "connectPort" as entered via the GUI
            //Ignore the NAT for now
   
           Network.Connect(connectToIP, connectPort);
       }
  
       if (GUILayout.Button ("Start Server"))
       {
            //Start a server for 32 clients using the "connectPort" given via the GUI
            //Ignore the nat for now 
   
           Network.InitializeServer(32, connectPort);
        }
       GUILayout.EndVertical();  
     }
     else
     {
          //We've got a connection(s)!
  
         if (Network.peerType == NetworkPeerType.Connecting)
        {
  
             GUILayout.Label("Connection status: Connecting");
   
         }
         else if (Network.peerType == NetworkPeerType.Client)
         {
   
             GUILayout.Label("Connection status: Client!");
             GUILayout.Label("Ping to server: "+Network.GetAveragePing( Network.connections[0] ) );  
   
         }
         else if (Network.peerType == NetworkPeerType.Server)
         {
   
             GUILayout.Label("Connection status: Server!");
             GUILayout.Label("Connections: "+Network.connections.length);
             if(Network.connections.length>=1)     
             {
                  GUILayout.Label("Ping to first player: "+Network.GetAveragePing(  Network.connections[0] ) );
              }   
          }
          if (GUILayout.Button ("Disconnect"))
         {
              Network.Disconnect(200);
          }
     }
 
}
// NONE of the functions below is of any use in this demo, the code below is only used for demonstration.
// First ensure you understand the code in the OnGUI() function above.
//Client functions called by Unity
function OnConnectedToServer()
{
      Debug.Log("This CLIENT has connected to a server"); 
}
function OnDisconnectedFromServer(info : NetworkDisconnection)
{
     Debug.Log("This SERVER OR CLIENT has disconnected from a server");
}
function OnFailedToConnect(error: NetworkConnectionError)
{
     Debug.Log("Could not connect to server: "+ error);
}

//Server functions called by Unity
function OnPlayerConnected(player: NetworkPlayer)
{
     Debug.Log("Player connected from: " + player.ipAddress +":" + player.port);
}
function OnServerInitialized()
{
     Debug.Log("Server initialized and ready");
}
function OnPlayerDisconnected(player: NetworkPlayer)
{
     Debug.Log("Player disconnected from: " + player.ipAddress+":" + player.port);
}

// OTHERS:
// To have a full overview of all network functions called by unity
// the next four have been added here too, but they can be ignored for now
function OnFailedToConnectToMasterServer(info: NetworkConnectionError)
{
     Debug.Log("Could not connect to master server: "+ info);
}
function OnNetworkInstantiate (info : NetworkMessageInfo)
{
     Debug.Log("New object instantiated by " + info.sender);
}
function OnSerializeNetworkView(stream : BitStream, info : NetworkMessageInfo)
{
 //Custom code here (your code!)
}

        脚本最上面的两个参数(connectToIP 和 connectPort)是用来对应GUI对话框里的用户输入,当用户点击链接按钮的时候,它们就会被调用。GUI函数分为4个部分:服务器,客户端已连接,客户端连接中,客户端断开。我们直接使用unity提供的状态:Network.peer.Type 来查看当前的链接状态。我们调用Network.Connect函数用来把客户端连接到服务器端,这个函数包含IP,端口还有密码(可选项)作为参数。建立一个服务器也差不多,我们调用另一个函数:Network.InitializeServer。这个函数包含端口和允许的最大连接数量作为参数。注意这里,你在服务器运行的时候,总是可以把连接数调低,但是没有办法超过在服务器初始化时所设置的数值。在你连接服务器或者初始化服务器之前,还有一个选项需要注意:Network.useNat 你应该能在connection/initializing函数的代码上方看到它。

NAT connection(Network.useNat)

(NAT=网络地址转换)
        我们设置Network.useNat为false因为我们不想用Network Address Translation(网络地址转换)。NAT 在客户端处在路由器之后的时候很有用(内部局域网)。这个网络Demo应该只在局域网中运行;你肯定没办法连接你朋友家(除非你朋友有个无限制的防火墙/路由器)关于NAT的更多信息请看连接:http://unity3d.com/support/documentation/Components/net-MasterServer.html

        现在,最后的一段代码;这十来个函数,会被unity自行调用。其实你不需要它们,就算你把它们都删了,这个Demo还是一样能跑。前六个客户端和服务器端的函数应该很好懂;它们只被客户端或者服务器端调用,如果你想调用这些函数传送的参数,自己去查查unity的手册吧。
        最后的三个函数不一样,OnFailedToConnectToMasterServer当你不能连接到主服务器的时候被客户端调用,主服务器的信息在后面会提到。OnNetworkInstantiate被实例化的物体调用,这个在后面也会被提到。OnSerializeNetworkView是我们用来在服务器和客户端之间传送信息的两个方法之一。RPC调用你自己定义的网络信息或网络函数。下一个教程里我们会看一下序列化还有RPC调用。服务器和客户端要有相同的物体上绑定着相同的RPC。


        教程的最后看一下这几个函数:Network.Messages Sent,Class Variables 和Class Functions
http://unity3d.com/support/documentation/ScriptReference/Network.html
        现在你知道在哪里能找到这些参考信息了,恩~用户手册。我们已经大致介绍了大概75%的信息了,爽吧!





五、Tutorial 2: Sending messages

Tutorial 2A:服务器播放,客户端监视,非实例化。

Tutorial 2/Tutorial 2A1

Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改

        不要让这些标题吓到了,打开场景:Tutorial 2/Tutorial 2A1. 教程1中网络连接的脚本,现在已经放在:Connect物体上了。另外PlayerCube物体被赋予了Tutorial2A1.js脚本和NetworkView组件。每一个物体,只要需要接受或者发送网络信息的,都需要一个NetworkView组件。你可以在整个游戏中只使用一个NetworkView组件,然后用脚本引用它。但是这样太麻烦了,最简单就是给每个需要网络功能的物体都加一个组件。

Tutorial2A1.js脚本:

function Update(){
 
   //Only run this on the server
   if(Network.isServer)
   {
         //Only the server can move the cube!   
         var moveDirection : Vector3 = new Vector3(-1*Input.GetAxis("Vertical"),0,Input.GetAxis("Horizontal"));
        var speed : float = 5;
        transform.Translate(speed * moveDirection * Time.deltaTime);//now really move!
    } 
}

        跑一下这个demo,服务器和客户端都打开。客户端应该能看到服务器移动方块物体。神奇吧,其实这一切就是使用了NetworkView组件的observing(观察)参数,它监视了这个方块的移动。现在看一下方块物体上的Tutorial 2A1.js脚本。这段代码只能在服务器上跑(因为用了Network.isServer来检查是否为服务器端):当服务器端的玩家移动方块,它就会立刻移动。不过你也能看到客户端上方块的移动特别卡,但是不要担心,我们Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改回头会解决这个问题,现在先讲最基本的内容。
        现在,怎么让客户端知道服务器端的物体移动了呢?看一下附加在物体上的NetworkView组件。它检测了物体的transform(变换)属性。也就是说unity会自动发送物体的变换属性(包括了位置,旋转角度和缩放的Vector3数值)。它只会把信息从服务器端发送到客户端,反之就不行,因为服务器端独占了NetworkView的功能。客户端就不能发送信息,只能够接收。



        我们看一下NetworkView的其他选项,稍微总结一下。PlayerCube物体的Networkview组件中,State synchronization选项,被设定为Reliable compressed。这说明只有被观察的参数发生改变的时候,它才会发送信息。如果服务器端15分钟都诶有移动方块物体,它就绝对不会发送任何信息,智能吧~。如果设置成Unreliable,无论参数有没有变,它都一致在发送信息。最后一个,如果设置State synchronization 为Off,会完全停止NetworkView所有的网络同步行为。如果你的NetworkView组件没有在对物体进行监视,你可以把同步选项关闭(不过也不是必须的)。如果你不明白我们为什么需要这么一个关掉了同步选项的NetworkView组件,就这么给你说吧,因为“Remote Procedure Calls”(远程程序调用)需要一个NetworkView组件,但是并不需要State synchronization和observed选项。不过你还是可以把RPC和observed一起用。RPC的内容会在下面的教程2A3里讲到。基本上它就是一个你自己定义的网络信息收发机制。


Tutorial 2/Tutorial 2A2
        如果你想让方块物体沿着Y轴移动怎么办,或者你想控制unity同步的具体内容。跑一下教程2/教程2A2.这个游戏应该和之前一摸一样,但是后端的代码已经改变了。PlayerCube上的NetworkView组件现在监测的是“Tutorial2A2.js”脚本。

function Update()
{
 
    if(Network.isServer)
    {
         //Only the server can move the cube!   
         var moveDirection : Vector3 = new Vector3(-1*Input.GetAxis("Vertical"), 0,Input.GetAxis("Horizontal"));
        var speed : float = 5;
        transform.Translate(speed * moveDirection * Time.deltaTime);
      }
 
}

function OnSerializeNetworkView(stream : BitStream, info : NetworkMessageInfo)
{
     if (stream.isWriting)
     {
            //Executed on the owner of the networkview; in this case the Server
            //The server sends it's position over the network
  
           var pos : Vector3 = transform.position;  
           stream.Serialize(pos);//"Encode" it, and send it
         }
        else
        {
             //Executed on the others; in this case the Clients
             //The clients receive a position and set the object to it
             var posReceive : Vector3 = Vector3.zero;
           stream.Serialize(posReceive); //"Decode" it and receive it
           transform.position = posReceive;          
        }
}

        具体就是说,这个NetworkView组件现在在检测脚本内的“OnSerializeNetworkView”函数。看一下这个函数.我们现在明确的指定了我们想要监测的内容。你可以用这个函数来同步具体你需要的内容,再说一次,当你选中Reliable delta compressed的时候,只有当参数发生变化的时候才会被发送出去。

        OnSerializeNetworkView函数有点诡异,它虽然是用来发送和接受数据,但是unity会查看Networkview组件的使用者,然后决定你是不是能够发送数据,如果你是服务器端,就调用“stream.isWriting”部分的代码,你就可以发送数据。如果你是客户端,就调用“else”部分的代码,你就只能接收数据。

 

Tutorial 2/Tutorial 2A3

        这是我最喜欢的发送信息的方法,也是最后一个方法;Remote Procedure Calls。我之前提到过这个,你可以去看看Tutorial 2/Tutorial 2A3的例子,搞个明白到底是怎么一回事。这个Demo和之前两个实现了一样的功能。不过Networkview不再监视任何物体,同步选项也已经被关闭。秘密就在Tutorial 2A3.js这个脚本里,特别是这一行networkView.RPC("SetPosition", RPCMode.Others, transform.position);.

 

Tutorial 2A3.js脚本:

private var lastPosition : Vector3;
function Update()
{ 
     if(Network.isServer)
    {
         //Only the server can move the cube!   
        var moveDirection : Vector3 = new Vector3(-1*Input.GetAxis("Vertical"), 0,Input.GetAxis("Horizontal"));
        var speed : float = 5;
        transform.Translate(speed * moveDirection * Time.deltaTime);
  
         //Save some network bandwidth; only send an rpc when the position has moved more than X
         if(Vector3.Distance(transform.position, lastPosition)>=0.05)
        {
              lastPosition=transform.position;
   
               //Send the position Vector3 over to the others; in this case all clients
             networkView.RPC("SetPosition", RPCMode.Others, transform.position);
         }
     }
}

@RPC
function SetPosition(newPos : Vector3)
{
      //This RPC is in this case always called by the server,
      // but executed on all clients
 
     transform.position=newPos; 
}

        服务器调用了RPC,这个RPC会要求客户端调用“SetPosition”函数,同时这个RPC还包含这一个新的位置信息“transform.position”,然后所有的客户端都调用”SetPosition”这个函数。下面是整个移动的过程:
1.服务器端玩家按下按键,他控制的物体移动。(代码14-18行)
2.服务器用移动的数值和上次更新的数值比较,如果差距大于设置的最小值,就发送一个RPC给出了自己的所有人,这个RPC包含了新的物体位置。(代码20-25行)
3.所有的客户端接收到RPC的设置物体位置命令,并且得到其中包括的新位置参数,然后再让它们本地执行位置移动的代码。
4.现在无论服务器还是客户端,大家的物体都处在相同的位置了~!


        如果我们想要使用RPC函数,需要在脚本中这个函数的上面加上“@RPC”(C#里面是”[RPC]”).当发送一个RPC的时候,我们可以指定下列的接收器:

 RPCMode.Server     只发送给服务器
 RPCMode.Others   发送给除了调用者之外的所有人
 RPCMode.OthersBuffered   发送给除了调用者之外的所有人,暂存的内容
 RPCMode.All  发送给包括调用者在内的所有人
 RPCMode.AllBuffered  发送给包括调用者在内的所有人,暂存的内容

        暂存的内容,指的是无论何时新玩家连接到服务器,都将会接收到这个信息。一个包含暂存内容的RPC可以用于比如说生成玩家的时候。这个暂存的内容会被服务器记住,然后每个玩家连接到服务器的时候,都会先收到一个生成玩家的RPC,这个RPC会在这个刚连接的新玩家的客户端中,生成其他所有在他之间加入服务器的玩家。

        如果你大致已经明白上面讲的所有的内容的话,你已经很牛啦!我们已经讲完了所有的基础内容,现在可以关注一下细节问题了。



Tutorial 2B: 服务器和客户端(们)都播放,实例化。

        我们现在要研究一下FPS游戏的基本细节。我们需要搞一个多人游戏,可以包括服务器端的玩家在内,并且也要可以剔除服务器端的玩家,把服务器放在后台。所以我们决定当新的客户端连接到服务器的时候,再生成玩家,而不是把玩家设置成物体,直接放在场景中。打开场景“Tutorial 2/Tutorial 2B”,服务器端还是在编辑器中,客户端在web里,都打开。移动一下方块,看看在客户端和服务器端是不是都工作正常。
        PlayerCube物体已经从场景中移除了,我们新建了一个Spawnscript物体,并且给它赋予了Spawnscript.js脚本。

public var playerPrefab : Transform;
function OnServerInitialized()
{
     Spawnplayer();
}
function OnConnectedToServer()
{
     Spawnplayer();
}
function Spawnplayer()
{
      var myNewTrans : Transform = Network.Instantiate(playerPrefab,                transform.position, transform.rotation, 0);
}
/*当一个玩家从服务器上断开时,在服务器端调用*/
function OnPlayerDisconnected(player: NetworkPlayer)
{
      Debug.Log("Clean up after player " + player);
      Network.RemoveRPCs(player);
      Network.DestroyPlayerObjects(player);
}
/*当失去连接或从服务器端断开时,在客户端调用*/
function OnDisconnectedFromServer(info : NetworkDisconnection)
{
/*这两句操作是无效的,只能删除本端场景中的自身player物体,会报错。因为已经断开连接,发送不了信息。*/
      Debug.Log("Clean up a bit after server quit");
      Network.RemoveRPCs(Network.player);
      Network.DestroyPlayerObjects(Network.player);
       
       Application.LoadLevel(Application.loadedLevel);
}

        当玩家(包括服务器和客户端)开始的时候,这个生成玩家的脚本会生成我们指定的预设物体(这里就是生成玩家-其实是方块)。生成脚本包含了位置,旋转角度和物体所在小组的信息。生成的物体会复制Spawnscripts物体本身的位置和角度信息,并且设置小组号为0(现在不用关心小组的事儿)。当我们断开连接的时候,会移除所有生成的预设物体。谁调用的Network.Instantiate谁就会自动获得这个函数本次生成的物体。这样我们就可以正确的控制不同的方块物体(服务器端和客户端就不会混在一起)。
        “Tutorial_2B_Playerscript.js”脚本使用了Tutorial 2AB的代码,不同的地方是,只有物体的所有者的输入才会被监测。





六、Tutorial 3: Authoritative servers(权威性服务器)
Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改
        之前的服务器设置,被称之为“非权威性”服务器;服务器对所有的网络信息没有任何控制权,客户端会和服务器共享物体的位置信息,并且所有的终端都接受并且执行这些信息。在你的FPS游戏里,你肯定不想有玩家能瞬间移动,水上飞什么的。所以一般来说服务器都是“权威性”服务器。设置一个权威性服务器也不需要什么特别难的代码,不过它的确需要你设计代码框架的时候,稍微做些调整。你需要在服务器端完成所有的工作并且检查所有的通讯。

        我们回头看一下上个教程B2,怎么才能把它修改成权威性服务器呢。首先,服务器需要生产玩家,玩家不能决定他们被生成的时间和地点。其次,服务器要告诉所有的客户端所有物体的位置,客户端之间无法发送和接受信息。因为只有服务器能够移动物体的位置,客户端的玩家想要移动的话,必须向服务器发送他的所需要的移动信息,然后接收服务器指令才能移动。
        我们要发送所有客户端的移动输入命令到服务器端,服务器会处理这些数据,然后送回结果数据(新的位置)到客户端。看一下Tutorial3场景。功能还是和以前一样,但是内部处理机制已经不一样了。移动起来可能比以前感觉更卡一点,但是现在这个暂时不重要。
        这个例子里没有新脚本,只有Playerscript脚本和spawnscript脚本的内容有改变。我们先看下


Tutorial_3_Spawnscript.js。

public var playerPrefab : Transform;
public var playerScripts : ArrayList = new ArrayList();
function OnServerInitialized()
{
      //Spawn a player for the server itself
      Spawnplayer(Network.player);
}
function OnPlayerConnected(newPlayer: NetworkPlayer)
{
       //A player connected to me(the server)!
      Spawnplayer(newPlayer);
} 
 
function Spawnplayer(newPlayer : NetworkPlayer)
{
      //Called on the server only 
      var playerNumber : int = parseInt(newPlayer+"");
      //Instantiate a new object for this player, remember; the server is therefore the owner.
      var myNewTrans : Transform = Network.Instantiate(playerPrefab, transform.position, transform.rotation, playerNumber);
 
      //Get the networkview of this new transform
      var newObjectsNetworkview : NetworkView = myNewTrans.networkView;
 
      //Keep track of this new player so we can properly destroy it when required.
      playerScripts.Add(myNewTrans.GetComponent(Tutorial_3_Playerscript));
 
      //Call an RPC on this new networkview, set the player who controls this player
      newObjectsNetworkview.RPC("SetPlayer", RPCMode.AllBuffered, newPlayer);//Set it on the owner
}
 
function OnPlayerDisconnected(player: NetworkPlayer)
{
        Debug.Log("Clean up after player " + player);
/*精确清除指定的脚本的RPC和物体*/
        for(var script : Tutorial_3_Playerscript in playerScripts)
        {
              if(player==script.owner)
              {
                    //We found the players object
                    //remove the bufferd SetPlayer call
                    Network.RemoveRPCs(script.gameObject.networkView.viewID);/*移除所有与这个viewID数相关的RPC函数调用*/
                  //Destroying the GO will destroy everything
                 Network.Destroy(script.gameObject);                    
                 playerScripts.Remove(script);//Remove this player from the list
                    break;
                }
         }
 
/*以下销毁语句无效,因为前面已经清除完实例化和相关缓存RPC了*/         
/*清除这个player的实例化对象的所有缓存RPC,加上清除物体后,在本例子中应该和上面的效果相同吧?*/
           //Remove the buffered RPC call for instantiate for this player.
           var playerNumber : int = parseInt(player+"");
          Network.RemoveRPCs(Network.player, playerNumber);
 
/*以下销毁语句无效,因为前面已经清除完实例化和相关缓存RPC了*/
           // The next destroys will not destroy anything since the players never
           // instantiated anything nor buffered RPCs
           Network.RemoveRPCs(player);
          Network.DestroyPlayerObjects(player);
}
function OnDisconnectedFromServer(info : NetworkDisconnection)
{
          Debug.Log("Resetting the scene the easy way.");
          Application.LoadLevel(Application.loadedLevel); 
}

        客户端在这个脚本里没有任何操作,每当客户端连接的时候服务器端才开始生成物体。服务器端还会保存一个已连接客户端的列表,列表中还包括了Playerscripts的信息,这样在一个客户端下线的时候,服务器就可以删除正确的玩家物体。这个Spawnscript脚本是一个纯粹的服务器端脚本,和客户端的“OnDisconnectedFromServer”函数没有任何关系。现在我们再看一下

 

Tutorial_3_Playerscript.js脚本:

public var owner : NetworkPlayer;
//Last input value, we're saving this to save network messages/bandwidth.
private var lastClientHInput : float=0;
private var lastClientVInput : float=0;
//The input values the server will execute on this object
/*虽然只有两个变量,但是在服务器中每个客户端有这两个变量的一份单独的副本,每份变量不影响其他客户端*/
private var serverCurrentHInput : float = 0;
private var serverCurrentVInput : float = 0;

function Awake()
{
      // We are probably not the owner of this object: disable this script.
      // RPC's and OnSerializeNetworkView will STILL get trough!
      // The server ALWAYS run this script though
/*禁用了该脚本后,RPC和OnSerializeNetworkView等网络函数仍然可以使用。*/
      if(Network.isClient)
      {
           enabled=false;  // disable this script (this enables Update()); 
       } 
}

@RPC
function SetPlayer(player : NetworkPlayer)
{
      owner = player;
      if(player==Network.player)
      {
           //Hey thats us! We can control this player: enable this script (this enables Update());
           enabled=true;
       }
}
function Update()
{ 
       //Client code
       if(owner!=null && Network.player==owner)
      {
            //Only the client that owns this object executes this code
            var HInput : float = Input.GetAxis("Horizontal");
          var VInput : float = Input.GetAxis("Vertical");
  
            //Is our input different? Do we need to update the server?
            if(lastClientHInput!=HInput || lastClientVInput!=VInput )
            {
                  lastClientHInput = HInput;
                  lastClientVInput = VInput;   
   
                  if(Network.isServer)
                  {
                        //Too bad a server can't send an rpc to itself 
                        //using "RPCMode.Server"!...bugged :[
/*遗憾的是服务器不能使用"RPCMode.Server"这种模式来给自己发送RPC信息,会报错*/
                        SendMovementInput(HInput, VInput);
                   }
                   else if(Network.isClient)
                   {
                       //SendMovementInput(HInput, VInput); //Use this (and line 64) for simple "prediction"
                     networkView.RPC("SendMovementInput", RPCMode.Server, HInput, VInput);
   }
   
  }
 }
 
 //Server movement code
 if(Network.isServer)//Also enable this on the client itself: "|| Network.player==owner)
{
       //Actually move the player using his/her input
       var moveDirection : Vector3 = new Vector3(serverCurrentHInput, 0, serverCurrentVInput);
       var speed : float = 5;
       transform.Translate(speed * moveDirection * Time.deltaTime);
 }
 
}
 

@RPC
function SendMovementInput(HInput : float, VInput : float)
{ 
        //Called on the server
       serverCurrentHInput = HInput;
       serverCurrentVInput = VInput;
}

function OnSerializeNetworkView(stream : BitStream, info : NetworkMessageInfo)
{
      if (stream.isWriting)
      {
             //This is executed on the owner of the networkview
             //The owner sends it's position over the network
  
             var pos : Vector3 = transform.position;  
           stream.Serialize(pos);//"Encode" it, and send it
    
         }
        else
        {
              //Executed on all non-owners
              //This client receive a position and set the object to it
  
               var posReceive : Vector3 = Vector3.zero;
             stream.Serialize(posReceive); //"Decode" it and receive it
  
              //We've just recieved the current servers position of this object in 'posReceive'.
  
               transform.position = posReceive;  
               //To reduce laggy movement a bit you could comment the line above and use position lerping below instead: 
               //transform.position = Vector3.Lerp(transform.position, posReceive, 0.9); //"lerp" to the posReceive by 90%
  
         }
}

        这个脚本现在不只被Networkview所有。因为现在由服务器端来生成所有的物体,所以全部的Networkview都为服务器所有。所以现在我们使用每个终端自己的“所有者”参数来控制,哪一个网络上的玩家会控制哪一个物体。playerscript脚本的所有者会发送移动信息到服务器。服务器执行这个移动信息并且负责移动玩家物体。这样一来,我们就有了一个“权威性”的服务器!

        关于卡的问题:在之前的例子里,玩家物体会在按下按键后立即移动,但是当我们使用权威性服务器,我们需要发送移动信息给服务器,然后服务器会处理它,然后再发回一个移动指令,然后我们才能移动物体。我们当然是想让服务器端有所有的控制权,但是我们不想让客户端等太长时间。其实这个问题也很简单,只要让客户端也同时计算移动信息,然后再让服务器端的信息覆盖客户端的计算结果,这样服务器端总是有控制权。很简单吧。Tutorial_3_Playerscript.js这个脚本在客户端调用了“SendMevementInput(HIput,Vinput)”函数。这里你可以发送一个移动信息RPC到服务器(第56行代码)。随后这个SendMovementInput RPC 会调用客户端移动脚本里的Update()函数的最后一部分代码,来更新物体的移动。同时在本地调用这一段代码:“|| Network.player == owner)” (第64行代码)。这样就可以确保客户端的移动立刻就能执行,而且让服务器端的计算结果为最终结果。
        虽然我们设置了让客户端可以“预测”物体的移动,但是还是有些卡,在代码的第100行这里有一段代码,它合并了当前的物体位置和服务器发送来的位置,并且以服务器的位置为主。你还可以把服务器发送来的位置保存为一个参数,然后用Vector3.Lerp这个命令来进行插值。这样你就可以在Update函数里进行平滑的插值,而不是只能在OnSerializeNetworkView函数里进行一次插值。
Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改
        注意一下,其实你不用总是在你的多人游戏里用“权威性”服务器。比如我们公司的Crashdriver 3D游戏,就用了非权威性服务器。玩家可以恶意修改他的赛车位置;但是谁在乎呢~这种修改最多也就能让玩家得到很高的分数。之后我们再检查那些高的离谱的得分要容易得多。总而言之:想明白你究竟为什么要用权威性服务器。另外也要知道,如果你直接修改权威性服务器,也能作弊哦。





七、更多的network科目解释:  


Unity编辑器中跟网络相关的选项
>>Edit - Project settings - Network
Sendrate(发送率):这个选项决定了每秒钟发送多少次网络信息(Unreliable 或者 Reliable delta compressed).注意,这个选项对RPC信息没有效果。在不影响游戏视觉效果的前提下,尽量把这个数值调低。
Debug level:改变多少debug信息会在编辑器中显示。

>>Edit - Project settings - Player
Run in background: Yes/No 当运行服务器端时,需要打开这个选项以便服务器可以在后台保持通讯。


限制通信数据量:限定视野内接收和限定同组接收
        你可以通过限制数据的数量来提高通讯性能。在多人游戏里,玩家不用接受所有的信息。一定距离以外发生的事情,对玩家也就没什么意义了。这里有两种方法,可以让玩家拒收信息:“组”或者”玩家“。

1、首先所有的NetworkView组件,要设置一个SetScop函数:
        function SetScope (player : NetworkPlayer, relevancy : bool) : bool
默认情况下,这个函数为true。你可以设置为false如果某个玩家已经离你足够远,然后你就不会再接收到他的信息。不过很可惜,这个函数只能用于NetworkViwe的observe属性,对RPC不起作用。

2、网络函数:
        static function SetReceivingEnabled (player : NetworkPlayer, group : int, enabled : bool) : void
        static function SetSendingEnabled (group : int, enabled : bool) : void
这两个函数可以根据网络组来限制信息的发送和接收。比如说,你可以把地图划分为32份,玩家只会发送/接收玩家周围的8个格子(还有玩家本身的一个,总共9个)。但是很遗憾在unity网络库中,你最多只能有32个组,虽然对FPS这样的游戏来说也够用了,但是对真正的MMO游戏,还是差很远。


网络连接的安全保密性
        添加AES加密,CRS,随即加密SYNCookies和RSA加密好像都挺复杂的哈。幸运的是我们可以只用一行代码就搞定这些加密问题:

function StartServer ()
{
      Network.InitializeSecurity();// 就是这一行!
      Network.InitializeServer(32, 25000);
}
        只是记得在初始化服务器之前调用Network.InitializeSecurity()函数一次,安全系统会让每个信息包增加15比特。 不过这样做数据量也相应增加了,应该有所权衡。


反作弊
        就算是有了之前的安全系统,当你设计游戏的时候,还是要考虑到最糟的情况。假设玩家对程序懂的和你一样多,并且他们可以随意修改你的网络信息包,搞出一些离谱的数值来。所以总是要在服务器端检查你接收到的数据。 只要设计网络功能的时候巧妙一些,就不用为了反作弊写一大堆额外的代码。


使用代理
        关于使用代理的事,手册上已经说的很清楚了,自己看下链接吧: http://unity3d.com/support/documentation/ScriptReference/Network-useProxy.html。虽然我们对使用代理来改善网络链接很有兴趣,但是我们还没有仔细研究这部分功能。


缓解战斗延迟:预测,外推法和插值
        我们已经在Tutorial 3里大致提到了这些问题:当你使用权威性服务器来做计算的时候,同时可以让客户端做预测计算来减少延迟。
        以下摘至Unity手册:“我们用来推测玩家行为的方法也可以用来推测敌人的行为。外推法就是根据服务器上一帧所收到的信息,计算一个敌人可能的方向和速度,然后假设敌人会继续朝这个方向移动。
        插值是怎么回事呢,当丢包的时候,通常玩家和敌人会突然卡住不动了,然后当下一个包发过来的时候,再跳到新的位置。但是我们可以设置一个延时(通常大约100毫秒)然后把之前的位置和新的位置做一个插值,这样的话,丢包的时候,玩家的移动依然是平滑的。”
        在unity的官方例子里可以找到关于插值和外推法的例子,这个教程中的FPS例子里,也有这相关内容。另外你也可以通过提高网络发送率来提高同步的精度。


手动分配networkView的ID
        有时候Network.Instantiate 对权威性服务器的支持不好。如果手动分配网络设置的ID可以获得更好的控制。
        代码例子: http://unity3d.com/support/documentation/ScriptReference/Network.AllocateViewID.html


网络加载

        对于网络工作来说,只要网络连接情况良好,无论服务器或者客户端上跑的是什么内容都无关紧要。也就是说你可以在服务器端跑一个游戏场景,而在一个刚连接的新客户端跑游戏大厅。通常都不会出什么问题,除非服务器向所有的客户端发送“缓存的实例化”游戏物体的命令。因为这个原因,你最好在载入游戏的时候暂时关闭网络通讯。你可以在客户端成功连接服务器之后,立即调用下面的代码:“Network.isMessageQueueRunning=false;”这样就可以关闭网络通讯。网络大厅的例子里有这个代码的应用。
        告诉你一个秘密,其实一个服务器可以同时跑多个场景/关卡,只要你能巧妙地设置好网络组。只是要小心不同场景中的玩家的碰撞信息。





八、真实例子  
Example 1: Chatscript(聊天系统)

public var usingChat : boolean = false; //Can be used to determine if we need to stop player movement since we're chatting
var skin : GUISkin;      //Skin
var showChat : boolean= false;   //Show/Hide the chat
//Private vars used by the script
private var inputField : String= "";
private var scrollPosition : Vector2;
private var width : int= 500;
private var height : int= 180;
private var playerName : String;
private var lastUnfocusTime : float =0;
private var window : Rect;
 
//Server-only playerlist
private var playerList = new ArrayList();
class PlayerNode
{
    var playerName : String;
    var networkPlayer : NetworkPlayer;
}
private var chatEntries = new ArrayList();
class ChatEntry
{
    var name : String= "";
    var text : String= ""; 
}
function Awake()
{
   window = Rect(Screen.width/2-width/2, Screen.height-height+5, width, height);
 
   //We get the name from the masterserver example, if you entered your name there ;).
    playerName = PlayerPrefs.GetString("playerName", "");
    if(!playerName || playerName=="")
    {
        playerName = "RandomName"+Random.Range(1,999);
     } 
 
}

//Client function
function OnConnectedToServer()
{
    ShowChatWindow();
    networkView.RPC ("TellServerOurName", RPCMode.Server, playerName);
    // //We could have also announced ourselves:
    // addGameChatMessage(playerName" joined the chat");
    // //But using "TellServer.." we build a list of active players which we can use for other stuff as well.
}
//Server function
function OnServerInitialized()
{
   ShowChatWindow();
   //I wish Unity supported sending an RPC on the server to the server itself :(
   // If so; we could use the same line as in "OnConnectedToServer();"
   var newEntry : PlayerNode = new PlayerNode();
   newEntry.playerName=playerName;
   newEntry.networkPlayer=Network.player;
   playerList.Add(newEntry); 
   addGameChatMessage(playerName+" joined the chat");
}
//A handy wrapper function to get the PlayerNode by networkplayer
function GetPlayerNode(networkPlayer : NetworkPlayer)
{
   for(var entry : PlayerNode in  playerList)
  {
     if(entry.networkPlayer==networkPlayer)
     {
         return entry;
     }
   }
 Debug.LogError("GetPlayerNode: Requested a playernode of non-existing player!");
 return null;
}

//Server function
function OnPlayerDisconnected(player: NetworkPlayer)
{
   addGameChatMessage("Player disconnected from: " + player.ipAddress+":" + player.port);
 
   //Remove player from the server list
    playerList.Remove( GetPlayerNode(player) );
}
function OnDisconnectedFromServer()
{
    CloseChatWindow();
}
//Server function
function OnPlayerConnected(player: NetworkPlayer)
{
   addGameChatMessage("Player connected from: " + player.ipAddress +":" + player.port);
}
@RPC
//Sent by newly connected clients, recieved by server
function TellServerOurName(name : String, info : NetworkMessageInfo)
{
   var newEntry : PlayerNode = new PlayerNode();
   newEntry.playerName=name;
   newEntry.networkPlayer=info.sender;
   playerList.Add(newEntry);
 
   addGameChatMessage(name+" joined the chat");
}
 

function CloseChatWindow ()
{
   showChat = false;
   inputField = "";
   chatEntries = new ArrayList();
}
function ShowChatWindow ()
{
   showChat = true;
   inputField = "";
   chatEntries = new ArrayList();
}
function OnGUI ()
{
    if(!showChat){
    return;
 }
 
 GUI.skin = skin;  
   
 if (Event.current.type == EventType.keyDown && Event.current.character == "\n" && inputField.Length <= 0)
 {
    if(lastUnfocusTime+0.25 0)
    {
        HitEnter(inputField);
     }
     GUI.SetNextControlName("Chat input field");
     inputField = GUILayout.TextField(inputField);
 
 
     if(Input.GetKeyDown("mouse 0"))
     {
        if(usingChat)
        {
            usingChat=false;
            GUI.UnfocusWindow ();//Deselect chat
            lastUnfocusTime=Time.time;
         }
      }
}
function HitEnter(msg : String)
{
     msg = msg.Replace("\n", "");
     networkView.RPC("ApplyGlobalChatText", RPCMode.All, playerName, msg);
     inputField = ""; //Clear line
     GUI.UnfocusWindow ();//Deselect chat
     lastUnfocusTime=Time.time;
     usingChat=false;
}

@RPC
function ApplyGlobalChatText (name : String, msg : String)
{
    var entry = new ChatEntry();
    entry.name = name;
    entry.text = msg;
    chatEntries.Add(entry);
 
     //Remove old entries
     if (chatEntries.Count > 4)
     {
          chatEntries.RemoveAt(0);
     }
 scrollPosition.y = 1000000; 
}
//Add game messages etc
function addGameChatMessage(str : String)
{
    ApplyGlobalChatText("", str);
    if(Network.connections.length>0)
     {
        networkView.RPC("ApplyGlobalChatText", RPCMode.Others, "", str); 
     } 
}

Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改
        Example1/Example1_Chat基本是就是教程1的代码加上一个聊天脚本。在游戏里添加一个聊天功能简单的要死。你可以重复使用这个脚本只要没有其他特殊要求。只是记得要对应好玩家的名字。现在可以显示4行聊天信息。你要想修改代码让它能显示更多聊天内容的话,可以用yield或者coroutine来删除或者淡出旧的信息。服务器中保存了一个玩家的列表。在真正的游戏中你应该做一个单独的游戏玩家列表,而不是把玩家列表放在聊天脚本里。


Example2: Masterserver example(主服务器管理游戏列表)
Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改


        打开场景“Example2/Example2_menu”.这个例子中使用到了masterserver来显示所有正在进行中的游戏。快速游戏的按钮可以让玩家随机加入第一个可以进入的游戏。下面的进阶选项中玩家可以创建一个服务器,填写IP和端口以便别的玩家可以直接连接到他,或者用masterserver的游戏列表直接手动选一个。唯一没有的功能是用密码开房间。这个功能可以简单的加在创建房间和连接的步骤之间,然后再游戏列表上你要加一个输入密码的窗口。
        这个例子中的”游戏“只是展示了怎么连接服务器和客户端,你可以轻易地替换游戏内容,网络功能也一样可以正常使用。你只需要设置“Network.isMessageQueueRunning = true;”。我们之前把这个函数关掉了,因为在客户端还在加载的时候,我们要防止从游戏局内发出一些无法识别的网络信息。还有一个事就是在服务器开始游戏之后,记得要在masterserver注册一下游戏。

        设置不同端口就可以开一个房间。          

        项目源码中不能显示列表和快速加入,因为缺少了在主服务器上注册和注销注册的功能,加上以下粗体代码即可正常实现。(亲测)
mianMenu.js

function playNowFunction(){
if(GUI.Button(Rect(490,185,75,20), "Cancel")){
Network.Disconnect();
currentMenu="";
playNowMode=false;
MasterServer.UnregisterHost();
}
 ...
}

multiplayerScript.js

function StartHost(players : int, port : int){
if(players<=1){
players=1;
}
//Network.InitializeSecurity();
Network.InitializeServer(players, port);
MasterServer.RegisterHost(gameName,port+"'s room");
}

gameScript.js

function OnGUI(){
...
if (GUILayout.Button ("Disconnect"))
{
Network.Disconnect(200);
if(Network.peerType == NetworkPeerType.Server){
MasterServer.UnregisterHost();
}
}
}

}
Unity3d网络教程(旧版networkView开发模式/附带项目源程序)_第1张图片


Example 3:Lobby system(游戏大厅)
Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改
        “Example3/Example3_lobby”:这个例子和第一个例子很像,唯一的不同是它给每个游戏创建了一个大厅,并且有密码选项。在大厅里,只会显示给玩家masterserver的游戏列表,游戏一旦开始就会被从列表中移除。还是一样,你要想用这些功能,直接拷贝代码然后针对你的游戏做点调整就行,只是记得在游戏场景里开启信息队列。


Example4:FPS game(本人5.3版本,缺了太多材质动画等,没法看出项目原意了)
Unity3d多人网络(从圣典拷贝来的,先看看,因为我觉得这位仁兄比我翻译得好,我也对这个教程作一些修改
        由于大多数想学unity网络功能的人都想做一个FPS游戏,我决定根据最后一个例子来做一个FPS的游戏。这个FPS例子是用的非权威性服务器,所以如果你愿意,可以重新设计代码,把它改成一个权威性服务器的游戏。
        这个例子用了masterserver的代码来连接主菜单。游戏中的功能有:聊天,得分板,移动,射击,拾取物体。

如果你想用这个例子作为基础来写你自己的游戏,你可能用到的新功能大概有:
1.权威性服务器控制移动:预防作弊
2.角色动画:远程同步动画,或者让客户端计算何时播放正确的动画
3.武器切换
4.和多人游戏不太相关的项目有:准星、改进GUI、观看模式、游戏回合时间




九、Tips!
同时打开多个unity(方便网络功能的查错)

        你无法打开相同的unity项目两次,所以你要用一个脚本来开第二个unity。你可以拷贝你的项目,一个跑服务器另一个跑客户端,但是你要保存你的改动2次。

Windows上:
修改快捷方式的属性。 在后面加上个 -projectPath,例如:   "D:\Program Files\Unity\Editor\Unity.exe" -projectPath
这样的话运行的时候窗口底部会报一个找不到路径的错误,无所谓,clear一次就行。

Mac上:
把Unity.app复制一份。分开运行。


在2.6版本(和更早)的OnSerializeNetworkView相关bug
        我一直都不想用OnSerializeNetworkView ,而喜欢RPC~,当我终于用OnSerializeNetworkView 的时候发现它有个缺陷,这个缺陷只发生在你要分配NetworkView ID给你自己的时候。
        具体如下:当你用OnSerializeNetworkView和NetworkView 监视功能的时候,服务器端的玩家没有问题,但是当客户端玩家连接的时候,它会报错说不知道第一个玩家(服务器端玩家)的networkview ID。错误信息是"Received state update for view ID ******random info here about your specific number*** but no initial state has ever been sent. Ignoring message." 这个问题是因为新的客户端从来没有初始化过他们自己的networkview,所以新的客户端连接的时候就会出问题。 也可以查看: http://forum.unity3d.com/viewtopic.php?p=77193


组限制
        你最多只能建32个组,即时你可以通过代码指定networkview中的组号为例如48。警告:指定组号48相当于指定48%32=16.


Scopes
        unity2.1有好多新功能,不过好像都不是支持RPC的,只支持OnSerializeNetworkWiew。


RPC bug?
        当你使用权威性服务器,而且服务器本身也在跑一个玩家的时候,会用到下面的代码:

networkView.RPC("SendUserInput", RPCMode.Server, horizontalInput, verticalInput);
        但是上面的代码其实不能用,用下面的:
if(Network.isServer)
{
SendUserInput(horizontalInput, verticalInput);
}
else
{
networkView.RPC("SendUserInput", RPCMode.Server, horizontalInput,verticalInput);
}


Run dedicated servers(专用服务器)
        现在untiy的专业服务器还不太完善,不过也凑合能用。在Mac上面跑专用服务器的话,要执行的时候添加一个批处理参数。Win从unity2.6开始也加上了相同的功能。参见以下链接 http://unity3d.com/support/documentation/Manual/Command Line Arguments.html
        当你用专用服务器的时候,应该用“Application.targetFrameRate”来控制帧率,否则unity可能会把帧率搞的太高,拖慢性能。


连接问题:如果连通互联网
        连接方面来说,本地局域网和互联网差不多,只是 局域网速度肯定快一点。当你让你的游戏在局域网上跑起来之后,你会发现在互联网上跑设置起来还是有点麻烦。下面这个表可以帮助你检查哪儿出的问题:

互联网连接不工作:
1.确定是连接的互联网还是局域网
2.两台电脑都连接了互联网没?
3.确定两台电脑的防火墙没有关闭你用到的端口,或者暂时关闭防火墙
4.尝试一下直接连接,开一个服务器,然后用另一台电脑做客户端直接连接服务器端的互联网网络地址

        如果还是连不上,你的路由器可能屏蔽了连接功能,作为安全措施。你有两个选择
1.用NAT穿透(见masterserver例子)然后祈祷你的路由器支持穿透功能。
2.你可以手动在路由器中打开你用到的端口,然后从这个端口转发所有的连接到你的内部局域网IP地址。这个绝对能用,但是你不能保证所有的玩家都会设置路由器端口。


其他的networking信息
        这里是一些unity网络的资料,有一些第三方的网络支持,可以自行查看(2009.8月)
Create your own custom RakNet backend
• Smartfox
• Photon & Neutron from ExitGames
• Project DarkStar
• Netdog
• Lidgren





你可能感兴趣的:(学习笔记之unity3d)