【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 3956|回复: 6

[多人在线技术] 利用Network View网络视图组件创建非授权服务器

[复制链接]
发表于 2014-2-16 23:01:37 | 显示全部楼层 |阅读模式
本帖最后由 夜行的猫仔 于 2014-2-18 13:50 编辑

本文的代码出自《M2H_Networking_Tutoria》--源代码是JSP的,本例题全部采用改写后的C#代码。
用到的控件是  Network View网络视图组件   点即可查看该控件的详细用法。


利用Network View网络视图组件创建非授权服务器

首先解释一下非授权服务器,这种服务器就像是CS(反恐精英)一样的模式,玩家在自己的机器上操作角色进行逻辑判断,最终把结果发给服务器,服务器负责把结果传给其他的联网者。这类服务器的简单易用,是学习服务器的好开端。Unity已经将复杂的网路操作简化成了标准的流程模式,因此我们只需要进行初始化服务器和连接服务器就行了。
服务器端只需要如下代码就可以初始化服务器:
[mw_shl_code=csharp,true]Network.InitializeServer(32, 1000);[/mw_shl_code]
下面先介绍最简单的服务器创建方式:
1.创建最简单的服务器:
创建一个NetServer脚本:
[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class NetServer : MonoBehaviour {

        // 初始化服务器
        void Start () {
                Network.InitializeServer(32, 6000);
        }


//服务器创建成功激活此函数
        void OnServerInitialized() {
                Debug.Log("服务器正常启动");
        }

        //当玩家连入服务器的时候激活该方法
        void OnPlayerConnected(NetworkPlayer player) {
                Debug.Log("有人连接到服务器");
        }

}
[/mw_shl_code]
客户端NetClient脚本如下:[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class NetClient : MonoBehaviour {

        // 客户端连接到服务器
        void Start () {
        Network.Connect("127.0.0.1", 6000);
        }
        }
[/mw_shl_code]
将以上两个脚本分别在两个项目中绑在Gameobject上,并先运行服务器项目,再运行客户端项目。
由代码可以看出,服务器端启动便创建了一个服务器,客户端在启动的时候便连入到了服务器。在服务器端可以看到如下图的报告:
QQ图片20140216223816.jpg
这样就创建了一个最简单的服务器。
2.对服务器进行完善
显然刚刚创建的服务器过于简单,在实际的使用中毫无用处。只有当我们需要服务器的时候才去创建服务器,那么就增加UI界面来控制服务器:
[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class NetServer : MonoBehaviour {
        string connectToIP  = "127.0.0.1";
        int connectPort= 6000;

        void OnGUI()
        {
                switch(Network.peerType)
                {
                case NetworkPeerType.Disconnected:
                        //服务器没有开启
                        StartServer();
                        break;
                case NetworkPeerType.Server:
                        OnServer();
                        break;
                }

        }

        void StartServer()
        {
                if(GUILayout.Button("创建服务器"))
                {
                        Network.InitializeServer(32, connectPort);
                }
        }

        void OnServer()
        {
                GUILayout.Label("服务器创建成功,等待接入.............");
        }

        //Server functions called by Unity
        void OnPlayerConnected(NetworkPlayer player) {
                Debug.Log("有人连接到服务器");
        }

}
[/mw_shl_code]
其中NetworkPeerType是当前网络接口的状态。客户端代码,运行后点“链接服务器”才会连接服务器端。
[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class NetClient : MonoBehaviour {

        // Use this for initialization
        void OnGUI()
    {
        if (GUILayout.Button("链接服务器"))
        Network.Connect("127.0.0.1", 6000);
        }
}
[/mw_shl_code]
通过以上的测试,已经可以方便的连入到服务器上。接下来的工作是创建非授权服务器。

3.添加Network View网络视图组件
Network View网络视图组件 是Unity提供的网络共享数据组件。
在服务器端和客户端分别创建一个gameObject。并为这两个gameObject添加Network View组件。(我创建了的是Capsule,为了区分客户端和服务器我分别给与不同的颜色,服务器是红的,客户端是绿的)。然后再增加一个地面和一盏灯光组建一个简单的场景。
添加Network View组件的方法是:Component-->Miscellaneous-->Network View。默认的2个View ID这里应该都是一样的。
写一个Move脚本:[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class move : MonoBehaviour {
void Update () {
                Vector3 moveDirection  = new Vector3(-1*Input.GetAxis("Vertical"), 0,Input.GetAxis("Horizontal"));
                float speed = 5;
                transform.Translate(speed * moveDirection * Time.deltaTime);
        }
}
[/mw_shl_code]将这个脚本放在服务器端,拖给服务器端的红色物体。然后运行服务器,客户端运行登陆到服务器上。用键盘的WSAD操作红色的物体可以看到绿色的物体跟随者动了起来。

在Unity中只要需要接受或者发送网络信息的,都需要一个NetworkView组件。你可以在整个游戏中只使用一个NetworkView组件,然后用脚本引用它。但是这样太麻烦了,最简单就是给每个需要网络功能的物体都加一个组件。目前这样做只能通过移动服务器端的物体,客户端能看到服务器端的操作。而且这个时候客户端移动的时候显得非常的卡,感觉很糟糕,这些问题都将在后面逐个解决。现在,怎么让客户端知道服务器端的物体移动了呢?看一下附加在物体上的NetworkView组件。它检测了物体的transform(变换)属性。也就是说unity会自动发送物体的变换属性(包括了位置,旋转角度和缩放的Vector3数值)。它只会把信息从服务器端发送到客户端,反之就不行,因为服务器端独占了NetworkView的功能。客户端就不能发送信息,只能够接收。
我们看一下 Network View网络视图组件  的其他选项,稍微总结一下。GameObject物体的Networkview组件中,State synchronization选项,被设定为Reliable compressed。这说明只有被观察的参数发生改变的时候,它才会发送信息。如果服务器端从来没有动过,它就绝对不会发送任何信息。如果设置成Unreliable,无论参数有没有变,它都一致在发送信息。最后一个,如果设置State synchronization 为Off,会完全停止NetworkView所有的网络同步行为。如果你的NetworkView组件没有在对物体进行监视,你可以把同步选项关闭(不过也不是必须的)。如果你不明白我们为什么需要这么一个关掉了同步选项的NetworkView组件,就这么给你说吧,因为“Remote Procedure Calls”(RPC远程程序调用)需要一个NetworkView组件,但是并不需要State synchronization和observed选项。不过你还是可以把RPC和observed一起用。RPC的内容会在下面的教程里讲到。基本上它就是一个你自己定义的网络信息收发机制。
现在直奔主题吧!Remote Procedure Calls!

 楼主| 发表于 2014-2-16 23:42:49 | 显示全部楼层
本帖最后由 夜行的猫仔 于 2014-2-18 14:29 编辑

4.使用RPC传送网络信息
RPC估计是目前使用最广的网络信息发送方式,至少我看到的例题大多都是RPC方式。
首次接触RPC,我们还是实现以下上面的功能吧。
修改服务器代码:将GameObject的NetworkView参数synchronization设为off,observed设为null。
[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class move : MonoBehaviour {
        private Vector3 lastPosition;

        // Update is called once per frame
        void Update () {
                if(Network.isServer){
                Vector3 moveDirection  = new Vector3(-1*Input.GetAxis("Vertical"), 0,Input.GetAxis("Horizontal"));
                float speed = 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]
        void SetPosition(Vector3 newPos)
        {
                // This RPC is in this case always called by the server,
                // but executed on all clients
               
                transform.position=newPos;        
        }
}
[/mw_shl_code]在这个代码中,增加了一个新的函数SetPosition,这个例题和之前两个实现了一样的功能。不过Networkview不再监视任何物体,同步选项也已经被关闭。秘密就在这个脚本里,特别是这一行networkView.RPC("SetPosition", RPCMode.Others, transform.position);   RPC是调用远程的函数,因此客户端的代码里也必须有这个函数。

服务器调用了RPC,这个RPC会求客户端调用“SetPosition”函数,同时这个RPC还包含这一个新的位置信息“transform.position”,然后所有的客户端都调用”SetPosition”这个函数。下面是整个移动的过程:

1.服务器端玩家按下按键,他控制的物体移动。
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会在这个刚连接的新玩家的客户端中,生成其他所有在他之前加入服务器的玩家。

5.创建服务器&客户端互动
以前这几个例子都是服务器端控制,客户端只能看到服务器端运行的结果而无法控制,这个例题客户端将会与服务器互动。首先,前面几个例子都是创建好物体,通过NetworkView将服务器的数据同步到客户端。实际的游戏中,客户端不登陆服务器,是互相看不到游戏中的角色的,也就是说游戏中的角色是实时创建出来的。
这个例子中,分别在客户端和服务器端都将原来的模型删掉。因为这次我们的资源要等到服务器创建的时候再加载,客户端创建客户端的,服务器创建服务器的,而且双方还要互相能看到对方动。 QQ图片20140218084124.jpg
在服务器和客户端都创建一个胶囊和一个球,分别染上红色和绿色。绑定好Move脚本和NetworkView ,以及刚体。然后将这两个物体都生成预制体备用。
修改Move脚本,这样修改的目的是每个端智能控制自己(客户端只能控制球,而服务器端智能控制胶囊)。
[mw_shl_code=csharp,true]using UnityEngine;using System.Collections;

public class move : MonoBehaviour {
        private Vector3 lastPosition;
        void Update () {
                if(networkView.isMine){
                Vector3 moveDirection  = new Vector3(-1*Input.GetAxis("Vertical"), 0,Input.GetAxis("Horizontal"));
                float speed = 5;
                transform.Translate(speed * moveDirection * Time.deltaTime);

                //移动的距离超出一定的值以后才会发送移动消息
                if(Vector3.Distance(transform.position, lastPosition)>=0.05){
                        lastPosition=transform.position;
                        networkView.RPC("SetPosition", RPCMode.Others, transform.position);
                }
                }
        }

        [RPC]
        void SetPosition(Vector3 newPos)
        {               
                transform.position=newPos;        
        }
}
[/mw_shl_code]
将预制体绿色的球拖到变量ClientPlayer 上。链接服务器的NetClient 脚本增加一句话, 当登录服务器成功以后,增加客户端角色。
[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class NetClient : MonoBehaviour {
        public Transform ClientPlayer;          //客户端的预制体(绿色的球体)
        // Use this for initialization
        void OnGUI()
    {
        if (GUILayout.Button("链接服务器"))
        Network.Connect("192.168.0.2", 6000);
        }
        
        //当登录服务器成功以后,增加客户端角色。
        void OnConnectedToServer() {
                Network.Instantiate(ClientPlayer,transform.position,transform.rotation,0);
        }
}
[/mw_shl_code]
服务器端也有相似的代码,等服务器创建成功以后,将红色的胶囊预制体拖到playerPrefab上,Move脚本和客户端的一样。[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class NetServer : MonoBehaviour
{
        public Transform playerPrefab;

        string connectToIP  = "127.0.0.1";
        int connectPort= 6000;

        void OnGUI()
        {
                switch(Network.peerType)
                {
                case NetworkPeerType.Disconnected:
                        //服务器没有开启
                        StartServer();
                        break;
                case NetworkPeerType.Server:
                        OnServer();
                        break;
                }

        }

        void StartServer()
        {
                if(GUILayout.Button("创建服务器"))
                {
                        Network.InitializeServer(32, connectPort);
                }
        }

        void OnServer()
        {
                GUILayout.Label("服务器创建成功,等待接入.............");
        }

        //Server functions called by Unity
        void OnPlayerConnected(NetworkPlayer player) {
                Debug.Log("有人连接到服务器");
        }

        void OnServerInitialized()
        {
                //当服务器建立的时候,创建角色预制体
                Network.Instantiate(playerPrefab, transform.position, transform.rotation, 0);
        }
}
[/mw_shl_code]
这样运行以后,服务器端会生成红色的胶囊,当客户端登陆以后,场景里就会多一个绿色的球,客户端移动小球服务器端也可以看到绿色小球的移动。
到此为止客户端和服务器端相互通信就解决了。但是这样的工作只是完成了第一步,接下来游戏开发资源会越来越多,客户端和服务器端分开开发就显得很麻烦,接下来将会整合这两套代码并完善其中的资源管理等。

下来的内容在4楼继续....................
未标题-1.png
发表于 2014-2-17 08:51:34 | 显示全部楼层
老大,我来顶你,每次我都是沙发!

点评

:) 谢谢,太给面子啦!  发表于 2014-2-17 08:52
 楼主| 发表于 2014-2-18 14:12:08 | 显示全部楼层
本帖最后由 夜行的猫仔 于 2014-2-18 15:05 编辑

沙发抢的太快了||
这个继续讲。。。。。。
6.对项目代码进行整理
经过前面5个步骤实现了客户端和服务器的交互。不过资源越来越多越复杂分开两个项目就不合适了,因此对代码进行整合。
对脚本也进行分工,目前脚本主要实现了服务器创建,登陆、角色的创建、角色的移动控制。
基于这几点把脚本分为三大块:与是不是服务器有关,与角色创建有关和与角色控制有关。对应三个脚本:NetServer,CreatePlayer和PlayerControll。
新建一个场景,在场景中创建一个地面和一个方向光。
创建一个空物体,Reset。改名为NetWork_Object,挂上脚本NetServer。[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class NetServer : MonoBehaviour
{
        string connectToIP  = "127.0.0.1";
        int connectPort= 6000;

        void OnGUI()
        {
                switch(Network.peerType)
                {
                case NetworkPeerType.Disconnected:
                        //服务器没有开启
                        StartServer();
                        break;
                case NetworkPeerType.Server:
                        OnServer();
                        break;
                case NetworkPeerType.Connecting:
                        break;
                case NetworkPeerType.Client:
                        break;
                }

        }

        void StartServer()
        {
                connectToIP = GUILayout.TextField(connectToIP, GUILayout.MinWidth(100));
                connectPort = int.Parse(GUILayout.TextField(connectPort.ToString()));
                if(GUILayout.Button("创建服务器"))
                {
                        Network.InitializeServer(32, connectPort);
                }
                if(GUILayout.Button("客户端登陆"))
                {
                        Network.Connect(connectToIP,connectPort);
                }
        }

        void OnServer()
        {
                GUILayout.Label("服务器创建成功,等待接入.............");
        }

        //Server functions called by Unity
        void OnPlayerConnected(NetworkPlayer player)
        {
                Debug.Log("有人连接到服务器");
        }
}
[/mw_shl_code]将CreatePlayer也挂在这个空物体上,对CreatePlayer中的变量playerPrefab,拖入一个创建好的预制体。
[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class CreatePlayer : MonoBehaviour
{
        public Transform playerPrefab;
        void OnServerInitialized()
        {
                //当服务器建立的时候,创建角色预制体
                Create(playerPrefab);
        }
        
        void OnPlayerDisconnected(NetworkPlayer player)
        {
                Debug.Log("Clean up after player " + player);
                Network.RemoveRPCs(player);
                Network.DestroyPlayerObjects(player);
        }
        
        void OnDisconnectedFromServer(NetworkDisconnection info)
        {
                Debug.Log("Clean up a bit after server quit");
                Network.RemoveRPCs(Network.player);
                Network.DestroyPlayerObjects(Network.player);
               
                /*
        * Note that we only remove our own objects, but we cannot remove the other players
        * objects since we don't know what they are; we didn't keep track of them.
        * In a game you would usually reload the level or load the main menu level anyway ;).
        *
        * In fact, we could use "Application.LoadLevel(Application.loadedLevel);" here instead to reset the scene.
        */
                Application.LoadLevel(Application.loadedLevel);
        }
        void OnConnectedToServer()
        {
                //当客户端连入的时候,创建角色预制体
                Create(playerPrefab);

        }
        void Create(Transform Prefab)
        {
                Network.Instantiate(Prefab, transform.position, transform.rotation, 0);
        }
}
[/mw_shl_code]
最后是删除move脚本,改为PlayerControll脚本。内容不变。
[mw_shl_code=csharp,true]using UnityEngine;
using System.Collections;

public class PlayerControll : MonoBehaviour
{
        private Vector3 lastPosition;

        void Update ()
        {
                if(networkView.isMine)
                {
                        Vector3 moveDirection  = new Vector3(-1*Input.GetAxis("Vertical"), 0,Input.GetAxis("Horizontal"));
                        float speed = 5;
                        transform.Translate(speed * moveDirection * Time.deltaTime);

                        if(Vector3.Distance(transform.position, lastPosition)>=0.05)
                        {
                                lastPosition=transform.position;

                                networkView.RPC("SetPosition", RPCMode.Others, transform.position);
                        }
                }
        }
        
        [RPC]
        void SetPosition(Vector3 newPos)
        {
                // This RPC is in this case always called by the server,
                // but executed on all clients
               
                transform.position=newPos;        
        }
}
[/mw_shl_code]

 楼主| 发表于 2014-2-18 14:30:11 | 显示全部楼层
我还是事先占个坑。。。。。有没有用先留着.............
发表于 2014-3-6 20:58:46 | 显示全部楼层
mark,留着以后用
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2024-4-26 19:29

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表