Unity Unet网络框架说明手册

一. Unet基本介绍。
Unet全称为 Unity Networking,为开发者提供开发多玩家在线的开发工具和模块,使多人在线游戏开发的网络部分变得更加简单。
其特点是:
1. 基于 UDP 的高性能传输层支持所有的游戏类型
2. 底层 API (LLAPI) 通过类似套接字的接口提供全面控制
3. 高层 API (HLAPI) 提供简单、安全的客户端/服务器网络模型
4. 匹配服务提供基本功能,让一个用户创建多人模式并允许他人加入
5. 中继服务器解决连接问题,使玩家在使用防火墙的情况能相互连接

Unet的基本结构并不像传统的C/S架构,而是每一个客户端都可以充当充当服务器,即客户端既可以是单纯的客户端,也可以同时兼顾客户端与服务器的双重身份。客户端将重要的操作放在服务器上去执行,并把执行结果广播同步至每个客户端。而服务器也可以远程调用客户端提供的过程。

二. Unet主要类
1.NetworkIdentity:网络物体最基本的组件,客户端与服务器确认是否是一个物体(netID),也用来表示各个状态,如是否是服务器,是否是客户端,是否有权限,是否是本地玩家等。
例子:A是Host(又是服务器,又是客户端),B是一个Client,A与B分别有一个玩家PlayA与PlayB.在机器A上,playA与playB isServer为true,isClent为true,其中playA有权限,是本地玩家,B没权限,也不是本地玩家。在机器B上,playA与playB isServer为false,isClent为true,其中playB有权限,是本地玩家,A没权限,也不是本地玩家。A与B上的PlayA的netID相同,A与B上的PlayB的netID也相同,其中netID用来表示他们是同一网络物体在不同的机器上。

2.NetworkConnection: 定义一个客户端与服务器的连接,包含当前客户端监视那些服务器上的网络物体,以及封装发送和接收到服务器的消息。
例如:网络物体上的监视者就是一个或多个NetworkConnection,用来表示一个或多个客户端对这个网络物体保持监视,那么当这个网络物体在服务器上更新后,会自动更新对所有监视者的对应的网络物体。

3.NetworkClient:主要持有当前NetworkConnection对象与所有NetworkClient列表的静态对象,处理一些默认客户端的消息。

4.NetworkScene: 1Server与Client需要维护一个网络物体列表,Server可以遍历所有网络物体发送消息等,并且维持Server与Client上的网络物体保持同步,并且客户端记录需要注册的prefab列表.其中NetworkServer与ClientScene都包含一个NetworkScene对象,引用网络物体列表。

5.NetworkServer:主要持有一个NetworkScene并且做一些只有在服务器上才能对网络服务做的事,如spawn, destory等。以及维护所有客户端连接。

6.ClientScene:主要持有一个静态NetworkScene对象,用于注册网络物体的prefab列表,以及客户端场景上已经有的网络物体列表,处理SyncVar,Rpc,SyncEvent特性等,还有以及ObjectSpawn,objectDestroy,objectHide消息等。

二. Unet主要属性
1.Spawn:把服务器上的GameObject,根据NetworkIdentity组件找到对应监视连接,在监视连接里生成相应的GameObject.

2.Command:客户端调用,服务器执行,这样客户端调用的参数必需要UNet可以序列化,这样服务器在执行时才能把参数反序列化。需要注意,在客户端需要有权限的NetworkIdentity组件才能调用Command命令。

3.ClientRpc:服务端调用,客户端执行,同上,服务端的参数序列化到客户端执行,一般来说,服务端会找到上面的NetworkIdentity组件,确定那些客户端在监视这个NetworkIdentity,Rpc命令会发送给所有的监视客户端。

4.Server/ServerCallback:只在服务器端运行,Callback是Unity内部函数。

5.Client/ClientCallback:同上,只在客户端运行,Callback是Unity内部函数。

6.SyncVar:服务器的值能自动同步到客户端,保持客户端的值与服务器一样。客户端值改变并不会影响服务器的值。
注意:这些属性分为两种调用方式(服务器调用,客户端执行 与 客户端调用,服务器执行),其中Spawn,ClientRpc,SyncVar都是服务器调用,客户端得到相应消息,执行相应方法。而Command等为客户端调用,服务器检测到相应消息后执行

三. Unet主要组件
Unet的组件分为两大类,一类为挂载在场景中不会消失的一个物体上,进行管理操作的。另外一类为挂载在Prefab上的组件,表面Prefab上的各种属性等

(一) 管理组件
1.NetworkManager:实现联网的基本功能,添加要同步的Prefab,

其中,Network Inof是对网络连接的各种属性进行设置。
而Spawn Info主要是在场景中要生成的物体进行设置:
·PlayerPrefab为客户端加入时自动生成的Prefab,可挂一个空的GameObject到场景中再去添加。

·PlayerSpawnMethod为Prefab的位置生成方法,两个选项(Random随机生成/ RoundRobin轮询,在指定的地点轮流生成)。使用这个方法首先要有指定的位置(NetworkStartPosition) 。

·RegisteredSpawnablePrefabs=>在这个数组中放置的Prefab是需要在网络中生成的,加入到这个数组中,才可以在网络中同步生成。

2.NetworkManagerHUD:一个测试用的链接UI界面,可以在测试时使用,但是在真正的游戏中不会保留。

(二) Prefab组件
1.NetworkIdentity:表明这个预设体是一个网络对象,可以设置这个预设体是在客户端生成还是只在服务器端生成,并且想要在网络上进行同步的物体都必须具有这个组件。

·ServerOnly:只在服务器端生成,不会在客户端批量生成
·LocalPlayerAuthority:如果这个对象是由拥有它的客户端控制的,本地玩家对象在这个客户端拥有它的权限,这个属性常被其他组件使用例如Networktransfrom。

2.NetworkTransform:同步对象在所有客户端上的Transform属性,可以选择同步Rigidbody属性。

·NetworkSendRate(seconds):同步的频率,0代表在生成是同步一次
·TransformSyncMode:同步的组件 ,有4个选项
SyncNone:不同步其他组件
SyncTransform:同步Transform组件,就算不选,也会同步
SyncRigidbody2D:同步Rigidbody2D组件,
SyncRigidbody3D:同步Rigidbody3D组件

3.NetworkTransformChild:想对同步对象中的子对象进行一个Transform属性的同步,可以将该组件挂载在子对象上。

4.NetworkStartPosition:将这个对象的位置作为Player生成的位置

四. Unet底层API
TransPort Layer API:能够发送和接受消息闭关表示为数组的字节,还提供了大量不同的“服务端质量”,以适应不同的场景,支持基础的网络通信服务。简单说:为一种更加具有自由的网络通信服务。
其特点为:
• 优化基于 UDP 协议。
• Multi-channel design to avoid head-of-line blocking issues 多通道设计,以避免头的线阻塞问题
• 每个通道支持各种levels 的服务质量 (QoS) 。
• 灵活的网络拓扑结构,支持对等 或 客户机-服务器体系结构。
其有两种协议
1. 通用通信UDP(典型工作流程)
(1) 初始化网络传输层

// 不带参数初始化 
NetworkTransport.Init(); 
// 设置最大数据包为500 
GlobalConfig globalconfig = new GlobalConfig();
globalconfig.MaxPacketSize = 500;
NetworkTransport.Init(globalconfig);

(2) 配置拓扑网络

ConnectionConfig config = new ConnectionConfig();
// QosType.Reliable 可靠连接,确保信息传输安全 
byte reliableByteId = config.AddChannel(QosType.Reliable);
// QosType.Unreliable 不可靠连接,传输速度快,不能保证信息传递 
byte unreliableByteId = config.AddChannel(QosType.Unreliable);
// 网络拓扑的定义, (连接使用配置信息,允许连接数) 
HostTopology topology = new HostTopology(config, 10);

(3) 创建服务器主机

// 创建主机服务(端口号:8888)
int intHostID = NetworkTransport.AddHost(topology, 8888);

(4) 开始通信

private byte error;
private byte[] buffer;
private int bufferLength = 1024;
// 先将不同的命令发送主机并检查状态 
// 返回一个给此链接的ID 
int connetionID = NetworkTransport.Connect(intHostID, "192.168.1.131", 8888, 0, out error);
// 此命令发送断开链接的请求
NetworkTransport.Disconnect(intHostID, connetionID, out error); 
// 发送信息,将消息存储在缓存区消息长度为bufferLength 
NetworkTransport.Send(intHostID, connetionID, reliableByteId, buffer, bufferLength, out error); 
 // 检测主机状态: 
    void Update(){
        myConnectinID = NetworkTransport.Connect(hostId, "192.168.1.133", 8888, 0, out error);
        // 此函数返回来自任意主机的事件(recHostId返回主机ID) 
        NetworkEventType recData = NetworkTransport.Receive(out recHostId, out connectionID, out reliableByteId, recbuffer, bufferSize, out dataSize, out error);
        switch (recData)
        {
            case NetworkEventType.ConnectEvent:
                if (myConnectinID == connetionID)
                {
                    // 主动连接请求成功
                }
                else
                {
                    // 其他人连接到我
                }
                break;
        }
        // 此函数为检测主机ID
        NetworkTransport.ReceiveRelayEventFromHost
    } 
  1. 用于WebGL的WebSockets
    Web客户端只能连接到服务器端,服务器端是一个独立的Player
NetworkTransport.AddWebSocketHost(topology,port,Ip);

ip为监听地址,为null,将监听所有的网路接口,服务器只支持一个WebSocket主机并在同一时间内处理常用的服务器

五. Unet 方法
(一)编写脚本时的相关说明:在编写脚本时,所有涉及网络的脚本都要引用UnityEngine.Networking,并使对象继承自NetworkBehaviour。且NetworBehaviour包含了MonoBehaviour。
例如:

using UnityEngine.Networking;
public class PlayerControler : NetworkBehaviour { }

(二)脚本中所用到的一些基本属性与方法
·bool 类型 isLocalPlayer,如果这个对象是标识本地机器上的播放的那个对象,则返回值为真

·bool 类型 isServer,如果这个对象是服务器,则为真

·bool 类型 isClient,如果这个对象运行在客户端的话,则为真

·方法OnStartLocalPlayer(){}在服务器联入时并且游戏开始时调用,相当于普通的Start方法。

·NetworkServe.Spawn(Prefab);将Prefab同步到所有的客户端上。在所有已准备好的客户端上同步给定的游戏对象的生成(这个方法并不会同步物体的移动)。

·注意
与player有关的都用localPlayer处理,与player无关的,用服务器处理

(三)标志方法
1.[Command]
[Command]的功能是标志以下的方法只能在服务器端调用,客户端调用该方法时,会自动调用服务器端的方法。 这个方法必须以Cmd开头,且不能是静态方法。例如:

[Command]
private void CmdFire(Vector3 rigidbodyVelocity, float launchForce, Vector3 forward, Vector3 position, Quaternion rotation)
{
        // Create an instance of the shell and store a reference to it's rigidbody.
        Rigidbody shellInstance =
             Instantiate(m_Shell, position, rotation) as Rigidbody;

        // Create a velocity that is the tank's velocity and the launch force in the fire position's forward direction.
        Vector3 velocity = rigidbodyVelocity + launchForce * forward;

        // Set the shell's velocity to this velocity.
        shellInstance.velocity = velocity;

        NetworkServer.Spawn(shellInstance.gameObject);
}

2.[ClientRpc]
[ClientRpc]的功能是标志的以下方法只能在客户端调用,服务器调用该方法时,也会在客户端调用。
要求,这个方法Rpc开头,例如:

[ClientRpc]
void RpcRoundStarting()
{
        // As soon as the round starts reset the tanks and make sure they can't move.
        ResetAllTanks();
        DisableTankControl();

        // Snap the camera's zoom and position to something appropriate for the reset tanks.
        m_CameraControl.SetAppropriatePositionAndSize();

        // Increment the round number and display text showing the players what round it is.
        m_RoundNumber++;
        m_MessageText.text = "ROUND " + m_RoundNumber;
        StartCoroutine(ClientRoundStartingFade());
}

3.[SyncVar]
[SyncVar]标记是作用在属性前的,作用是同步该属性到所有的客户端和服务器端。
这个标记中可以增加方法[SyncVar(hook=”FunctionName”)],引号中是方法名。当标记的数据值发生变化时,会自动调用引号中的方法,同时将变化后的属性那个值调用到这个方法中。例如:

[SyncVar(hook = "OnChangeHealth")]//同步该数据,并在数据发生变化时,调用该方法,同时,这个数据也会被调用到该方法中。
private int currentHealth = MaxHealth;
void OnChangeHealth(int health)
{
    healthSlider.value = health / (float)MaxHealth;
}

4.[SyncLists]
[SyncLists]同步列表类似于[SyncVar]同步变量,但是他们是一些值的列表而不是单个值。同步列表和同步变量都包含在初始的状态更新里。同步列表不需要[SyncVar]属性标识,他们是特殊的类。内建的基础类型属性列表有:
SyncListString
SyncListFloat
SyncListInt
SyncListUInt
SyncListBool
  还有个SyncListStruct可以给用户自定义的结构体用。从SyncListStruct派生出的结构体类可以包含基础类型,数组和通用Unity类型的成员变量,但是不能包含复杂的类和通用容器。  
  同步列表有一个叫做SyncListChanged的回调函数,可以使客户端能接收到列表中的数据改动的通知。这个回调函数被调用时,会被通知到操作类型,和修改的变量索引。
例如:

public class MyScript : NetworkBehaviour
{
    public struct Buf
    {
        public int id;
        public string name;
        public float timer;
    };
    public class TestBufs : SyncListStruct { }
    TestBufs m_bufs = new TestBufs();

    void BufChanged(Operation op, int itemIndex)
    {
        Debug.Log("buf changed:" + op);
    }
    void Start()
    {
        m_bufs.Callback = BufChanged;
    }
}

5.定制序列化函数
通常在脚本中使用同步变量,但是有时候也需要更复杂的序列化代码。NetworkBehaviour中的虚函数允许开发者定制自己的序列化函数,这些函数有:

public virtual boolOnSerialize(NetworkWriter writer, bool initialState);
public virtual voidOnDeSerialize(NetworkReader reader, bool initialState);

initalState可以用来标识是第一次序列化数据还是只发送增量的数据。如果是第一次发送给客户端,必须要包含所有状态的数据,后续的更新只需要包含增量的修改,以节省带宽。同步变量的钩子函数在initialState为True的时候不会被调用,而只会在增量更新函数中被调用。

如果一个类里面声明了同步变量,这些函数的实现会自动被加到类里面,因此一个有同步变量的类不能拥有自己的序列化函数。

OnSerialize函数应该返回True来指示有更新需要发送,如果它返回了true,这个类的所有脏标志位都会被清除,如果它返回False,则脏标志位不会被修改。这可以允许将多次改动合并在一起发送,而不需要每一帧都发送。

6.回调函数
7.远程调用的参数
基本数据类型(字节,整数,浮点树,字符串,64位无符号整数等)、基本数据类型的数组、包含允许的数据类型的结构体、Unity内建的数学类型(Vector3,Quaternion等)、NetworkIdentity、NetworkInstanceId、NetworkHash128、带有NetworkIdentity组件的物体
(四)序列化流程
具有NetworkIdentity组件的游戏物体可以带有多个从NetworkBehaviour派生出来的脚本,当这些物体要进行同步操作时,会执行序列化流程。这些物体的序列化流程为:

在服务器上:
·每个NetworkBehaviour上都有一个脏数据掩码,这个掩码可以在OnSerialize函数中通过syncVarDirtyBits访问到
·NetworkBehavious中的每个同步变量被指定了脏数据掩码中的一位
·对同步变量的修改会使对应的脏数据位被设置
·或者可以通过调用SetDirtyBit函数直接修改脏数据标志位
·服务器的每个Update调用都会检查他的NetworkIdentity组件
· 如果有标记为脏的NetworkBehaviour,就会为那个物体创建一个更新数据包
·每个NetworkBehaviour组件的OnSerialize函数都被调用,来构建这个更新数据包
·没有脏数据位设置的NetworkBehaviour在数据包中添加0标志
·有脏数据位设置的NetworkBehavious写入他们的脏数据和有改动的同步变量的值
· 如果一个NetworkBehavious的OnSerialize函数返回了True,那么他的脏标志位被重置,因此直到下一次数据修改之前不会被再次发送
·更新数据包被发送到能看见这个物体的所有客户端

在客户端:
·接收到一个物体的更新数据包
·每个NetworkBehavious脚本的OnDeserialize函数被调用
·这个物体上的每个NetworkBehavious脚本读取脏数据标识
·如果关联到这个NetworkBehaviour脚本的脏数据位是0,OnDeserialize函数直接返回;
·如果脏数据标志不是0,OnDeserialize函数继续读取后续的同步变量
·如果有同步变量的钩子函数,调用钩子函数

六. Unet项目构建流程
接下来由一个Demo来介绍Unet项目构建的基本流程:
1.在场景中创建一个空的GameObject,将其更名为Manager,并在其上挂载NetworkManger,NetwokManagerHUD。(在实际项目中的UI界面由自己编写,UI中的参数等通过[SyncVar]进行同步)

2.在场景中创建一个Cube,设置其Scale中的x、y值为100,当作是demo的地形。

3.现在制作palyer,还是在场景中创建一个cube,大小位置等默认不变,改名为palyer,在其上挂载NetworkIdentity、NetworkTransform、Rigidbody组件。

4.将player的prefab拖入Manager的Spawn Info中的PlayerPrefab中。
5.接下来编写控制player移动的脚本,创建C#脚本,命名为PlayerMove,脚本内如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMove : MonoBehaviour {

    Rigidbody rig;
    Vector3 moveDerictor;
    public float speed = 10f;

    // Use this for initialization
    void Start () {
        rig = GetComponent();
    }

    // Update is called once per frame
    void Update () {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        MoveController(h, v);
    }

    /// 
    /// 控制角色移动
    /// 
    /// x轴移动
    /// z轴移动
    private void MoveController(float h, float v)
    {
        moveDerictor.Set(h, rig.transform.position.y, v);
        moveDerictor = moveDerictor.normalized * speed * Time.deltaTime;
        rig.MovePosition(transform.position + moveDerictor);
    }
}

运行游戏,此时物体可以通过键盘输入进行移动,接下来进行下面的操作。

6.通过上述代码可以实现player的移动,但是现在只能在本地移动,移动的状态、位置并不能同步到其他客户端上去,这是就要对脚本与Player Prefab进行改动:
(1) 对Player Prefab的NetworkIdentity组件中勾选local Player Authority选项,这样本地客户端就可以对player进行操作。
(2) 对于脚本来说,要对Player Prefab进行一个判断,当为本地客户端的player时,进行相应的操作,比如demo中的移动,当移动发生变化时,要将变化后的属性通过Cmd函数进行一个广播。当为非本地的player时,对其数据进行一个差值更新。
脚本如下:


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour {

    Rigidbody rig;
    Vector3 moveDerictor;
    public float speed = 10f;

    //同步位置的变量
    [SyncVar(hook = "SyncPositionValues")]
    private Vector3 syncPos;    

    //更新间隔
    float PosLerpRate = 25;

    // Use this for initialization
    void Start () {
        //当为本地玩家时,进行操作
        if (isLocalPlayer)
        {
            rig = GetComponent();
            rig.transform.position = new Vector3(Random.Range(0,10),0, Random.Range(0, 10));
        }
    }

    // Update is called once per frame
    void Update () {
        if (!isLocalPlayer) //非本地玩家进行差值更新
        {
            PositionRotationLerp();
            return;
        }
        if (isLocalPlayer)  //当是本地玩家时,进行移动操作
        {
            //控制的是旋转
            float h = Input.GetAxis("Horizontal");
            float v = Input.GetAxis("Vertical");

            MoveController(h, v);
        }
    }

    /// 
    /// 控制角色移动
    /// 
    /// x轴移动
    /// z轴移动
    private void MoveController(float h, float v)
    {
        moveDerictor.Set(h, 0, v);
        moveDerictor = moveDerictor.normalized * speed * Time.deltaTime;
        rig.MovePosition(transform.position + moveDerictor);
    }

    /// 
    /// 对于非本地玩家进行差值更新
    /// 
    /// 
    void SyncPositionValues(Vector3 pos)
    {
        syncPos = pos;
    }

    /// 
    /// 对于非本地玩家进行差值更新
    /// 
    private void PositionRotationLerp()         
    {
        this.transform.position = Vector3.Lerp(transform.position, syncPos, PosLerpRate * Time.deltaTime);
    }

    void FixedUpdate()
    {
        TransmitPosition();
    }

    private void TransmitPosition()
    {
        if (isLocalPlayer)
        {
            CmdUpdatePositionToServer(this.transform.position);
        }
    }

    /// 
    /// 服务端收到信息同步给所有客户端的该对象的syncPos变量
    /// 
    /// 
    [Command]
    void CmdUpdatePositionToServer(Vector3 pos)
    {
        syncPos = pos;        
    }
}

从前后两个脚本的代码差异就可以看出进行网络连接时,脚本要做什么。要注意的是,所有与网络连接有关的脚本都要先引用UnityEngine.Networking命名空间,再继承NetworkBehaviour。

7.按照以上步骤操作后,就可以实现两个客户端通过网络实现物体移动的同步与异步操作。

你可能感兴趣的:(unity,C#)