unreal 可破坏

2017-03-11 16:41:47 zjjwch 阅读数 1605
综述

多玩家游戏与共享现实有关:所有的玩家都感觉他们都在同一世界里,在这个世界里以不同视角看着相同的事件发生。最初的多玩家游戏是双玩家的调制解调器游戏,以DOOM为代表,而现在多玩家游戏已经进化成为大型的、持久的、交互形式更加自由的游戏,如Quake2,Unreal和Ultima Online,共享现实背后的技术已经有了巨大的进步。

从一开始就实现网络!

需要实现的一件重要事情是:如果你计划要在游戏中支持联网的多玩家,在你开发游戏的时候要建立和测试网络!建立一个高效的网络实现对于游戏对象设计决策具有重要影响。改进解决方案是很困难的,当不考虑网络时,像跨越很多对象分解功能这样的重要决策可能会在多玩家游戏中带来重大问题。

点对点模型

在一开始的时候,有像DoomDuke Nukem这样的点对点模型,游戏中每台机器都是平等的。每台机器都精准地同步其输入,并与其他机器校准时间,而且每台机器在完全相同的输入上执行相同的精确游戏逻辑。结合了完全确定的(即固定速率,非随机)游戏逻辑,机器中的所有玩家都感知到相同的现实。

这种方法的优势是简洁。劣势是:

  • 缺乏持久性。所有的玩家都必须一起开始游戏,新玩家不能随心所欲地想来就来想走就走。
  • 缺乏玩家可扩展性。由于网络架构的锁步性质,协调的开销和网络引起故障的可能性随着玩家数量的增加线性增加。
  • 缺乏帧率可扩展性。所有玩家必须以相同的内部帧率运行,这很难支持多种机器速度。

客户端-服务器模型

下一个是庞大的客户端-服务器架构,Quake最先使用,之后Ultima Online也使用了这种架构。在这种模型中,一台机器被指定为“服务器”,负责进行所有的游戏运行决策。其他机器是“客户端”,它们被看作是不声不响的显示终端,这些机器会把它们的“击键”发送给服务器,之后收到需要呈现对象的列表。这种进步支持大规模的互联网游戏,因为游戏服务器开始在互联网上兴起。客户端 -服务器体系结构随后被QuakeWorld和Quake 2扩展,将额外的模拟和预测逻辑移到客户端,以便在降低带宽使用的同时增加可见的细节。这里,客户端不仅接收要呈现的对象的列表,而且要接收关于它们的轨迹的信息,因此客户端可以对对象运动做出初步的预测。此外,这种模型引入了锁步预测协议,为的是消除客户端动作中的感知延迟。

但是,这种方式还是有一些缺点:

  • 缺乏开放性——当用户和有权限的人创建新对象类型时(武器、玩家控件等),必须创建黏合逻辑以指定这些新对象模拟和预测方面的内容。
  • 预测模型的困难——在这个模型中,网络代码和游戏代码各自是独立的模块,但是每个模块都必须充分了解对方的实现,为的是保持游戏状态合理同步。(理想上)不同独立模块间的强耦合是不合需要的,因为这会让扩展变得困难。

Unreal网络架构

Unreal将一个新方法引入到多玩家游戏中,术语叫广义客户端-服务器模型。在这个模型中,服务器在游戏状态的演变的过程中仍然是权威。然而,客户端实际上在本地保留了游戏状态的准确子集,并且可以通过执行与服务器相同的游戏代码来预测游戏流程,对于大致相同的数据,从而最小化必须在两个机器之间交换的数据量。服务器通过复制相关参与者及其复制属性来向客户端发送有关世界的信息。客户端和服务器还通过复制函数进行通信,当一个参与者调用了函数,这些复制函数仅在拥有该参与者的服务器和客户端之间复制。

进一步讲,游戏状态是通过一种可扩展的面向对象脚本语言UnrealScript自描述的,UnrealScript将游戏逻辑从网络代码中完全解耦出来。网络代码以这样一种方式推广:它可以适应任何被语言描述的游戏。这就达到了面向对象提升可扩展性的目的,有一种概念认为对象的行为应该完全被该对象描述,不要引入对于其他代码块的依赖,这些代码块通过硬编码才能了解该对象的内部实现。

基本概念


目标

这里的目标是以一种相对严格的方式定义Unreal的网络架构,因为这复杂性很高,如果没有准确定义就会很容易产生误解。

基本术语

我们精确定义基本术语:

  • 变量(variable)是固定名称和可修改值之间的联系。变量的例子包括像X=123这样的整型,Y=3.14这样的浮点数,Team="Rangers"这样的字符串,和 V=(1.5,2.5,-0.5)这样的向量。
  • 对象(object) 是一个自包含的数据结构,由一组固定变量组成。
  • 参与者(actor)是可以在一个级别上独立移动的对象,而且可以在该级别上于其他参与者交互。
  • 级别(level)是一个包含了一组参与者的对象。
  • 滴答(tick)是一个操作,假如一段变量DeltaTime时间已经过去了,该操作就会更新整个游戏。
  • 级别的游戏状态(game state)指的是存在于当前级别中所有参与者的完整集合,以及目前不进行滴答操作时所有变量的当前值。
  • 客户端(client)是UE的一个运行实例,它保留了适合粗略模拟世界中发生事件的游戏状态的近似子集,该子集也适合为玩家呈现一个世界的大致视图。
  • 服务器(server)是UE的一个运行实例,它负责为一个级别滴答, 权威地与所有客户端进行通信。

更新循环

可能除了滴答和游戏状态,上述所有概念都很容易理解。那么,我们之后会更详细地进行介绍。首先,这是对于Unreal更新循环的简单描述:

  • 如果我是一台服务器,我与所有客户端通信当前游戏状态。
  • 如果我是一个客户端,我向服务器发送我请求的动作,从服务器收到新的游戏状态信息,向屏幕渲染我当前的近似世界视图。
  • 如果在之前滴答操作之后已经过去了一段变量DeltaTime时间,进行一个滴答操作来更新游戏状态。

一个滴答操作包括更新级别中的所有参与者,完成参与者的物理操作,通知它们已经发生的有趣游戏事件,执行任何需要的脚本代码。Unreal中所有的物理现象和更新代码都设计用于处理一个变量时间的流逝。

例如,Unreal的行动物理看起来像:

位置 += 速度 *DeltaTime

这带来了更强的帧率可扩展性。

当正在进行一个滴答操作时,游戏状态被执行的代码持续更新。准确来说游戏状态可以以三种方式改变:

  • 可以修改参与者中的一个变量
  • 可以创建一个参与者
  • 可以销毁一个参与者

服务器是一个人

从上面来看,服务器的游戏状态是由处于同一级别的所有参与者的所有变量集合完全、简明地确定的。因为服务器对于游戏运行流是权威的,服务器的游戏状态可以总是被看做一个真实的游戏状态。客户端机器上的游戏状态版本应该总是被看做近似服从各种不同误差,这些误差来自于服务器游戏状态。存在于客户端机器上的参与者可以被看做是代理,因为它们是一个对象暂时的、近似的代表,而不是对象本身。

当客户端加载一个级别在联网的多玩家游戏中使用时,它会删除该级别上的所有参与者,除了那些将bNoDelete设置为true或bStatic设置为true的参与者。其他与该客户端相关的参与者(由服务器决定)会被从服务器复制到客户端。一些参与者(如GameInfo Actor)从不会被复制到客户端。

带宽限制

如果网络带宽是无限的,网络代码就会非常简单:在每个滴答操作的最后,服务器只是会向客户端发送完整的、准确的游戏状态,所以客户端总是会显示正在服务器上发生的游戏界面。但是,互联网现实是:28.8K的调制解调器可能只有带宽的1%是需要通信完整、准确更新的。但是消费者的网络连接未来会更快,带宽的增长率比摩尔定律低得多,摩尔定律定义了游戏和图形学中的增长率。因此,现在和未来对于完全游戏状态更新来讲,都不会有足够的带宽。

所以,网络代码的主要目标就是让服务器能够与客户端通信游戏的合理近似状态,这样一来客户端就可以渲染世界的交互视图,这些视图很接近共享现实,如果给定了带宽限制,这也会很合理。

复制

Unreal把“在服务器和客户端之间协调处一个合理的共享现实近似值”的问题,看成“复制”的问题。也就是说,问题是:决定数据和指令集,这些数据和指令从客户端和服务器之间流动,为的是实现这个近似共享现实。

参与者


角色

总的来说,每个都有一个RoleRemoteRole属性,这些属性在服务器和客户端上有不同的值。服务器上的每个参与者都有一个Role设置给ROLE_Authority

服务器上的参与者可能有它们的RemoteRole

  • ROLE_AutonomousProxy——被复制到拥有客户端时,PlayerControllers和它们控制的Pawn
  • ROLE_SimulatedProxy——所有其他复制的参与者
  • ROLE_None——从不会被复制给任何客户端的参与者

服务器上参与者的RemoteRole是该参与者在客户端上的Role。所有复制到客户端的参与者都有设置到ROLE_AuthorityRemoteRole。.

定义

Actor类定义了ENetRole枚举和两个变量RoleRemoteRole,如下:

Actor.uc

//Net variables.

enumENetRole

{

   ROLE_None,              // 没有角色

   ROLE_SimulatedProxy,    // 该参与者的本地模拟代理

   ROLE_AutonomousProxy,   //该参与者的本地自治代理

   ROLE_Authority,         // 对于参与者的权威控制

};

varENetRole RemoteRole, Role;

RoleRemoteRole变量描述了本地和远程机器分别对于参与者有多大控制能力:

  • Role == ROLE_SimulatedProxy ——意味着参与者是临时的、近似的代理,这种代理可以模拟物理和动画。在客户端,模拟的代理执行它们基本的物理运动(线性的或者受重力影响的移动和碰撞),但是它们不会做任何高等级的移动决策,它们就这么进行。 它们可以用simulated关键字执行脚本函数,而且可以进入标记为simulated的状态。这种情况只可能在网络客户端中发生,不会在网络服务器或单人游戏中发生。
  • Role == ROLE_AutonomousProxy——意味着参与者是本地玩家。自治代理对于客户端的移动预测(不是模拟)有特殊的逻辑。这些代理可以在客户端上执行任何脚本函数;而且它们可以进入任何状态。这种情况只在网络客户端中发生, 不会在网络服务器或单人游戏中发生。
  • Role == ROLE_Authority ——意味着该机器对于参与者有绝对的、权威的控制。

这是所有单人游戏的情况。这些游戏可以执行任何脚本函数;而且可以进入任何状态。

这是服务器上所有参与者的情况。

在客户端上,这是针对客户端在本地产生参与者的情况,比如无端的特效,这些特效是在客户端完成的,为的是节约带宽用量。

在服务器端,所有的参与者都有Role ==ROLE_Authority,而且RemoteRole设置给代理类型之一。在客户端,RoleRemoteRole总是与服务器值正好反过来。不出所料,这是来自RoleRemoteRole的含义。

大多数值的含义都是在UnrealScript类中用复制语句定义的,比如ActorPlayerPawn。下面有几个例子,关于复制语句怎样定义不同角色值的含义:

注:这些例子是与UE1和UE2严格相关的;但是核心概念与UE3是相同的:

  • Actor.AmbientSound变量是由服务器发送给客户端的,因为在Actor类中的复制定义:

o   if(Role== ROLE_Authority) AmbientSound;

  • Actor.AnimSequence变量是由服务器发送给客户端的,但是只对渲染成网格的参与者是这样,因为在Actor类中的复制定义:

o   if(DrawType== DT_Mesh && RemoteRole <= ROLE_SimulatedProxy) AnimSequence;

  • 当模拟代理最初产生时,服务器会向客户端发送所有这些模拟代理和移动刷子的速率,因为在Actor类中的复制定义:

o   if((RemoteRole== ROLE_SimulatedProxy && (bNetInitial || bSimulatedPawn)) || bIsMover)Velocity;

通过在所有的UnrealScript类中研究复制语句,你会理解所有角色的内部工作机制。关于复制,很少有“behind-the-scenes _magic_”发生:在低级的C++级别上,引擎提供了复制参与者、函数调用和变量的基本机制。在较高的UnrealScript级别上,不同网络角色的含义是这样定义的:基于不同的角色指定什么变量和函数应该被复制。所以,在UnrealScript中,角色的含义几乎是自定义的,例外是一小部分的behind-the-scenesC++逻辑,这些逻辑有条件地为模拟代理更新物理和动画。

关联

定义

一个Unreal关卡可能很巨大,任何时候玩家都只能看到关卡中参与者的一小部分,关卡中大部分的其他参与者都是不可见的、不可听到的,而且对于玩家没有重大影响。服务器认为可见或者能够影响客户端的参与者都被视为是与该客户端有关的参与者集合。Unreal网络代码中的显著带宽优化是服务器只告诉客户端关于该客户端中相关集合的事情。

在为一个玩家确定相关的参与者集合时,Unreal(按顺序)应用如下规则:

  • 如果参与者的RemoteRole是ROLE_None,那就是不相关的。
  • 如果该参与者与附属于另外一个参与者的骨架,那么其相关性是由它的基类的相关性决定的。
  • 如果参与者有bAlwaysRelevant,那就是相关的。
  • 如果角色的bOnlyRelevantToOwner设置为true(用于详细目录),它可能只是与客户端相关,而且该客户端的玩家拥有该参与者。
  • 如果玩家拥有参与者(Owner == Player),那就是相关的。
  • 如果参与者是隐藏的(bHidden = true),那么它就不会碰撞(bBlockPlayers = false),也不会有环境音(AmbientSound == None),那么该参与者就是不相关的。
  • 根据参与者位置和玩家位置之间的瞄准线校验,如果参与者是可见的,那就是相关的。
  • 如果参与者在之前的2到10秒之内是可见的(确切的数字是不定的,因为一些性能优化),那就是相关的。

注意bStaticbNoDelete参与者(存在于客户端)也可以被复制。

制定这些规则是为了给出参与者集的良好近似解,这些参与者真的可以影响玩家。当然,这是不完美的:如果有巨大的参与者,瞄准线校验有时可能会错误的负值(尽管我们会用一些启发法来解决这个问题),它不负责环境音的声音遮蔽等事情。然而,近似解就是这样,它的错误被网络环境中固有的错误所淹没,互联网会有延迟和包丢失这样的特征。

优先化

在基于调制解调器的互联网连接中,在死亡竞赛游戏中,服务器几乎永远不会有足够的带宽来告诉每个客户端它们想要了解的游戏状态,Unreal用了一个负载平衡小技巧,优先排序所有的参与者,基于每个参与者对于游戏运行的重要性,再给每个参与者一个比较公平的带宽份额。

每个参与者都有叫NetPriority的浮点型变量,数值越高,该参与者相对于其他参与者收到的带宽就越多。一个拥有2.0优先权的参与者正好是1.0优先权参与者更新频率的两倍。关于优先权唯一重要的事是它们的比率;所以很明显,你不能通过增加所有优先权来提升Unreal的网络性能。所以我们在性能调节中给NetPriority赋的一些值是:

  • Actor - 1.0
  • Pawns - 2.0
  • PlayerController - 3.0
  • Projectiles - 2.5
  • Inventory - 1.4
  • Vehicule - 3.0

复制


复制综述

网络代码基于三个简单的、低级复制操作,这些操作用于在服务器和客户端间通信游戏状态信息:

参与者复制

服务器为每个客户端都定义了一组“相关的”参与者(这些参与者要么对客户端可见,要么可能会对客户端视图或即时运动有点影响),并且告诉客户端创建和维护一个参与者的“复制的”副本。但是服务器总是有该参与者的权威版本,在任何时刻,许多客户端都可能有那个参与者近似的、复制的版本。

当一个复制的参与者是在客户端上产生时,只有LocationRotation(如果bNetInitialRotation设为true就是合法的)是在PreBeginPlay()PostBeginPlay()期间合法的。复制的参与者只能被销毁,因为服务器会关闭它们的复制通道,但如果bNetTemporarybTearOff属性已经被设为true那就例外了。

参与者属性复制是可靠的,这意味着参与者的客户端版本属性最终会反映出服务器上的值,不是所有的那些属性值变化都会被复制。在任何情况下,参与者属性都只能从服务器复制到客户端;如果参与者属性包括在定义了那个属性的Actor类的复制定义中,那么这样的属性就会被复制。

复制定义指定了复制条件,这些条件描述了在目前考虑情况下,何时以及是否需要将一个给定属性复制到客户端。即使一个参与者是不相关的,也不会复制它全部的属性。仔细指定复制条件可以充分地减少带宽使用。

在复制阶段只有三个参与这属性是合法的,根据客户端,为确定复制的服务器改变值:

  1. 如果任何复制的属性已经被UnrealScript改变,bNetDirty 为真,这不是用作优化(不需要检查UnrealScript复制条件,或者如果bNetDirty为假,不需要检查属性是否在已修改的脚本中被修改)。不要使用bNetDirty来管理频繁更新的属性!
  2. 直到所有参与者属性的初始复制都完成之后,bNetInitial 仍旧为真。
  3. 如果参与者的顶层拥有者是当前客户端拥有的PlayerController 的话,bNetOwner为真

变量复制

描述游戏状态方面的参与者可以被“复制”,这些方面对于客户端来讲很重要。也就是说,在服务器端无论何时该变量值改变,服务器都会向客户端发送更新的值。变量在客户端也有可能改变——在这种情况下新值会覆写原值。变量复制条件是在UnrealScript类的Replication{}块中指定的。

函数调用复制

在网络游戏中在服务器上调用的函数可以被路由到远程客户端,而不是在本地执行。或者,客户端上调用的函数可以被路由到服务器,而不是本地调用。函数复制由服务器、客户端、可靠不可靠的关键字在函数定义中指定。

示例

举一个具体的例子,考虑在网络游戏中你是客户端的情况。你看到两个敌人跑向你,朝你射击,你听到他们的枪声。既然所有的游戏状态都是在服务器上而不是在你的机器上维护的,为什么你能看到和听到这些事情的发生呢?

因此,在这一点上,Unreal多玩家游戏操作的低层次机制就比较明确了。服务器正在更新游戏状态,做出所有大的游戏决定。服务器正在将一些参与者复制到客户端。服务器正在向客户端复制一些变量。服务器正在向客户端复制一些函数调用。

还应该清楚的是,并不是所有的参与者都需要被复制。例如,如果一个参与者在关卡路程的一半,远在你视线之外,你不需要浪费带宽发送更新。此外,所有的变量不需要更新。例如,服务器用来做AI决策的变量不需要发送给客户端,客户端只需要知道它们的显示变量、动画变量和物理变量。此外,在服务器上执行的大多数函数不应该被复制。只有那些能让客户端看到或听到一些东西的函数才需要被复制。因此,总的来说,服务器都包含了大量的数据,只有很小的一部分对客户端比较重要——那些影响玩家看到、听到或感觉的东西。

因此,下一个逻辑问题是,“UE如何知道哪些参与者、变量和函数调用需要复制?”

答案是,为参与者编写脚本的程序员负责确定脚本中需要复制哪些变量和函数。并且,他负责编写一块小的代码名为“复制语句”,在该脚本中,告诉UE需要什么条件下复制。对于现实世界的例子,考虑一些在Actor类中定义的东西。

注:并不是所有的这些变量都在UE3 的Actor.uc中(它们曾出现在UE1和2);但核心概念仍然是有效的。

•在人物类,还有一堆的布尔变量定义按键和按钮,如bfire和bjump。这些是在客户端(输入发生的地方)生成的,服务器需要知道它们。因此,复制条件基本上说:“复制这个如果我是客户端”。

  • 位置变量(矢量)包含参与者的位置。服务器负责维护位置,所以服务器需要把位置变量发送到客户端。因此,复制条件基本上说:“如果我是服务器,复制这个它”。

·        网格变量(对象引用)引用了应该为参与者渲染的网格。服务器需要把网格变量发送给客户端,但是只有参与者是作为网格渲染的情况下才需要发送,也就是说,如果参与者的DrawTypeDT_Mesh的话。因此,复制条件基本说:“如果我是服务器而且DrawType是DT_Mesh,复制它。

·        在PlayerPawn类中,有一大堆布尔型变量,这些变量定义了按键响应和按钮响应,如bFirebJump。这些都是在客户端上生成的(输入在此发生),服务器需要了解这些。因此,这个函数的复制条件可能是“如果我是客户端,复制它”。

  • PlayerController类中,有一个ClientHearSound函数,这个函数会告诉玩家他/她听到了一个声音。这个函数是在服务器上调用的,但是,当然了,声音需要被玩游戏的人确切听到,人是在客户端。所以这个函数的复制条件可能是“如果我是服务器,复制它”。

从上面的例子来看,应该注意以下几点。首先,每个可能被复制的变量和函数需要有一个“复制条件”连接到它,也就是说,一个表达式计算为true或false,取决于是否需要复制东西。其次,这些复制条件应该是双向的:服务器需要能够向客户端复制变量和函数,客户端需要能够将它们复制到服务器上。第三,这些“复制条件”可以是复杂的,如“如果我是服务器,复制,这是第一次通过网络复制这个参与者。”

因此,我们需要一个通用的条件表达方式(复杂),在该条件下应该复制变量和函数。表达这些条件最好的方式是什么?我们看了所有的选项,并认为Unrealscript已经是一个非常强大的语言创作类、变量和代码,会成为编写复制条件的完美工具。

UnrealScript:复制语句

在UnrealScript中,每个类可以有一个复制的语句。复制语句包含一个或多个复制定义。每个复制定义包括一个复制条件(一个计算为true或false的语句),以及条件适用的一个或多个变量的列表。

类中的复制语句只能引用该类中定义的变量。这样,如果Actor类包含一个变量DrawType,然后你知道在哪里寻找它的复制条件:它只能位于Actor类中。

类不包含复制语句是有效的;这仅仅意味着类不定义任何需要复制的新变量或函数。事实上,大多数类不需要复制语句,因为影响显示的“有趣”变量是在类中定义的,并且只由子类修改。

如果在类中定义了一个新变量,但不在复制定义中列出该变量,则意味着您的变量绝对不会被复制。这是规范,大多数变量不需要被复制。

这里是一个复制语句的Unrealscript语法例子,封闭在replication {}块中。这是从PlayerReplicationInfo类中提取出的:

PlayerReplicationInfo.uc

replication

{

   // Things theserver should send to the client.

   if (bNetDirty && (Role == Role_Authority) )

      Score,Deaths, bHasFlag, PlayerLocationHint,

     PlayerName, Team, TeamID, bIsFemale, bAdmin,

     bIsSpectator, bOnlySpectator, bWaitingPlayer, bReadyToPlay,

      StartTime,bOutOfLives, UniqueId;

   if (bNetDirty && (Role == Role_Authority) && !bNetOwner )

     PacketLoss, Ping;

   if (bNetInitial && (Role == Role_Authority) )

      PlayerID,bBot;

}

可靠vs不可靠

unreliable关键字复制的函数不保证能够到达另一方,如果确实到达了另一方,它们可能是乱序收到的。唯一可以预防收到不可靠函数的方法是网络的包丢失和带宽饱和。所以,你需要理解这件怪事,这里我们将非常近似。在不同类型的网络中,结果变化很大,所以我们不能做任何保证:

  • 局域网——在局域网游戏中,我们猜测,不可靠的数据在约99%的情况下都会成功接收。然而,在游戏过程中,成千上万的东西被复制,所以你可以肯定,一些不可靠的数据将丢失。因此,即使你只是针对局域网的性能,在复制函数在线缆上丢失的情况下,你的代码也需要处理不可靠复制函数。
  • 互联网——在一个典型的低质量28.8k ISP连接中,大体上90% - 95%的不可靠函数会收到。换句话说,它经常丢失。

为了获得可靠的和不可靠的函数之间的权衡更好的感觉,检查Unreal脚本中的复制语句,并衡量它们的重要性vs.我们作出的可靠性决定。要谨慎,只有在绝对必要时才使用可靠函数。

变量总是可靠的

即使在数据包丢失和带宽饱和的条件下,变量也总是保证最终到达另一方。这样的变量变化不能保证以它们发送的顺序到达另一方。此外,尽管变量的值最终将被同步,但不是值的每一个变化都可以被复制。

复制条件

下面是类脚本中复制条件的简单示例:

Pawn.uc

replication

{

   if(Role==ROLE_Authority )

      Weapon;

}

这个复制的条件,翻译成英文是“如果这个参与者角色变量的值等于ROLE_Authority,那么这个参与者的Weapon变量应该被复制到所有的客户端,因为这个参与者是相关的”。

复制条件可能是计算为true或false(即布尔表达式)的任何表达式。所以,任何你可以写在UnrealScript中的表达式都可以,包括比较变量;调用函数;并使用布尔!,&&,| |,和^ ^运算符合并条件。

一个参与者的Role变量通常描述了本地机器对参与者的控制程度。ROLE_Authority意味着“本机是服务器,所以它对于代理参与者是完全权威的”。ROLE_SimulatedProxy意味着“本机是客户端,它应该模拟(预测)的参与者的物理”。在后面的部分中详细描述了Role,但快速总结如下:

  • if (Role == ROLE_Authority) ——意味着“如果我是服务器,我应该把这复制给客户端” 。
  • if (Role < ROLE_Authority) ——意味着“如果我是客户端,我应该把这复制给服务器”。

在复制语句中经常使用下列变量,因为它们具有很高的实用性:

  • bIsPlayer ——参与者是否是个玩家。如果是就是true,是其他参与者就是false。
  • bNetOwner——此参与者是否由正在评估复制条件的客户端所有。例如,假如说“弗莱德”拿着DispersionPistol,“鲍勃”不持任何武器。当DispersionPistol被复制到“弗莱德”时,其bNetOwner变量将变为true(因为“弗莱德”拥有武器)。当它被复制到“鲍伯”时,其bNetOwner变量将是false(因为“鲍伯”没有自己的武器)。
  • bNetInitial——只在客户端合法(也就是说,如果 Role = ROLE_Authority)。显示此参与者是否第一次被复制到客户端。这对于Role = ROLE_SimulatedProxy的客户端是有用的,因为它让服务器就发送一次自己的位置和速度,客户端随后预测。

复制条件指南

由于变量通常是单向复制的(从客户端到服务器,或从服务器到客户端,但绝不是两者同时),通常开始于RoleRemoteRole的比较:例如,if(Role == ROLE_Authority)或者if(RemoteRole< ROLE_SimulatedProxy)。如果复制条件不包含RoleRemoteRole的比较,有可能有点儿问题。

在网络运行过程中,服务器上的复制条件非常非常频繁地被评估。让复制条件尽可能的简单。

当复制条件允许调用函数时,尽量避免这样做,因为它可能会减速很多。

复制条件不应该有任何副作用,因为网络代码可以选择在任何时候调用它们,包括当你不期望的时候。例如,如果你做一些什么像if(Counter++ > 10)……,祝你好运,试着找出会发生什么!

变量复制

更新机制

每次滴答后,服务器检查其相关集合中的所有参与者。所有复制的变量都被检查,看它们自上次更新以来是否已更改,并且对变量的复制条件进行评估,以查看是否需要发送变量。只要有带宽可用的连接,这些变量就可以通过网络发送到其他机器。

因此,客户端接收到世界上正在发生的重要事件的更新,这些事件在客户端可见或听到。关于变量复制的关键点是:

  • 变量复制仅在滴答完成后发生。因此,如果在一个滴答的持续时间内,一个变量会变为一个新的值,然后它返回到它的原始值,那么这个变量就不会被复制。因此,客户只能在其滴完成后听到服务器参与者的状态在滴答过程中变量的状态对客户端是不可见的。
  • 相对于变量之前的已知值,这些变量只会在它们改变时复制。
  • 当变量在客户端的相关集合中时,它们只会被复制到客户端。因此,客户端没有不在参与者相关集合中的参与者准确变量。

UnrealScript没有全局变量的概念;所以只能复制属于一个参与者的实例变量。

变量类型说明

  • 矢量旋转——为了提高带宽效率,Unreal量化了向量和旋转值。向量的X、Y、Z分量在发送之前被转换为16位有符号整型,因此任何超出-32768...32767范围的分数值或值都会丢失。旋转的PitchYawRoll组件转换为字节,形式为(Pitch >> 8) & 255。因此,你需要小心矢量和旋转。如果你必须有充分的精度,那么就使用int或float变量为个体组件使用;所有其他数据类型都以完整精度发送。
  • 一般结构体——这些都是通过发送所有的组件复制的。结构体要么全发送,要么全不发送。
  • 可以复制变量数组,但数组的大小(以字节为单位)必须小于448字节。无法复制动态数组
    • 数组可以被有效复制;如果大数组的单个元素发生更改,则只发送该元素

注:复制规则可以改变,有些规则比其他规则优先级高。例如,结构体中的静态数组总是完整地发送!

参与者属性

参与者属性复制是可靠的。这意味着该参与者的客户端版本的属性将最终反映服务器上的值,而不是所有属性值更改都将被复制。

  • 属性是从服务器复制到客户端。
  • 只有当属性包含在定义了该属性的类的复制定义中时,它们才会被复制。

函数调用复制

远程路由机制

当在一个网络游戏调用UnrealScript函数时,并且该函数含有一个复制关键字,关键字会被评估,执行过程如下:

函数调用被发送到网络连接另一端上的机器来执行。也就是说,函数的名字,和其所有参数,都一起填进数据包中,之后传输到另一台机器上用于之后的执行。当这件事发生时,函数会马上返回,执行继续。如果函数被声明有返回值,那么它的返回值会被设为0(或者相当于零的其他类型,即:0,0,0向量,空对象,等等)。任何输出参数都不受影响。换句话说,UnrealScript从不坐等复制函数的调用完成,所以它不会死锁。发送复制函数调用来让远程机器执行,本地代码继续执行。

与复制变量不同,参与者上的函数调用可以从服务器复制到拥有该参与者的客户端(玩家)。所以,复制函数只在PlayerController的子类中有用(即玩家,拥有它们自身的),Pawn (即玩家的化身,是由一个控制它们的控制器所有),还有Inventory的子类(也就是说,武器和拾取物品,它们是正在带着它们的玩家所有的)。也就是说,函数调用只能被复制给一个参与者(拥有它的玩家);它们不进行组播。

如果在客户端调用带有server关键字标记的函数,它将被复制到服务器上。相反,当在服务器上调用带有client关键字的函数时,它将被复制到拥有那个参与者的客户端。

与复制变量不同的是,复制函数一被调用,这些调用就会发送到远程机器,它们总是被复制而且不考虑带宽。因此,如果复制函数调用过多,可能会淹没可用带宽。只要有可用带宽,复制函数都会吸走,然后剩下的带宽用于复制变量。因此,如果用复制函数淹没连接,变量复制可能没有足够的带宽,视觉上这会导致看不到其他参与者更新,或者在一个非常剧烈的运动中看到参与者更新。

在UnrealScript中,没有全局函数,所以没有“复制的全局函数”的概念。一个函数总是在一个特定的参与者的上下文中调用。

复制函数调用vs 复制函数

太多复制函数可以淹没可用带宽了(因为这些函数总是复制,不管可用带宽有多少),复制变量会自动根据可用带宽节流和分配。

当函数实际被调用时,函数调用只在UnrealScript执行期间复制,但是变量只在没有脚本代码执行时在当前“滴答”的末尾复制。

参与者上的函数调用只会复制到拥有该参与者的客户端,但是参与者的变量会复制到所有与该参与者相关的客户端。

模拟函数和状态


在客户端上,许多参与者以“代理”的形式存在,这意味着服务器创建了参与者的近似副本,再发送到客户端,为客户在听觉上、视觉上提供游戏运行阶段听到和看到的合理近似值。

在客户端上,这些代理参与者经常用客户端物理四处游走,并且影响环境,所以在它们的函数可能在任何时刻被调用。例如,一个模拟代理TarydiumShard炮弹可能遇到一个自治代理Tree参与者。当参与者们发生碰撞时,UE尝试调用它们的Touch()函数来通知碰撞的发生。根据上下文,客户端想要执行其中一些函数调用,但是忽略另外一些。例如,Skaarj的Bump()函数不应该在客户端调用,因为他的Bump()函数想要执行游戏运行逻辑,而游戏运行逻辑应该在服务器上发生,所以Skaarj的Bump()函数不应该被调用。但是,应该调用TarydiumShard炮弹的Touch()函数,因为它阻止了物理,并且生成了一个客户端特效参与者。

UnrealScript函数可以选择性地用simulated关键字来声明,让程序员控制哪些函数应该在代理参与者上执行有良好粒度。对于代理参与者来说(也就是说,Role == ROLE_SimulatedProxy的参与者),只会调用带有simulated关键字声明的函数,其他函数都会跳过。

这是一个典型模拟函数的例子:

simulated function HitWall( vector HitNormal, actorWall )

{

 SetPhysics(PHYS_None);       

 MakeNoise(0.3);   

 PlaySound(ImpactSound);

 PlayAnim('Hit');

}

所以,simulated意味着“这个函数应该总是为代理参与者执行。”

注:确保模拟函数的子类的实现在其定义中也有simulated关键字!当发生这种情况时,Unrealscript的编译器产生警告。

模拟状态类似于模拟函数。

复制模式


在UE和搭载Epic的游戏中,通用复制模式的目标是:

最小化服务器CPU使用

  • 最小化复制参与者的代价
    • 最小化潜在的复制的参与者数量(RemoteRole != ROLE_None的那些)
    • 最大限度地减少需要检查每个客户端相关性的参与者数量。
    • 尽量减少任意给定“滴答”下每个客户端实际关联的参与者数量
    • 尽量减少任意给定“滴答”下每个客户端需要为每个复制参与者检查的复制属性的数量。
    • 避免不必要地设置bNetDirty。
  • 最小化参与者“滴答”代价
    • 避免在服务器上生成不必要的参与者(粒子效果 )。
    • 如果没有游戏运行相关性,避免执行代码。
  • 尽量减少处理收到的复制函数的代价
    • 尽量减少收到函数和需要处理的数量。

由于玩家数量增加,复制参与者的成本是服务器执行时间的主要组成部分,因为它往往以几何级数增长而不是与玩家的数量呈线性关系(因为一些潜在的复制行为往往与玩家的数量测成比例)。

  • 尽量减少带宽使用
    • 每个客户端的相关参与者数量
    • 属性更新的频率
    • 发送的数据包数

Unsuppress DevNetTraffic来查看所有复制参与者和属性的日志。控制台命令Stat Net也是有用的。使用网络探查器检查UE发送和接收的数据包也很有用。

尽量减少感知延迟

客户端根据玩家输入预测客户端拥有的行为;在收到服务器确认之前,模拟这种行为(如有必要进行纠正)。我们将这个模型用于Pawn运动和Weapon处理,而不是用于Vehicle,由于节省带宽和物理模拟的复杂性比Vehicle处理减少延迟的好处重要,其中典型的网络响应延迟与典型的真实世界的Vehicle控制响应延迟没那么不同。

ReplicationInfo类

ReplicationInfo类将bAlwaysRelevant设置为true。服务器的性能可以通过设置一个较低的NetUpdateFrequency改进。每当一个复制的属性发生变化,明确改变NetUpdateTime来进行强制复制。服务器的性能也可以通过将bSkipActorPropertyReplicationbOnlyDirtyReplication设为true真正改进。

使用 ReplicatedEvent()

当复制有Repnotify关键字标记的属性时,会调用ReplicatedEvent()事件,修改的属性作为参数名。系统提供了一种高效的方法来初始化多个属性或者组件,这种方法是基于单个的复制属性更新。比如说,当Vehicle.bDriving改变了,这会被ReplicatedEvent()事件捕捉到,该事件之后会调用DrivingStatusChanged()。我们在UT中使用这种方法启动或关闭引擎声音和其他客户端特效。相似地,当一个UTCarriedObject收到了team属性时,它会更新客户端特性,改变应用于网格的材质这样的组件属性,或者动态光颜色。

当在客户端本地PlayerController拥有的PlayerReplicationInfo更新它的团队属性或所有者属性时,它会调用所有参与者上的NotifyLocalPlayerTeamReceived()

它也可以用于延迟初始化代码的执行,直到所有所需的属性都已经被复制。注意,即使属性没有从默认值发生改变,也不会有复制事件,所以你需要确定参与者在这种情况下已经正确初始化。

WorldInfo类

在一个联网游戏中,每个游戏世界实例都有一个NetMode。WorldInfo类定义了ENetMode枚举和相关的NetMode变量,如下:

varenum ENetMode

{

  NM_Standalone,        // Standalone game.

  NM_DedicatedServer,   // 专用服务器,没有本地客户端

  NM_ListenServer,      // 监听服务器

  NM_Client             // 只有客户端,没有本地服务器

}NetMode;

NetMode属性经常用于在不同游戏实例类型上控制哪些代码。

GameInfo类

GameInfo类实现了游戏规则。服务器(专用的和单玩家的)有一个GameInfo子类,在UnrealScript可以作为WorldInfo.Game访问到。对于Unreal中的每种游戏类型,都有一个特殊的GameInfo子类。例如,一些现有的类是:UTGame, UTDeathmatch, UTTeamGame。

网络游戏中的客户端没有GameInfo,也就是说,客户端上有WorldInfo.Game ==None。客户端不应该期望拥有GameInfo,因为服务器实现了所有的游戏运行规则,而且客户端的大部分代码调用都不知道游戏规则是什么。

GameInfo实现了一系列广泛的功能,如识别的玩家来来去去,为死亡分配credit,确定武器是否应该重生,等等。在这里,我们只会着眼于与网络编程直接相关的GameInfo功能。

InitGame

事件InitGame(string Options, out stringErrorMessage);

当服务器(无论是网络游戏还是单人游戏)首次启动时调用。这给了服务器解析启动URL选项的机会。例如,如果服务器用“Unreal.exe MyLevel.unr?game=unreali.teamgame”启动,Options字符串是"?game=unreali.teamgame"。如果将Error设置为非空字符串,则游戏会出现严重错误。

PreLogin

事件PreLogin(string Options, stringAddress, out string ErrorMessage, out string FailCode);

在网络客户端登录前立即调用。这可能会让服务器拒绝玩家。这是服务器应该验证玩家的口令(如果有的话)的位置,还要加强玩家限制,等等。

Login

事件 PlayerController Login(stringPortal, string Options, out string ErrorMessage);

Login()函数总是在调用PreLogin()不返回错误字符串之后调用。它负责生成玩家,使用Options字符串中的参数。如果成功的话,它应该返回它生成的PlayerController参与者了。Login()PostLogin()也被用来在一个单机游戏创造PlayerController参与者。

如果Login()函数返回None,那就意味着登录失败了,那么他就应该给字符串设置一个错误信息来描述错误。Login()失败应该尽量避免发生,如果你要让一个登录失败,在中PreLogin()失败比Login()高效。

PostLogin

事件 PostLogin(PlayerControllerNewPlayer);

PostLogin()函数在成功登录之后调用,这是复制函数可以被调用的第一个点。

玩家移动和预测

综述

如果一个纯粹的客户端/服务器模型应用于Unreal,玩家的运动会延迟。在300毫秒的ping连接上,当你按下前进键,你不会看到自己移动300毫秒。当你把鼠标向左推,你不会看见自己转了300毫秒,这会非常令人沮丧。

为了消除客户的运动滞后,Unreal使用了类似于QuakeWorld最先提出的预测方案。必须提到的是,玩家预测方案是在UnrealScript全面实现的。这是一个在PlayerController类实现的高层次功能,而不是网络代码的功能:Unreal的运动预测是完全在网络代码的通用复制功能上分层的。

内部运作

你可以清楚地看到Unreal如何通过检查PlayerController脚本进行玩家预测。由于代码有点复杂,在这里简要介绍了它的工作原理。

该方法可以被最好地描述为一个锁步预测/校正算法。客户需要考虑他的输入(操纵杆,鼠标,键盘)和物理力(重力,浮力,区域速度),并作为一个三维加速度矢量描述了其运动。客户端用各种与输入相关的信息和当前时间戳(客户端上WorldInfo.TimeSeconds的当前值)将加速度发送到服务器,这些都在复制ServerMove函数中调用:

服务器函数ServerMove(float TimeStamp, vector InAccel, vector ClientLoc, byteMoveFlags, byte ClientRoll, int View)

然后,客户端在本地调用MoveAutonomous())进行相同的运动,他用SavedMove类将运动存储在记忆运动的链表中。正如你所看到的,如果客户端从未从服务器上听到任何东西,客户端就可以像一个单人游戏一样在零延迟的情况下移动。

当服务器接收到一个ServerMove()函数调用时(通过网络复制),服务器立即执行服务器上的相同的运动。从当前ServerMoveTimeStamp和以往的TimeStamp推导出运动的DeltaTime。这样,服务器与客户端执行相同的基本运动逻辑。但是,服务器可能会看到与客户端稍有不同的情况。例如,如果有一个怪物运行,客户端可能会认为它是在与服务器不同的位置上(因为客户端只是与服务器粗略近似同步)。因此,客户端和服务器可能不同意客户实际移动所造成的ServerMove()调用。无论如何,服务器是权威的,他完全负责确定客户端的位置。一旦服务器处理客户端的ServerMove()调用,它就会调用客户端的ClientAdjustPosition()函数,这个函数是通过网络复制到客户端的:

客户端函数ClientAdjustPosition(float TimeStamp, name newState, EPhysicsnewPhysics, float NewLocX, float NewLocY, float NewLocZ, float NewVelX, floatNewVelY, float NewVelZ, Actor NewBase)

现在,当客户端收到了ClientAdjustPosition() 调用,他必须尊重服务器对于他位置的权威性。所以,客户端通过ClientAdjustPosition()指定的内容设定其确切的位置和速度。然而,服务器在ClientAdjustPosition()中指定的位置在反映了客户在过去的一段时间的实际位置。但是,客户想预测他现在应该在哪里。所以,现在客户端浏览其链表中所有的SavedMove。舍弃所有比ClientAdjustPosition()调用的TimeStamp早的移动所有在TimeStamp之后发生的移动之后都会通过循环和为每个移动调用MoveAutonomous()重新运行。

这样,在任何时间点,客户端总是比服务器告诉他的提前预测,提前时间大约是ping时间的一半。而且,他的本地运动也一点也不延迟。

优点

这种方法是纯粹的预测,它给出了两全其美结果:在所有情况下,服务器仍然完全权威。几乎在任何时候,客户端运动模拟都准确地反映了服务器进行的客户端运动,所以很少纠正客户端的位置。只有在罕见的情况下,如玩家被火箭击中,或撞到敌人时,客户的位置才需要纠正。

移动模式

下面的图表帮助说明服务器和客户端上的移动模式,包括错误调整。

服务器

 

客户端

   

ReplicateMove()

   

代替ProcessMove()被调用。基于对玩家输入进行Pawn物理更新,保存(在PlayerController SavedMoves中)并复制了结果。SavedMove可以作为子类来保存游戏特定运动输入和结果。ReplicateMove() 也试图合并复制的移动来节省上行带宽,提升服务器性能。

ServerMove()

<-

CallServerMove()

根据接收到的输入执行Pawn物理更新,并将结果与客户端发送的结果进行比较。注意运动更新是基于客户端的时钟。如果客户端积累了明显的位置错误,请求更正。否则的话,请求良好动作的ack信息。

 

用客户端时钟的时间戳发送一个或两个当前动作(取决于帧速率和可用带宽)。每次发送两个动作可以节省带宽,但会增加更正的延迟。也可能调用OldServerMove()来重新发送数据包丢失的情况下的最“重要”的动作。

SendClientAdjustment()

->

ClientAckGoodMove()

如果多个ServerMoves()收到了这个“滴答”,客户端响应会延迟到PlayerController的“滴答”结束,避免发送多个响应。如果没有错误,确认是良好动作。

 

更新ping,基于时间戳的往返时间,用较早的时间戳清除savedmoves。

 

服务器

 

客户端

SendClientAdjustment()

->

ClientAdjustPosition()

如果多个ServerMoves()收到了这个“滴答”,客户端响应会延迟到PlayerController的“滴答”结束,避免发送多个响应。如果没有错误,确认是良好动作。

 

用修正时间戳之前的时间戳清除savedmoves。移动Pawn到服务器指定的位置,并设置bUpdatePosition。

   

ClientUpdatePosition()

   

当bUpdatePosition 为真时来自PlayerTick()。回放所有优秀的savedmoves,让Pawn回到了当前客户端时间。

玩家状态同步

PlayerController代码认为客户端和服务器总是尝试运行完全相同的状态; ClientAdjustPosition()包含了状态,这样一来,如果进入了不同状态,就可以更新客户端。在这种情况下:服务器需要改变状态但是客户端不能自己模拟该状态时,ClientGotoState()用于强制客户端立即进入那个状态。不支持处理/同步UnrealScript的状态栈功能(PushState() / PopState()),而且我们不建议将其用于PlayerController。

玩家动画(客户端)

如果动画与游戏运行无关,它就不需要在服务器上执行。SkeletalMeshComponent的bUpdateSkelWhenNotRenderedIgnoreControllersWhenNotRendered属性可以控制这些,也不需要在带有SkelControlBase::bIgnoreWhenNotRendered的每个骨骼控制器基础上运行。客户端动画是受Pawn状态检查驱动的(物理,Pawn属性)。

对于动画驱动的运动,根骨运动被转换成加速度/速度,这就是被复制。因此,动画仍然锁定到位(相对于参与者),但根骨运动被转移到加速/速度移动参与者。

这对于服务器/客户端来讲不比非根运动移动沉重。

死尸

如果bTearoff为真,那么这个参与者就不再被复制到新的客户端,而且还会从已经复制了它的客户端上移除(变成ROLE_Authority)。TornOff()事件是在收到bTearOff时调用。默认的实现会在死亡的Pawn上调用PlayDying()

武器射击

武器射击与玩家移动的模式相似:

  • 玩家一旦输入请求射击,客户端立即产生射击效果(声音、动画、muzzleflash),调用ServerStartFire()ServerStopFire()请求服务器射击。
    • 客户有足够的状态信息(弹药数、武器定时状态等)来正确预测武器是否可以发射,除了在极少数情况下,客户端和服务器版本的相关属性不同步。
  • 服务器会生成射弹/破坏武器(生成射弹),射弹复制到客户端。

射弹

这个例子对于简单的、可预测的射弹是有用的:

  • bNetTemporary被设为真。
    • 初始复制后,参与者通道关闭,参与者不再更新。参与者将被客户端销毁。
    • 节省带宽和服务器属性复制测试。
  • bReplicateInstigator被设为真。
    • 所以射弹可以与煽动者正确交互。
  • 客户端特效生成
    • 注意客户端上生成的参与者在此客户端上有ROLE_Authority,服务器和其他客户端上没有。
    • 这些特效不需要在服务器上生成,也不需要复制。

缺点:如果目标和/或射弹的客户端模拟结束了,可能误击中或错过目标。不要为此使用单射杀型射弹。

武器附件

避免让相互关联的几组参与者都复制,是考虑到性能和尽量减少同步问题。

Unreal Tournament中,武器只复制给拥有它的客户。武器附件不复制,但通过一些复制的Pawn属性在客户端生成和控制。Pawn复制FlashCountFiringMode,UT Pawn复制CurrentWeaponAttachmentClass。Pawn 中的ViewPitch属性是该模式的另一个应用案例。

声音

函数ClientHearSound() 在每一个听得见声音的PlayerController 调用。函数ClientCreateAudioComponent() 在负责声音的参与者上调用。如果参与者不在客户端上,声音会在复制的地方播放,其中音频组件由WorldInfo创建。PlayerController中的函数ClientPlaySound() 在客户端播放非定位声音。

试着尽可能在客户端模拟声音!

物理

复制

物理模拟在客户端和服务器上运行。更新从服务器发送到客户端。下面的结构体是用来描述一个刚体的物理状态,并且被复制(如在Actor中所定义的):

structRigidBodyState

{

  var vector Position;

  var Quat Quaternion;

  var vector LinVel; // RBSTATE_LINVELSCALEtimes actual (precision reasons)

  var vector AngVel; // RBSTATE_ANGVELSCALEtimes actual (precision reasons)

  var int bNewData;

};

使用了结构体,所以所有属性都会同时改变。向量被压缩为整数分辨率,以便在发送之前缩放它们。Quats被压缩到只发送三个值;第四个值是从其他三个推出来的。

对于物理复制,有两种类型的校正:

  • 小校正和物体移动:20%位置调整,80%额外的相对于目标的速度。
  • 大校正或对象停止:100%位置调整。

模拟

下面的场景描述了物理模拟:

  • ROLE_SimulatedProxy 参与者模拟
    • 客户端根据接收到的位置和速度不断更新模拟的参与者位置。
    • 如果bUpdateSimulatedPosition 为真, 权威的位置更新不断从服务器发送到客户端(否则,在参与者初始复制之后不会发送位置更新)。
  • 其他客户端上的Pawns
    • 不像其他的参与者,模拟Pawn不在客户端执行正常的物理功能。这意味着物理事件,如Landed()事件,都不会在非拥有的客户端上为Pawn所调用。
    • Pawn的物理方式是从它的位置和bSimulateGravity标志推断,其位置预测是基于复制的速度更新。
      • 设置了bSimGravityDisabled标志,如果Pawn在复制的位置不合适,并在客户端有整个世界失败的风险,那就暂时关闭重力模拟。
  • PHYS_RigidBody 参与者(车辆,K参与者等)
    • 客户端和服务器都模拟对象,但服务器定期向客户端发送权威更新(当对象处于唤醒状态时)。然后客户端移动对象以匹配服务器版本。
      • 如果错误是低于一个可接受的阈值,通过改变速度带引出位置上的收敛速度,而不是捕捉的位置,试试这样做,很容易。
    • 当所有属性都必须同步接收时,为原子复制使用RigidBodyState结构体。

对于Radoll物理,只复制髋关节位置。它往往可能完全排除,而不是复制所有。

对于Vehicle(PHYS_RigidBody参与者),有下述网络流:

  1. 在客户端按键
  2. 向服务器发送输入(油门、转向、上升)——复制ServerDrive调用的函数
  3. 生成输出(OutputBrake, OutputGas等);打包进复制结构体,这些结构体可以发送到客户端——服务器调用ProcessCarInput()  
  4. 在服务器和客户端上更新车辆;用输出(OutputBrake, OutputGas等)将力/力矩应用到车轮/车轮——在服务器和客户端上调用UpdateVehicle()

性能小贴士


优化目标

这里的目标是在给定带宽限制下,最大化可见重要细节的数量。在运行时确定的带宽限制,为多玩家游戏中的参与者编写脚本的目的是保持最低限度的带宽使用。我们在脚本中使用的技术包括:

尽可能使用ROLE_SimulatedProxy和模拟运动。例如,几乎所有的虚幻的炮弹都使用ROLE_SimulatedProxy。Razorjack alt-fire叶片是一个例外,玩家可以在游戏中控制它,因此服务器必须不断向客户端更新位置。

对于快速特效,只在客户端生成特效参与者。例如,我们的许多炮弹使用模拟HitWall()函数在客户端生成特效。由于这些特殊效果只是装饰,而不会影响游戏,所以全在客户端完成这些事没有弊端。

当带有Repnotify关键字的属性被复制时, ReplicatedEvent()事件会被调用,修改属性的名称作为参数。查看复制模式部分,以了解如何节省网络带宽。

微调每个类默认的NetPriority。炮弹和玩家需要有高度的优先级,纯粹的装饰效果可以有较低的优先级。Unreal提供的默认值作为第一遍的猜测是不错的,但你总能通过微调这些值获得一些改进。

当一个参与者第一次复制到客户端时,所有变量都初始化为类默认值。随后,只有与最近的已知值不同的变量被复制。因此,您应该设计您的类,以便尽可能多的变量自动设置为它们的类默认值。例如,如果一个参与者总要有一个值为123的LightBrightness,有两种方法可以做到:(1)将LightBrightness的类默认值设为123,或(2)在参与者的BeginPlay()函数中,将LightBrightness初始化为123。第一种方法是更有效的,因为LightBrightness值不需要被复制。在第二种方法中,每当参与者第一次与客户端相关时,都需要复制LightBrightness

也要注意以下情况:

  • 如果参与者引用不可序列化,不清除bNetInitialbNetDirty(因为它与客户端不相关)。这意味着服务器将继续尝试复制属性,花费CPU周期成本。

欺骗检测及预防

Unreal Tournament中,我们遇到了以下几种与网络有关的欺骗:

    • 加速
      • 利用我们使用客户端的时钟进行运动更新的事实。
      • 内置检测:通过验证客户端和服务器时钟不以不同的速率移动。
      • 误报与大量的数据包丢失
    • Aimbots——UnrealScript和外部版本
    • 墙黑客和雷达——UnrealScript和外部版本

流量监控

 

【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权;

2018-05-10 10:39:45 heneyT 阅读数 1664

Unreal Open Day 2017 活动上 ,Epic Games 资深开发者支持工程师王祢先生为到场的开发者介绍了在 Unreal Engine 4 中动画系统,以下是演讲实录。 

大家好!鉴于引擎移动端功能以及 UI 优化都有同事做了介绍,今天我选择讲的主题是关于动画。动画是一个非常复杂的系统,我会主要介绍一些基本的概念,大家在了解了基本概念后,就可以在上面做出扩展。我并不会教大家怎么使用动画工具,关于一些动画节点的使用,我们的在线文档上都有比较详细的说明,也有比较多的资源。今天不会讲到的内容包括 Morph target,IK,Retargeting,Rootmotion,Additive,Skeletal Control 这些。

首先,我们先来看看引擎中的动画系统是如何工作的。为什么我要先讲解这样一个问题,因为国内有很多用户在使用动画系统的时候,有很多疑问。这些疑问并不是因为他们没有查阅文档,而是因为没有理解系统的工作方式。本质上,动画系统工作原理是非常简单的,我这里还是重新介绍一下。


我们先来看看在引擎中动画相关的资源主要分为哪几类。

第一大类是最基本的数据资源。其中主要来自于外部 DCC 工具制作并导入的原始资源,我们称之为 Anim sequence。

然后,有些资源可能为了制作和导入的方便是分散开来的,但是有些情况下会组合到一起使用。所以引擎中有一种资源叫 Anim Composite。他是使用多个 Anim sequence 或是自身(Anim Composite)所组合成的资源。在使用时,依然被看作是普通的 Anim Sequence。

第三种数据资源类型叫 Blendspace。他可以是一维的也可以是二维的。二维的情况下,在两个轴上,通过变量控制对任意在二维平面上指定的动画序列(Anim sequence)作混合。对于任意的二维输入,总能找到这个输入值在二维图像附近最接近的四个动画序列按照权重来混合。严格来说,Blendspace 并不是单纯的基础数据,他也受其它输入参数的影响来混合 Pose 。但是,由于在动画混合蓝图中是作为 Pose 的输入结点,我们这里依然把它作为数据类资源。

第四种数据资源叫 Montage。这一类资源一般是直接受逻辑控制的组合资源。


在数据资源的基础上,我们还可以绑定一些额外的数据。

第一类常用的数据类型叫 Notify。引擎包括一些内建的 Notify类型。譬如,在走路的时候希望脚步踩到地面的那一刻,触发踩地面的事件,用来向地面投射贴花,用于产生脚印,以及播放脚步音效或扬起尘土的特效之类。这里的 Notify 你还可以扩展成你自定义的事件类型,可以在蓝图以及代码中去处理事件对应的逻辑。举个例子:如果做一个动作或格斗类游戏,在出招的时候,判定并不是从这个动画开始播放的时刻就已经有了的,可能是从出招动画到某一时刻开始,才有打击判定。那么我们就可以通过 Notify 来用事件通知游戏逻辑在特定的时候去打开和关闭判定。

第二类叫 Curve。Curve 就是伴随动画序列的时间轴所绑定的曲线数据,后面会有一些举例。再然后你也可以绑定一些你自定义的数据类型。


讲完刚刚这些数据类型,接下来就是最重要的处理动画混合逻辑的资源,叫 Anim instance。Anim sequence 的设计是基于对于 3A 级游戏中复杂的动画需求所产生。这里有一个假设,那就是动画状态在复杂的情况下一定是需要对骨骼结构有认知的。所以引擎中的 Anim instance 和骨骼是强耦合的关系。譬如你需要知道腰部的骨骼位置来区别开上半身和下半身的动画,这样的设计可以完成相当复杂的动画混合,但是却也带来了一些限制。如果我的整个动画状态只需要简单的一个状态机在不同的状态中,譬如闲置、追逐、攻击、受击、死亡,在每中状态中,并不作复杂的混合,而只是播放一个简单的 Anim instance。在整个逻辑中完全不需要用到骨骼信息。那么照理来说,即使拥有不同骨骼结构的对象,如果只需要这个简单逻辑的话都可以共享这套逻辑。然而由于我刚刚所说的 Anim instance 和骨骼的强耦合设计导致在现在的引擎框架下,这样的功能暂时无法完成。我们在内部也在作一些讨论,以后可能会有支持纯逻辑的 Anim instance 功能,而目前来看,如果大家有这样的需求,我建议在可能的情况下把这些对象的骨骼层次结构尽可能保持一致,这并不是说多个对象的骷髅要完全一致,而只是骨骼树的层次结构一致就可以了。譬如你的基础骨骼是个人形,有些怪物会多出尾巴或翅膀,这些多出的骨骼并不破坏原先的树状结构,而只是多出来的分支。所以还是可以利用 Retargeting 来共享 Anim instance 的逻辑。


Anim instance 中,最明显的两块分别是 EventGraph 和 AnimGraph。其中 EventGraph 就类似于普通的蓝图,用来在 tick 的时候处理一些逻辑状态的更新以及播放 Montage。当然这些逻辑也可以在 C++里面做。AnimGraph 是用来混合和输出 Pose 的地方。说到混合,我们可以把每一帧中整个混合的过程看成是一棵树,从叶子结点输出的 Pose 经过枝干结点的混合计算输出到根结点的最终 Pose。我们刚刚说到的数据类的资源,就是这里所谓的叶子结点。这些结点本身不需要其它的 Pose 作为输入,而直接提供了 Pose 的输出。而枝干结点则是进行混合的结点,当然真的说混合也不是很准确,有些枝干结点只需要输入一个 Pose,在自己的结点逻辑中,对这个 Pose 作一些修正,并不进行混合。我们把这些枝干结点计算调整和混合Pose 的行为称作评估(evaluate)。举个最简单的枝干结点的例子,那就是多结点混合。譬如,输入的有两个 Pose ,一个权重是 0.8,另一个是 0.2,相当于是把第一个 Pose 的 BoneMap 的 transform 乘以 0.8,第二个乘以 0.2,再相加输出。这里我列了一个树状图,来表示动画混合的过程。但是因为这是个非常简化了的例子,所以其中不包括直接对骨骼进行控制或者直接 Override 一个 Fullbody slot 来强制更新整个 BoneMap 之类的行为。并且一般来说,一个正常的 anim graph 的一帧的混合也不会像这张简化图这样是棵红黑树。首先,就像我刚刚说的,你并不能保证他是二叉的,譬如刚刚说的多混合结点完全可以由三个或以上结点来混合,以及我刚刚说的有些枝干结点,只有一个输入。再者,大部分情况下他也不会是平衡的。在混合状态复杂的情况下,我们一般会分层次来混合,这就导致了这棵混合树会往一个分支方向衍生出去。



好了,那么刚刚看到的是单帧的 Pose 混合计算情况。当持续到多帧以后,情况又会稍微复杂一些。譬如说两个 Pose 混合起来,他们的长度很有可能不一样。举例,我有一个走路的动画,他可能长达 2 秒,同时我又有一个跑步的动画,他长达 1 秒。如果我直接混合,就会出现很怪异的情况,譬如走路还在迈左腿的时候,跑步已经迈右腿了,混合起来的姿势就会非常奇怪。基于这种情况,我们引入了 Sync Groups 的概念,当我们设置这两个动画序列在同一个 Sync Groups 下进行混合时,引擎会把当前混合时权重较高的作为领导,把剩下的序列缩放到和领导序列一样长的情况,再按比例去做混合。这样就能解决动画长度不一致的混合问题。



再来看多帧动画状态下,如果状态复杂,动画树上的某些分支在不同的帧内是完全不同的状态。为了简化树的逻辑,动画混合系统中可以使用状态机来隔离每一帧的状态。我这里的图例举了一个比较简单的 Locomotion 的状态机。

关于动画混合的这棵树,在复杂的情况下,我们还会把他做分层。也就是把一棵混合完的树的根结点缓存下来,作为另一棵树的叶子结点。当然你也可以把整个复杂的树连到一起,分层只是为了便于维护和调整。这个图片是我们的 MOBA 游戏《虚幻争霸》中一个角色分层混合的模版示例。


讲完了动画的基础概念后,我们来看一些例子加深理解。

子树类用例。在引擎中有一类功能叫 Sub anim instance。这就类似于刚刚说到分层里面的一棵子树,这个子树可以拥有一个输入结点,并且输出一个 Pose 。典型的应用方式,是把在同一个逻辑下有多种可替换的子逻辑分离开,做到不同的 Sub anim instance 中。这样可以把剩余的逻辑用来共享。通过替换不同的 Sub anim instance 来组合出最终不同的效果。



接下来讲一些叶子类的用例。通常的叶子类结点就是我们刚刚说的数据类结点,我这里举两个比较特殊的例子。在 4.17 版本中,我们会加入一个叫 live link 的结点。它通过引擎的消息总线从外部实时读入数据输出Pose 。这里的输入源可以是各种 DCC 工具,也可以是动作捕捉或手势识别类设备。在我们放出的第一个版本中,会带有一个 maya 的实现,通过 maya 的插件把在 maya 中当前动画的 BoneMap 数据通过 live link 消息总线和引擎进行通信。引擎把接收到的数据转换成引擎内的数据输出当前的 Pose 。这样就可以做到在 maya 中一边播动画一边在引擎中看到效果了。



下一个叶子类结点的举例,叫 Pose Snapshot。Pose Snapshot 就是把任意指定帧的 BoneMap 记录下来,在接下来的任意时刻,用来作为数据源输入和其它 Pose 做混合。譬如在 Robo Recall 中,你打倒了机器人,机器人会进入物理状态而倒地。你可以把这个状态存下来,在之后再和站起来的动画作混合。


刚刚举了两个叶子类结点的例子,我们再来看看动画混合中最大的一类——枝干类结点的例子。大部分情况都是多个 Pose 按权重进行混合,当然也可以是按照 bool、int、enum 值进行混合。我这里依然举一些特殊的例子。


第一个例子是 RigidBody 结点。在讲这个结点前,我要先介绍一个伴随而来的概念,叫 immediate mode physics。引擎中以前的 Physics 是所有的 RigidBody 都加到同一个 PhysX scene,这种情况下如果每个角色身上都有多个需要计算物理的 RigidBody,场景中又有大量的这样的角色,计算量就相当的大。但是大部分时候角色互相之间的物理碰撞细节大家并不关心,所以这样的效率比较低。

因此我们和 Nvidia 进行了合作,他们对 PhysX 的 Api 进行了调整。在新版本中放出了更底层的 Api 可以让我们在引擎中做更细致的控制。大家可以看到这个新的 immediate physics,一个角色身上所有的 RigidBody 都只注册在当前这个 skeletal mesh component 下,多个 SMC(skeletal mesh component 缩写)之间并不会有交互,这样很大程度上提高了运行的效率。

大家可以看到,这里的视频同屏有几百个小兵站在地上做闲置的动画,在受到物理冲击后转入到物理状态。这么大量的物理对象在我的笔记本上依然能稳定在 60 帧,而右边的图也显示了单个较为复杂的角色在模拟物理时候的开销,只使用了 0.24ms。大家可能觉得这是一个纯粹物理的功能,为什么我放到动画的枝干结点的例子里来讲呢? 因为事实上你可以在动画中把动画计算完的 Pose 输入进去,在这个结点中根据当前动画的Pose 和前一帧计算完的结果计算出骨骼结点的变化,从而模拟出物理受力的变化,并根据输入的权重混合回你的 Pose 。有了这样的功能,做我之前说的 Robo Recall 中很自然的击倒机器人或者拳击类的游戏、以及用枪射击怪物时怪物比较自然的受击都变得相当简单。



好了,下面我们再来看另一个枝干结点的例子。我们称之为 Speed Warping。传统的游戏中如果你调整了移动速度,那么为了不产生滑步你也需要调整跑步的动画播放的速率。譬如你的速度翻了一倍,那么很多时候你就需要把动画也加快一倍播放,大家可以看到在这里的视频右边加快播放后的动画其实是很别扭的。真实情况下我们提高速度除了迈出的脚步速度会有一些变快以外,更多的情况下,其实是调快了步幅。同样的减慢速度也是这样。所以 Speed Warping 就是做了这么一个效果。那我们是怎么计算的呢?



简单来讲,原始的动画双脚的位置是这里的红球。我们计算他跟腰部垂线的水平距离并根据加减速的倍率横向扩展。譬如当是 2 倍的时候,调整到绿球的位置。但这个时候两只脚的距离被拉的太长了,因此我们适当的往下调整了屁股的位置,并且将两只脚以刚才绿球所在位置到屁股的连线上挪动一段距离使得脚步的长度保持不变,所以最终计算出来的就是蓝球的位置。



我再举一些其它的例子。比如引擎中当你对 AnimBP 进行继承的时候,所创建出来的内容叫 Child AnimBP ——它所做的事情是让你重载所有的叶子类结点。举个实用的例子:譬如我有一种敌人,他永远是从初始的出现状态到发现玩家到向玩家攻击这样转化,而这样的怪物在地图上不同的场景下有不同的出现动画,有可能是从地上爬出来的,有可能是从墙上跳出来的。对于这个怪物来说,他的动画切换状态都是一样的,所不同的只是初始状态所需要使用的资源,所以只需要替换初始的动画(某个叶子结点)就可以了。



再举一个例子,有不少人问过,在《虚幻争霸》中,是怎么做到让角色不滑步的。传统的主机游戏中,为了让脚不滑步很多时候我们都是使用 root motion 来做移动的动画。但是因为《虚幻争霸》是个 MOBA 游戏,策划会希望能够用数据来驱动移动的速度。譬如在有不同的 buff 或者装备的情况下,角色的速度也会发生变化,这用 root motion 就很不好处理。所以我们做了一个叫 Distance Curve 的功能,这也是我刚刚说到的 Curve 数据的一种运用方式。我们可以把 Distance Curve 的方式看成是反向的 root motion。它通过给所有的启动、旋转、站定动画都加入曲线数据,曲线上的数值表示当前这帧动画到达站定点的位置的距离,其中站定点(Marker)是很容易预测的。


当玩家的输入发生变化,引擎的计算在那一刻就能完成,可预测出最终速度衰减后站定的位置。通过查询曲线中动画到站定的距离可以直接从对应距离的那一帧动画开始混合。当然这些计算都有一些前提,首先,曲线中的数值在靠近站定点的动画中取负值,而远离取正值。这种时候 Piviting 行为也就是你在往左走的时候突然往右,这条曲线是从负值到正值的,这样这条曲线就满足了无论什么情况下都单调递增并且除了0其它的值都不会重复,这就方便我们在O(n)复杂度下找到对应的动画帧数。



举完了这些例子以后我们来看看动画的优化。优化是个很大的话题,有很多方面。有些是可以在设计上规避掉的,有些是则是在内容上做了优化。虽然今天我不对这些做举例,但其实引擎也有工具可以直接在骨骼结构上右键设置在某一级 LOD 以下不更新这些骨骼,这也算是内容上的优化。那么接下来我主要讲在不希望太大的妥协效果的情况下,两大类优化的手段:

其中一类就是降低人们低感知部分的采样频率。譬如空间上的 LOD 或者时间上的更新频率 URO,基本思路就是离的远的、占据视频面积小的、或者甚至是看不见的,降低更新的细节层次以及降低更新的频率。另外一类是尽可能提高利用硬件的计算能力,尽可能降低不同动画任务的依赖性来提高并行计算。



在引擎中,SkeletalMeshComponent 中有一个 Update Flag 选项,默认是 Always Tick Pose。这意味着当 SMC 不被渲染到的时候,动画逻辑还是会 Tick,并且 AnimGraph 里的节点虽然不会计算 BoneMap 也就是没有实际的 Evaluate 计算,不过还是会计算对应节点的 Update,也就是计算这些节点的输入权重之类的数值。这使得在动画对象重新进入视野中进行绘制时,可以很自然的直接更新到最新状态下的姿态。所以大部分时候,不是不得已,都不需要使用 Always Tick Pose and Referesh Bones。而如果你对于一些不太重要的动画对象,甚至不关心他们不被渲染的时候 Pose 逻辑需不需要更新的情况下,可以进一步的选择 Only Tick Pose when Rendered 来进一步减小 CPU 的开销。



另一个比较重要的设置是 AnimGraph 的各种枝干接点上的 LOD Threshold 选项,大部分这类需要进行 Evaluate Pose 计算的接点上,都会有这个选项,默认数值是 -1,也就是不会起效,如果设定了正整数值的话,就相当于在对应的 LOD 情况下,这个节点以及往下的子树就都不会评估了。对于同屏有大量骨骼动画对象的情况下,仔细调整和设置 AnimGraph 中各个节点的 LOD Threshold 能很有效的降低动画 CPU 的开销。



再有一个是刚才说道的 RigidBody 接点的 LOD 优化,引擎在创建 RigidBody 加到当前 SCM 中的时候,已经根据所有的 LOD 从最下级到最上级进行了排序,这样一来,切换 LOD 后,自然而然的只要取列表的前几项做计算就可以了。



再来我们说一下 URO,也就是更新频率的优化。例如,可以根据离 Camera 的距离,调整 Tick 的频率。我这里给了个开启 URO 的例子,甚至我们可以根据不同的 LOD 设置不同的频率,引擎中也有 LODMapForURO 的设置。



那么我们再来看看怎么提高并行。由于 BP 是在虚拟机上执行的,所以都是在游戏线程进行的,无法进行并发,所以如果有大量的动画对象希望提高并行的话,建议大家不要使用 AnimInstance 的 EventGraph 更新逻辑,而是写到 C++ 中,在自己的 AnimInstance 类中指定自己的 Proxy 继承类,并写到 Proxy 的 UpdateAnimation 中,这样引擎就能把动画的 Update 以及 Evaluate 都放到 Proxy 上通过其他工作线程并行执行。



大家可能会注意到,在 ACharacter::PostInitializeComponents() 中,对我们的 MeshComponent 的 PrimaryComponentTick 加了一个前提条件,也就是角色的 CharacterMovement 这个 component 的 tick。因为引擎希望当前这帧的动画更新的信息是基于移动后的位置进行的。如果大家不需要这样准确的依赖,还可以在自己的角色继承类中,重新去除依赖,来使得动画的计算能更早的利用工作线程并行计算。



再有一点,基于 UE4 的网络模型,服务器端在默认情况下也会有不小的动画计算开销,我们其实可以在大部分时候做一些优化,譬如关闭服务器的物理状态计算。如果不需要很精确的在服务器端计算角色的动画变化,可以保证服务端的计算不依赖于骨骼位置,那么可以在服务端完全不评估整个动画(仅使用 Capsule 作角色位置的验证或计算)。如果还能保证所有的动画中触发的事件不会影响到 Gameplay 而只会影响表现,那么还可以关闭整个动画的更新和 tick。一般来说 Montage 是游戏逻辑直接控制的动画状态,那么我们可以把在这些动画中影响游戏逻辑的事件全都加在 Montage 上,所以只有在播放 Montage 的时候才需要 tick。最后,即使进行 tick 也依然可以使用之前说到的 URO 以比较低的频率来 tick。




这里是初始化的时候设置当进行播放和停止 Montage 的时候进行回调的例子:通过判断 AnimInstance->IsAnyMontagePlaying() 来决定是不是要允许 tick,这里的事例实现了根据当前是否在播放 Montage 在服务端自动调整是不是要 tick。因为服务器端从来不需要渲染,所以当客户端设置成了 OnlyTickPoseWhenRendered 的时候,服务端就可以完全不需要 tick。

以上就是今天要讲的所有内容,谢谢大家。


本文章转自http://gad.qq.com/article/detail/27926#

2018-12-15 15:33:00 weixin_30825199 阅读数 46

http://blog.csdn.net/garuda/article/details/7650845

 

 

看了unreal engine4的ElementalTech DemoFeaturesand Development Walkthrough两个视频,效果还是非常赞的。看来epic这几年是下了大力气了。

下面说说ue4的重要特性吧

1.     延迟渲染

Ue4完全抛弃了预计算技术,取而代之的是全实时的延迟渲染。这个应该是ue4在渲染上最大的改变。随之而来的将会是ue4比ue3对硬件的要求会有大幅的提高。(硬件厂商又高兴了)

2.    实时GI。

当年CryTech的LPV一出来立刻就成为了业内的热门话题。这回epic终于也搞了个real-time GI出来,而且更进了一步。这个所谓的SVOGI – Sparse Voxel Octree Global Illumination相比LPV最明显的优势是精度更高,能够实现high-frequency的间接光照。关于这项技术更详细的信息可以参考nvidia的paper

地面的高光反射出塑像的颜色

Ue4间接光照

3.    软粒子的光照

软粒子接受直接和间接光照

软粒子投射阴影

4.    GPU粒子

GPU粒子虽然不是特别特别新鲜的技术,但是集成到游戏引擎中我还是第一次见到。特别是ue4的实现中,通过向量场与GPU粒子交互的效果还是蛮酷的。

Ue4 GPU粒子

5.    高度可破坏的场景

这是APEX的主打技术。

可破坏的场景

6.    全新的Kismet

据说unreal engine4完全移除了动态脚本(出于效率的原因?),取而代之的是纯C++脚本系统,并且完全是实时编译,热更新。

Ue4新的可视化编程系统

实时编译和热更新

 

总之,unreal engine4还是让人眼前一亮的。期待尽快能有用ue4开发的游戏上市,到时候我就换块GTX680

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!希望你也加入到我们人工智能的队伍中来!http://www.captainbed.net

转载于:https://www.cnblogs.com/skiwnchiwns/p/10123518.html

2018-01-23 13:21:59 ZJU_fish1996 阅读数 2577

 之前的版本直接右键都有这个选项的,新版Unreal没有了,原因是这个选项被默认关闭了。

可以在菜单栏->编辑->Plugins->Physic中勾选可破坏物体的插件。再右键就出现该选项了。



参考:https://forums.unrealengine.com/development-discussion/content-creation/1389700-solved-create-destructible-mesh-is-missing-from-right-click-menu-4-18-1

2015-12-25 13:33:00 weixin_30514745 阅读数 25

Unreal Engine 4的物理引擎用的是PhysX。

支持网格破坏、布料。物理粒子等,非常强大。曾经须要编码才干完毕的工作,在Unreal Engine 4 中仅仅须要拖拖拽拽就完毕了,非常方便。

这次演示做一个可破坏的网格。先上效果:


我做了个圆锥。让它从天落下,然后破碎掉。

以下说下详细制作的步骤:

  1. 新建一个项目,向场景加入一个Cone Brush(圆锥)。
  2. 调整Cone Brush的属性,设置Sides(边数)为20。这样看起来平滑一些。在属性栏Brush Settings的最以下点击buttonCreate Static Mesh(创建静态网格),然后给网格命名,就得到了一个Cone的网格模型。能够在Content Browser(资源浏览器)中看到它了。
  3. 在Content Browser(资源浏览器)中右键这个模型,选择菜单Create Destructible Mesh(创建可破坏网格),得到可破坏网格。
  4. 调整可破坏网格的属性
  5. 向场景加入此Destructible Mesh。调整此Actor的属性。勾选Physics栏以下的Simulate Physics,勾选Collision栏以下的Simulation Generates Hit Event。这两个參数设定它能够下落。而且生成碰撞事件,否则不会破裂的。
  6. 把Actor拉高一点,点击Simulate,能够看见效果了。

转载于:https://www.cnblogs.com/bhlsheji/p/5075672.html