Unet基础学习

转载自:http://www.cnblogs.com/xianunity/p/5643469.html

包含

[SyncVar]

[Command]

[ClientCallback]

[SeverCallback]

 

Unet针对不同用户的使用有高级和低级API两种--Unity制作多玩家游戏的用户,这类用户应该从NetworkManager或者高级API开始。
搭建网络架构或制作高级的多玩家游戏的用户,这类用户应该从网络传输层低级API开始。

  下面从高级APIHLAPI)开始介绍,使用高级API可以实现:

  - 通过Network Manager控制游戏的网络状态
  - 操作客户端主持的游戏,主持游戏的客户端同时也是一个玩家
  - 使用一个通用的序列化器序列化数据
  - 发送和接收网络消息
  - 从客户端向服务器发送网络命令
  - 在服务器上远程调用客户端提供的过程(RPC
  - 从服务器向客户端发送网络事件

关于Unet的介绍网上有很多,大家可以自行查找。

 

  首先,Unet提供了网络连接以及管理的组件:

  网络管理器(NetworkManager)可以作为控制多人游戏的核心组件。在场景中创建一个空的游戏对象或挑选一个方便管理器对象。从Network/NetworkManager菜单项里选择添加NetworkManager组件。新添加的NetworkManager应如图所示:


在编辑器中的NetworkManager的的inspector 面板上,有允许您配置和控制与网络相关的很多东西。

  NetworkManagerHUD 是与NetworkManager相关的的另一个组件。游戏运行控制网络状态时,它给你一个简单的用户界面。用来调试或者实验都可以,但不能用作游戏最终的 ui 设计。NetworkManagerHUD 看起来像:


真正的游戏用户需要自己设置正确的UI ,用于控制游戏状态和允许玩家选择什么样的游戏 。用户设置自己的UI时,脚本需要继承NetworkManager,使用里面的StartHost()StartClient()来开启伺服器和客户端。并且可以重写方法来检测连接或者实现相应功能,egOnClientConnectOnStartHost等。

  之后便可以使用一系列组件实现自己想要的功能了:

  

unity自带的组件有上述这些,根据名字也可以大致推测出可以实现什么功能,下面简要介绍一下:

--Animator:动画同步

--Identity:网络标识组件(带有Identity的物体只有网络开启后才会显示出来)

--LobbyManager:游戏大厅

--LobbyPlayer:玩家控制

--Manager:网络管理器

--ManagerHUD:默认UI

--StartPosition:游戏开始玩家所处的位置

--Tranform:物体位移等的同步控制(挂载在需要同步物体的最小父物体上即可)

 

 

 

同步中的操作

 

同步变量[SyncVar]--

   同步变量是NetworkBehaviour脚本中的成员变量,他们会从服务器同步到客户端上。当一个物体被派生出来之后,或者一个新的玩家中途加入游戏后,他会接收到他的视野内所有物体的同步变量。成员变量通过[SyncVar]标签被配置成同步变量:

class Player :NetworkBehaviour
{
[SyncVar]
int health;

public void TakeDamage(int amount)
{
if (!isServer)
return;

health -= amount;
}
}
  同步变量的状态在OnStartClient()之前就被应用到物体上了,所以在OnStartClient函数中,物体的状态已经是最新的数据。

  同步变量可以是基础类型,如整数,字符串和浮点数。也可以是Unity内置数据类型,如Vector3和用户自定义的结构体,但是对结构体类型的同步变量,如果只有几个字段的数值有变化,整个结构体都会被发送。每个NetworkBehaviour脚本可以有最多32个同步变量,包括同步列表(见下面的解释)。

  当同步变量有变化时,服务器会自动发送他们的最新数据。不需要手工为同步变量设置任何的脏数据标志位。

  注意在属性设置函数中设置一个同步变量的值不会使他的脏数据标志被设置。如果这样做的话,会得到一个编译期的警告。因为同步变量使用他们自己内部的标识记录脏数据状态,在属性设置函数中设置脏位会引起递归调用问题。

  同步变量还可以指定函数,使用hook

  当服务器改变了playerName的值,客户端会调用OnMyName这个函数
[SyncVar(hook = "OnMyName")]
public string playerName = "";

public void OnMyName(string newName)
{
            playerName = newName;
            nameInput.text = playerName;
}

同步列表(SyncLists--

  同步列表类似于同步变量,但是他们是一些值的列表而不是单个值。同步列表和同步变量都包含在初始的状态更新里。同步列表不需要[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<Buf> {}
TestBufs m_bufs = new TestBufs();

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

void Start()
{
m_bufs.Callback = BufChanged;
}
}

定制序列化函数--

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

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

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

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

序列化流程--

  具有NetworkIdentity组件的游戏物体可以带有多个从NetworkBehaviour派生出来的脚本,这些物体的序列化流程为:

在服务器上:

-
每个NetworkBehaviour上都有一个脏数据掩码,这个掩码可以在OnSerialize函数中通过syncVarDirtyBits访问到

- NetworkBehavious
中的每个同步变量被指定了脏数据掩码中的一位

-
对同步变量的修改会使对应的脏数据位被设置

-
或者可以通过调用SetDirtyBit函数直接修改脏数据标志

服务器的每个Update调用都会检查他的NetworkIdentity组件

-
如果有标记为脏的NetworkBehaviour,就会为那个物体创建一个更新数据包

-
每个NetworkBehaviour组件的OnSerialize函数都被调用,来构建这个更新数据包

-
没有脏数据位设置的NetworkBehaviour在数据包中添加0标志

-
有脏数据位设置的NetworkBehavious写入他们的脏数据和有改动的同步变量的值

-
如果一个NetworkBehaviousOnSerialize函数返回了True,那么他的脏标志位被重置,因此直到下一次数据修改之前不会被再次发送

-
更新数据包被发送到能看见这个物体的所有客户端

在客户端:

-
接收到一个物体的更新数据包

-
每个NetworkBehavious脚本的OnDeserialize函数被调用

-
这个物体上的每个NetworkBehavious脚本读取脏数据标识

-
如果关联到这个NetworkBehaviour脚本的脏数据位是0OnDeserialize函数直接返回;

-
如果脏数据标志不是0OnDeserialize函数继续读取后续的同步变量

-
如果有同步变量的钩子函数,调用钩子函数

对下面的代码:

public class data :NetworkBehaviour
{
[SyncVar]
public int int1 = 66;

[SyncVar]
public int int2 = 23487;

[SyncVar]
public string MyString = "esfdsagsdfgsdgdsfg";
}
产生的序列化函数OnSerialize将如下所示:

public override boolOnSerialize(NetworkWriter writer, bool forceAll)
{
if (forceAll)
{
//
第一次发送物体信息给客户端,发送全部数据
writer.WritePackedUInt32((uint)this.int1);
writer.WritePackedUInt32((uint)this.int2);
writer.Write(this.MyString);
return true;
}
bool wroteSyncVar = false;
if ((base.get_syncVarDirtyBits() & 1u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.WritePackedUInt32((uint)this.int1);
}
if ((base.get_syncVarDirtyBits() & 2u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.WritePackedUInt32((uint)this.int2);
}
if ((base.get_syncVarDirtyBits() & 4u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.Write(this.MyString);
}

if (!wroteSyncVar)
{
// write zero dirty bits if no SyncVars were written
writer.WritePackedUInt32(0);
}
return wroteSyncVar;
}
反序列化函数将如下:

public override voidOnDeserialize(NetworkReader reader, bool initialState)
{
if (initialState)
{
this.int1 = (int)reader.ReadPackedUInt32();
this.int2 = (int)reader.ReadPackedUInt32();
this.MyString = reader.ReadString();
return;
}
int num = (int)reader.ReadPackedUInt32();
if ((num & 1) != 0)
{
this.int1 = (int)reader.ReadPackedUInt32();
}
if ((num & 2) != 0)
{
this.int2 = (int)reader.ReadPackedUInt32();
}
if ((num & 4) != 0)
{
this.MyString = reader.ReadString();
}
}
  如果这个NetworkBehaviour的基类也有一个序列化函数,基类的序列化函数也将被调用。

  注意更新数据包可能会在缓冲区中合并,所以一个传输层数据包可能包含多个物体的更新数据包。

远程动作--

  网络系统允许在网络上执行远程的动作。这类动作有时也叫做远程过程调用(RPC)。有两种类型的远程过程调用,命令(Commands由客户端发起,运行在服务器上;和客户端远程过程调用(ClientRpc - 服务器发起,运行在客户端上。

命令(Commands--

  命令从客户端上的物体发给服务器上的物体。出于安全考虑,命令只能从玩家控制的物体上发出,因此玩家不能控制其他玩家的物体。要把一个函数变成命令,需要给这个函数添加[Command]属性,并且为函数名添加"Cmd"前缀,这样这个函数会在客户端上被调用时在服务器上运行。所有的参数会自动和命令一起发送给服务器。

  命令函数的名字必须要有"Cmd"前缀。在阅读代码的时候,这也是个提示这个函数比较特殊,他不像普通函数一样在本地被执行。

class Player :NetworkBehaviour
{
public GameObject bulletPrefab;

[Command]
void CmdDoFire(float lifeTime)
{
GameObject bullet =(GameObject)Instantiate(
bulletPrefab,

transform.position +transform.right,
Quaternion.identity);

var bullet2D =bullet.GetComponent<Rigidbody2D>();
bullet2D.velocity = transform.right *bulletSpeed;
Destroy(bullet, lifeTime);

NetworkServer.Spawn(bullet);
}

void Update()
{
if (!isLocalPlayer)
return;

if (Input.GetKeyDown(KeyCode.Space))
{
CmdDoFire(3.0f);
}
}
}
  注意如果每一帧都发送命令消息,会产生很多的网络流量。

  默认情况下,命令是通过0号通道(默认的可靠传输通道)进行传输的。所以默认情况下,所有的命令都会被可靠地发送到服务器。可以使用命令的"Channel"参数修改这个配置。参数是一个整数,表示通道号。
1
号通道是默认的不可靠传输通道,如果要用这个通道,把这个参数设置为1,示例如下:

[Command(channel=1)]

  从Unity5.2开始,可以从拥有客户端授权的非玩家物体发出命令。这些物体必须是使用函数NetworkServer.SpawnWithClientAuthority()派生出来的,或者是使用NetworkIdentity.AssignClientAuthority()授权过的。从物体发送出来的命令会在服务器上运行,而不是在相关玩家物体所在的客户端上。

客户端远程过程调用(ClientRPC Calls

  客户端远程过程调用从服务器的物体发送到客户端的物体上去。他们可以从任何带有NetworkIdentity并被派生出来的物体上发出。因为服务器拥有授权,所以这个过程不存在安全问题。要把一个函数变成客户端远程过程调用,需要给函数添加[ClientRpc]属性,并且为函数名添加"Rpc"前缀。这个函数将在服务端上被调用时,在客户端上执行。所有的参数都将自动传给客户端。

  客户端远程调用必须带有"Rpc"前缀。在阅读代码的时候,这将是个提示这个函数比较特殊,不像一般函数那样在本地执行。

class Player :NetworkBehaviour
{

[SyncVar]
int health;

[ClientRpc]
void RpcDamage(int amount)
{
Debug.Log("Took damage:" +amount);
}

public void TakeDamage(int amount)
{
if (!isServer)
return;

health -= amount;
RpcDamage(amount);
}
}

  当使用伺服器模式运行游戏的时候,客户端远程调用将在本地客户端执行即使他其实和服务器运行在同一个进程。因此本地客户端和远程客户端对客户端远程过程调用的处理是一样的。
  如果想将[ClientRpc]用在点击事件的同步操作上,不能直接绑定点击事件函数,而是应该起一个新的Rpc函数,点击事件去绑定这个Rpc函数,Rpc函数里才是对点击事件的操作:

 //点击事件
    public void ClickDXView()
    {

            RpcDXView();

    }

    [ClientRpc]
    public void RpcDXView()
    {
        readyPN.gameObject.SetActive(false);
        startGm();
        Camera.main.GetComponent<DOTweenPath>().DOPlay();
    }

回调函数--

[ServerCallback]:只执行在服务器端,并使一些特殊函数(eg:Update)不报错(若在此函数中改变了带有syncvar的变量,客户端不同步)

          (使用ServerCallback时,将Update中的重要语句摘出来写入Rpc函数中并调用)

[ClientCallback]:只执行在客户端

另:[Server]:只执行在服务器端但是不能标识一些特殊函数(可以在这里调用Rpc类函数)

   

远程过程的参数

  传递给客户端远程过程调用的参数会被序列化并在网络上传送,这些参数可以是:

-
基本数据类型(字节,整数,浮点树,字符串,64位无符号整数等)

-
基本数据类型的数组

-
包含允许的数据类型的结构体

- Unity
内建的数学类型(Vector3Quaternion等)

- NetworkIdentity

- NetworkInstanceId

- NetworkHash128

-
带有NetworkIdentity组件的物体

  远程过程的参数不可以是游戏物体的子组件,像脚本对象或Transform,他们也不能是其他不能在网络上被序列化的数据类型。

    

  在使用过程中发现一个问题:带有NetworkIdentity的组件在运行之前不能是隐藏的,否则同步会受影响,在代码Start函数中置为SetActive = false,或者因为网络问题一开始隐藏的物体在后续同步中都没有问题。

猜你喜欢

转载自blog.csdn.net/qq_25601345/article/details/78551567