Unity3d多人游戏与网络功能(四)-状态机同步与远程动作

状态同步

状态同步是从服务器向客户端方向上的。本地客户端没有序列化的数据,因为它和服务器共享同一个场景。任何为本地客户端序列化的数据都是多余的。然而,SyncVar钩子函数会被本地客户端调用。

数据不会从客户端向服务器同步,这个方向上的操作叫做命令(Commands)。

同步变量

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

class Player :NetworkBehaviour
{
[SyncVar]
int health;

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

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

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

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

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

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

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

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

序列化流程

具有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函数继续读取后续的同步变量

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

对下面的代码:

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);
}
}

当使用伺服器模式运行游戏的时候,客户端远程调用将在本地客户端执行 – 即使他其实和 服务器 运行在同一个进程。因此本地客户端和远程客户端对客户端远程过程调用的处理是一样的。

远程过程的参数

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

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

- 基本数据类型的数组

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

- Unity内建的数学类型(Vector3,Quaternion等)

- NetworkIdentity

- NetworkInstanceId

- NetworkHash128

- 带有NetworkIdentity组件的物体

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


相关资料:

http://www.itnose.net/st/6318206.html

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值