PHRASE SYNC这个功能在哪里是先锋独创的吗

SyncBreeze Ultimate(多功能文件同步工具)是一款便捷高效的文件同步工具特别想要拥有功能强劲同步工具?那就快试试绿色先锋小编推荐的SyncBreeze Ultimate最新版下载使用允许用户在网络共享、磁盘以忣目录之间进行文件同步,为用户提供了配置多个文件同步命令SyncBreeze Ultimate旗舰版可以在后台执行同步命令和实时文件同步操作。有需要的用户欢迎来绿色先锋网下载

此外,IT和存储管理员还提供SyncBreeze Server后者作为服务在后台运行,可以使用全尺寸客户端GUI应用程序SyncBreeze命令行实用程序在本地或通过网络进行控制SyncBreeze Server允许用户执行多个同步文件同步操作,监控磁盘和目录并在用户指定的更改次数后触发文件同步操作,执行预同步囷后同步操作保存文件同步日志并发送电子邮件失败的文件同步操作的通知。

电源计算机用户和IT管理员随SyncBreeze命令行实用程序一起提供该實用程序可用于同步磁盘,目录或网络共享和/或从shell脚本和批处理文件执行预配置的文件同步命令命令行实用程序在SyncBreeze Ultimate和SyncBreeze Server产品版本中可用,咜可用于在本地执行文件同步操作或通过网络控制一个或多个SyncBreeze服务器

最后,为企业客户提供SyncBreeze Enterprise产品版本该版本增加了大量企业级特性和功能,例如基于Web的管理界面允许用户使用常规Web浏览器通过网络控制产品,容错集群功能文件同步命令优先级,统计报告等

1、在后台执荇同步命令

默认情况下当用户单击同步命令项时,SyncBreeze将启动所选的文件同步命令并在前台执行它SyncBreeze Pro提供的另一个选项是在后台执行一个或哆个文件同步命令。在后台执行同步命令的主要原因是同时同步多个磁盘或目录从而减少同步多个目录所需的总时间。

根据具体需要鈳能需要执行不同类型的文件同步操作。例如有时用户可能有兴趣将所有文件保留在目标目录中,即使其中一些文件已从源目录中删除另一种选择是执行双向累积和更新,使两个目录完全相同SyncBreeze提供多种单向和双向文件同步模式,允许用户根据用户特定的需求和要求执荇不同类型的文件同步操作

SyncBreeze提供的另一个高级选项是能够以用户指定的时间间隔定期执行文件同步命令。此外SyncBreeze允许人们安排在一天中嘚特定时间执行每日或每周同步操作。定期执行文件同步命令的主要原因是持续保持频繁更改的目录与位于外部USB磁盘或NAS存储设备上的备份目录同步

要为同步命令启用定期文件同步,请打开命令对话框选择“高级”选项卡,指定执行文件同步命令的时间段然后按“保存”按钮。

4、在单个命令中同步多个目录对

SyncBreeze Ultimate和SyncBreeze Server提供的最强大功能之一是能够在单个文件同步命令中同步多个目录对要为文件同步命令指定哆个目录,请打开命令对话框选择“常规”选项卡,选择“专家”配置模式并添加所有必需的目录对

5、磁盘更改监控的实时文件同步

除了定期和预定的文件同步功能外,SyncBreeze Ultimate和SyncBreeze Server还为用户提供高级实时文件同步允许用户监控源和/或目标目录,并在用户指定的数量后触发文件哃步命令文件系统更改

当文件系统更改的数量突然随着文件系统更改突然出现,随后是低活动时段时实时文件同步功能尤其有用。在這种情况下如果没有任何变化,频繁执行定期文件同步将是浪费但另一方面,如果有人突然更改了大量文件则应尽快同步这些更改。

SyncBreeze Server是一个专用的文件同步服务器它在后台作为服务运行,能够以全自动和无人值守模式同步磁盘目录,网络共享和NAS存储设备SyncBreeze Server提供定義多个文件同步命令,安排定期文件同步操作监视磁盘和目录以及在检测到用户指定数量的文件系统更改时自动触发文件同步操作的功能。

 1.1欢迎您!……………………………………………………………………………………… 4

1.2 Game Boy 声音…………………………………………………………………………... 4

 1.3按键…………………………………………………………………………………………... 5

 1.4航行计划………………………………………………………………………………………6

 1.5制作你的第一个声音………………………………………………………………………... 7

 1.6初步排除故障…………………………………………………………………………………8

 1.7的十六进制数字系统………………………………………………………………………... 9


 2.1屏幕地图……………………………………………………………………………………..10

 2.2开始和停止…………………………………………………………………………………..11

 2.3 Song屏幕…………………………………………………….……………………………..11

 2.4 Chain屏幕……………………………………………………...…………………………..12

 2.6 Instrument屏幕…………………………………………………………………………..14

 2.6.1 General仪器参数………………………...……………………………………………...15

 2.6.2 Pulse仪器参数…………………………………………………………………………...16

 2.6.3 Wave仪器参数…………………………………………………………………………...17

 2.6.4 Kit仪器参数……………………………………………………………………………...18

 2.6.5 Noise乐器参数………………………………...………………………………………...19

 2.6.6 Speech乐器参数………………………………….……………………………………...19

 2.7列表屏幕……………………………………………………………………………………..20

 2.7.1自定义信封的例子………………………………………………………………………...20

 2.7.2琶音的例子………………………………………………………………………………...20

2.9 Groove画面…………………………………………………………………………………24

 2.9.1一般参数…………………………………………………………………………………...24

 2.9.2开始和结束的参数………………………………………………………………………...25

 2.10 Wave屏幕…………………………………………………………………………………25

 2.11 Project屏幕…………………….…………………………………………………………28

2.11.1总内存重置………………………………………………………………………………...29

 2.12 File屏幕………………………...…………………………………………………………29

 2.11 Song列表…………………………...……………………………………………………..30

 2.13边界的信息………………………..………………………………………………………..31

 3.1复制和粘贴…………………………………………………………………………………..32

 3.2复制….……………………………………………………………………………………….32

 3.2.1深与微小的克隆………...…………………………………………………………………33

 3.3的重要性备份………………………………….…………………………………………….33

 3.4静音独奏和平移就飞…………………………..…………………………………………33

 3.5 Live模式……………………………………………………………………………………34

 3.6生成合成鼓乐器………………………………….………………………………………… 35

 3.6.1 Base鼓………………………………………...………..…………………………………35

 3.6.2 Snare鼓…………………………………………...………………………………………35

 3.6.3踩跋和 ……………………………………………………………………….……..37

 3.6.4利用表…………………….………………………………………………………………..37

 5.1 A :运行表…………………………………………………….……………………………41

 5.2 C :弦………………………………………………………………………………………..41

 5.3 D :延迟…………………………………………………………………………………….42

 5.4 E :在振幅包络……………………………………………………………………………..42

 5.4.1 Pulse和Noise乐器……………………………………………………………………42

 5.4.2 Wave乐器………………………………………………………………….……………..42

 5.5F:Wave框架………………………………………………………………………………..42

 5.5.1 Kit乐器….………………………………………………….…………………………..42

 5.5.2 Wave乐器:……………………………………………………………………………….43

5.6G:瑺规的选择…………………………………………..……………………………………43

 5.7 H :跳跃(或停止)…………………………….…………………………………………43

 5.7.1 Phrase屏幕…………..………………………………….……………………………….43

 5.7.2 Table屏幕………………………………………………………………………………..44

 5.8K: Kill Note………………………………………..………………………………………44

 5.9 L:幻灯片( Legato的)………………………..………………………………………..44

5.10 M:主音量………………………………………………………………………………….45

5.11 O :设置输出……………………………………………..………………………………..45

5.12 P:弯音/音高的转变………………………………………………………………..………45

5.13 R :重噺制动最新的发挥注意……………………………………………………………46

5.14 S: Sweep/Shape………………………………………………………………………..46

 5.14.1 Pulse樂器……………………………………………………………………………….46

 5.14.2 Kit乐器…………………………………..…………………………………………….46

 5.14.3 Noise乐器………………………………………………………………………………46

5.15 T:节奏……………………………………………...……………………………………….47

 5.16 V:颤音……………………………………………………………………………………..47

 5.17 W:Wave…………………………….……………………………………………………..47

 5.18 Z:随机…………………………………….………………………………………………47

 6.1.1激活同步…………………………………………………….……………………………..48

 6.1.2使用同步同车在song 播放模式……………………………….………………………...49

 6.1.3使鼡同步同车在live播放模式………………………………….………………………...49

 6.2. MIDI同步……….…………………………………………………………………….…..49

 6.3 nanoloop同步………………………………………………………………………………..49

 6.4键盘控制……………………………………………………………………………………..50

 6.4.1键盘布局…………………………………………………………………………………...50

 7.1导言…………………………………………………………………………………………..52

 7.2语言学………………………………………………………………………………………..52

 7.3编程语言……………………………………….…………………………………………….52

 7.4使用音位变体 …………………………….………………………………………………..54

 7.4.1短元音………………………………………………….………………………………….54

 7.4.2长元音………………………………………………………..……………………………54

 7.4.3 R -有色元音…………………………………………………………..…………………55

 7.4.4 回响…….. ………………………………………………………………………………..55

 7.4.5表示fricatives ………………………………………………… ………………………..55

 7.4.6发言权fricatives ………………………………………………………… ……………56

 7.4.7表示停止………………………………………………………………… …………..56

 7.4.8聲停止…………………………………………………… ………………………………56

 7.4.9 塞插音……………………………………………… ……………………………… 57

 7.4.10鼻音………………………………………………………………………………………57

感谢您购买Little Sound Dj !付出辛勤劳动使这一计划莋快速的工作和强大的改进。第3版稳定性已大大改善,用户界面有重大的改善现在应该比以往任何时候都更容易学习和使用。 如果您”tracker”类型的音乐没有任何的经验

首先一大堆的新的概念可能会让你头疼

我只能提供意见,不是要强调这个问题学习步骤要一步一步,嘗试保持它的趣味性以自己的步伐进步。几天之内要知道足够的关于该计划的信息,制作自己的第一首歌曲

这本手册主要是作为一個绝对的初学者的指南,但

此外作为一个参考手册。目标与该手册的目的是要明确涵盖所有领域。还有很多的经验和知识

这并不适合荿为一个手册我高度推荐检查

用户维持的wiki网站

它包含了许多有益的信息,如教程技巧和小窍门。

与其他用户取得联系祝快乐!

Game Boy声音嘚芯片有4个通道,每个通道有4-bit的分辨率

Wave Channe : 软合成器,采样重放和语言合成

在这方面的文件标识按键:

开始了lsdj ,你应该面对一个屏幕像 圖1.1

SONG的标志在左上方的窗口表明这是SONG屏幕,这个窗口安排可以你的音乐四个栏目用破折号,每个栏代表的一个Game Boy sound通道有两个pulse 波通道,一個自定义wave通道(使用采样鼓组或软合成波的形式) 和一个noise通道。你可以用光标移动左右之间的不同通道

会显示在屏幕右下角的屏幕上(见图1.2 ) 。最有用的

屏幕上规定了在中东行也称为主要行。

它包含 song, chain, phrase instrument 和 table屏幕这些屏幕是后一级的菜单。最左边的song屏幕上介绍的概况在整个song屏幕,而右边的table屏幕作详细的乐器编程您可以浏览之间不同的屏幕,并在上选择和按压光标键

在同一个树型结构中。phrase屏幕是一个16步音序器注意实际数的据输入那里。

chain屏幕是一个16步音序器您可以在此输入phrases的序列

song屏幕是一个长256步音序器,如果您所输入chains的音序将会被播放

1.5制作你的第一个乐曲


浏览到song屏幕,并把光标在PU1栏现在轻拍A按钮两次,插入一个新的chain数字00应该会出现

进入chain屏幕。在那里经过相哃的程序:轻敲两次插入一个新的phrase,

在phrase屏幕(图1.3 ) 您可以输入note并用于播放。

光标移动到的note栏中并按下A编辑一个note。之后C - 3

将马上出现: C在noteΦ 3代表一个八度。按下START播放phrase

 note怎样被播放从屏幕顶部到底部。你可以改变note的值靠按压A和按压光标按钮来实现

现在,可以尝试将光标移箌向上和向下插入更多note在其他位置

如果您想要删除一个note,先按B然后按A, 当听完之后再次按压START停止phrase。干净的pulse声音可能变得有点沉闷移动咣标到instrument屏幕上按下SELECT + RIGHT。

在Instrument屏幕中(图1.4 ) 我们可以让声音更加有趣。

尝试更改envelope 设置从A8到A3 。现在再次按压START,你可以听到一些改变现在这些声音应更有弹性 。

让我们尝试采样drum kits现在,我们要改变通道向wave 通道,返回到song屏幕上,光标移动到wave 通道上并创建一个新的chain和一个新的phrase之湔(在空白的位子上轻敲两次A) 。然后移动到phrase屏幕上的INSTR栏上

,轻拍A两次以创建一个新的instrument

按下SELECT + RIGHT去编辑那些instrument,改变instrument的类型按A + RIGHT一次,在类型界面上然后再返回phrase屏幕上。现在您应该可以进入drum的声音,和你之前进入notes的方式相同

Little Sound Dj的表现很奇怪这里有一些东西尝试。


 ?如果您嘚烧录卡根本没有启动只有显示乱码,在启动任天堂的标志时问题可能是烧录卡插脚被氧化。请尝试移动并重新插入您的烧录卡30 40次。

?如果该软件的表现有一点奇怪乐器似乎并不工作

作为假定的或相似的,做了充分重置的内部记忆体靠导航到该projec屏幕并按压SELECT + A


1.7 十六进淛数字系统

在谈下一章之前,现在正是一个好时机介绍与十六进制数字系统,Little Sound Dj所代表的值

十六进制数字系统的工程类似于传统的十进淛数字系统。唯一的区别是它的基础是16而不是10 这意味着它由16个独特的符号组成: 数字0到9,其次是字母A至F本手册将标记十六进制值与一媄元的迹象。

请注意十六进制和十进制的值是真正的平等;只是

方式不同。之所以要使用十六进制系统在这里是节省屏幕空间;

十六进制數字,每一个字节值使用不超过两位数字 (值范围是0到

代表正负数与两位数字,既可以是一个问题

在Little Sound Dj 里,号码是包装材料这意味着,当最小的数字( 0 )减去1 它将跳转到尽可能高的值($FF) 。因此$F能被描绘1以及255 ,看情况而定

如果您没有立即明白这一切,请不用太担惢-

正如前Little Sound Dj有几个屏幕,提出了在一个屏幕

其余的屏幕上project 和 groove,有更多的专门用途大部份的活动,您将可能在所谓的“在地图中部的主偠的部位


1,有两个隐藏屏幕不是显示在地图上的:file 和word屏幕。

如果您想要开始从其他一些屏幕播放的所有四个通道可以这样按SELECT + START.

 Song屏幕是朂高级别的音序器。你可以制作歌曲

屏幕上包含四个栏,四个通道栏

包含chains的列表,这将播放从顶端到下面不同chains是用于不同的通道。

鉯插入一个chain将光标移动到一个空地方同时按压A:如果你想要

注意:如果您要添加一个新的chain上的一个位子上已经有了一个值,没有什么会發生这是一项安全措施,以防止你意外的覆盖现有的chain如果你真的想制作一个新的chain在这一步,首先删除旧的chain按压B + A ,然后按AA

在song屏幕中,开始或停止播放的所有通道按下START。

立即重新播放所有的通道在song屏幕上,按下SELECT + START

(这具有相同的效果按下START启动非常迅速) 。

?一个空皛的位子上你可以向下面拉起chains按下B + A。


 ?您可以添加或删除song屏幕按压B三次(B,B,B,)做标记 这将遮盖下面的光标。

Chain是用于把phrases组合在一起从而創造一个单位联合许多phrases。一个chain可以代表较长的节奏 一个旋律或一个低音乐器。

chain屏幕包含两栏第一栏包含phrases的目录,它被连贯在一起而苐二栏使phrase变调

要添加一个phrase在chain里,将光标移动到一个空的位子并按A。如果您要插入一个新的phrase按压A两次。编辑一个phrase光标移动到phrase的数字,並按下SELECT + RIGHT.

不同通道可以共享相同的一个chain,即是没有chain

是以往任何时候都分配到一个特定通道Chains的数量是从很无限小到128($00-$7F) 。

在Phrase屏幕(图2.3 )是音序器最根本的一部分你可以进入note


不同通道共享相同的一个Phrase,那就是任何phrase可在任何通道播放一个phrase的声音可能会非常不同,视乎有关通道

鈳以使用64种不同的乐器,在instrument屏幕进行编辑

例如,在通道里K命令可以消除此位子的声音。

?在默认情况下所有的Phrases是16步长。使用的H (hop)命令它也有可能订出一个较短的长度的。

W***E这类型的乐器能够播放合成波在synth 屏幕上使用。这是用在wave通道

KIT 这类型的乐器起着采样元件,儲存在ROM (采样储存在4bit, 11468 kHz)这是用在wave通道

NOISE这类型的乐器产生过滤噪音,是在Noise通道用的

用于编程语言。对于如何学习制造编程语言 请看看了第7章。

请记住在右边通道内乐器不能自动播放。例如如果您要使用的kit乐器,以播放鼓采样你必须做到以下几点:

1 。到song屏幕迻动光标移动到wave栏,并插入新的chain轻按A两次。



这些参数都用在最大多数的instrument类型中

LENGTH 改变声音的长度。

PAN 声向 Pan的声音,以左/右/两边/无扬声器 (使用耳机输出,听到差异!)


VIB影响颤音(V)命令的变化。高频颤音类是短期的高频率调制可用于制造相当有趣的音色。其他类型嘚颤音是更传统的


W***E 选择可使用的波的类型。

失谐设置可用于制作有趣的phase效果当两个pulse 通道同时播放phase

SYNTH 选择合成的声音播放。利用编辑合成聲音按下SELECT + DOWN前往该SYNTH屏幕。


OFFSET 设置开始时的loop点如果loop被设置为OFF,此值可用于初步的声音跳跃的一部分

LEN 设置声音的长度。 (AUT=始终以此种状态播放到结束)

SPEED 选择全速或半速度

DIST 选择使用2个kits混在一起时的运算法则时CLIP是默认类型。SHAPE 和 SHAPE2的声音类似于CLIP但WRAP有更多的高频率和一些低音。

用来添加一些有趣的数字失真当按下A +(LEFT, LEFT) ,当CLIP值被选定时该程序将跳出的范围,从原材料的记忆播放声音

对于那些运行lsdj在模拟器或与备份齿輪,有一个Java应用软件取代原来的kits可在


如需有关如何生成speech,的信息请参阅第7章。Instruments的数目是有限的到64 (十六进制:$00-$39)

Tables本质上是变调的序列 命囹和振幅的转变,从而可以执行任何速度(默认情况下一tick每一步)和适用于任何通道。如果您想要您可以分配tables给instruments(在instrument屏幕通过改变TABLE的設置来实现) ,使table将开始在您每次播放instrument.

Tables包含6个栏目,这是从顶端到低端执行 第一栏是envelope栏其中是有可能的创建自定义的振幅包络线envelope。其佽是transpose栏可以用来使note变调,所扮演的某一特定的数目半音

其他的还有command栏,就像一个在phrase屏幕

默认情况下,每个步骤都将被执行在一tick但咜也是可能选择一个不同的groove用G (groove)命令

首先数字在envelope栏设定一个的振幅;然后数字设定多少的ticks的振幅应保持。

在table 的图2.9创建一个振幅包络与短期攻击囷中等难以为继它可用于base instrument.

一个典型的tables使用,是为了制造琶音 这是一出音乐性的播放非常快,使听者将得到的印象是一个是和弦被播放。Table在图2.10将制造的一个主要和弦

短的琶音可以使用C (弦)命令创建在phrases(见5.2为例) 。Tables但仍然有被用来创造更长的arpeggios 。

?制作一种instrument attack声音听起来更有意思,它可以是有益的让第一列在table中予以向上或向下变调

这是在phrase 和 table屏幕一个关键的捷径。

Grooves规定的速度与您的phrases 和 tables 的播放速度相同它们可以用来给您的音乐,一些额外swing摇摆不同声音的通道,不需要同步向对方;这意味着您可以使用一个单独的groove 对于每一个 phrase 和 table

为了解Groove嘚概念,您需要知道音序器'的时间处理是基于一个抽象的时间被称作tick.

 tick.的长度随不同歌曲的节奏而变化,但通常是靠近

1/60th一秒钟在groove屏幕上,您可以指定多少ticks每个note步骤的播放如图2.11将使编曲花费大约6/60th一秒钟对每一个note的步骤。

您也可以使用groove屏幕上创建自定义的节奏如图2.12,将使編曲花8/60th每秒在每一个note的步骤并且剩下的将以 5/60th每秒note步骤进行。这将创建一个摇摆的感觉在深思熟虑的规划中,groove也可以被用来创造三个一組或其他复杂的节奏结构

Groove 0是默认槽对所有的phrases。如果您想要您可以轻松地切换到另一个Groove( G )命令,在phrase屏幕上

您可以选择你想编辑的Groove,按下B + 光标

按下A + UP / DOWN将会改变swing的百分比率 而保持ticks的总数- 因此由此产生的音乐的速度不变。 (例如:起初的值是6/6 = 50%按下A + UP.,现在的值变化成了7/5 = 58%!)

正方 锯齿 或 三角形


Q Q也称为共振集多少波形信号是带动周围的截止频率。它影响向量或沉闷波的声音如何变动

Phase压缩波形水平它是适用于过濾后与Q 和cutoff。见图2.14为例子

2.9.2开始和结束的参数

使用这些设置来指定的值为开始和结束的声音。该程序将然后创建一个在开始和结束平稳淡出の间的值

把波形垂直。见图2.15为例子

在wave屏幕上,您可以查看和编辑单独的波形的合成程序每个程序有16($10) synth程序,

project屏幕(图2.16 )包含设置影響整个编程。

TEMPO 速度变化的节奏。这是有可能建立新的节奏无论是按A +光标,

SYNC 激活连接起来的串行端口 (阅读更多关于此,在第6章! )

閱读第3章为充***释CLONE

LOOK 改变字体和颜色的设置。

此屏幕还包含两个时钟显示工作时间,时钟显示Little Sound D j的时间已用于自上次记忆复位,以小時和分钟当播放时,时钟取而代之的是一个播放时间的时钟,这表明多久这首歌一直播放。总的时钟显示的时间Little Sound D j已经被用于在其中天数,小时和分钟

在 FILE按钮,按SELECT + A 在 LOAD/S***E您可以选择重置所有的记忆体。这是可以有用的如果你的记忆力在某种程度上得到打扰,或您的燒录卡开始表现奇怪

file屏幕(图2.17 )在project屏幕上,进入LOAD/S***E FILE按钮。File屏幕是用来储存你的音乐的 你的作品就到存储记忆体里。它也可以从储存记憶体里来调出歌曲File屏幕让您可以保存高达32首歌曲,在一个烧录卡上

注:file屏幕适用的烧录卡在1兆位的静态存储或更多。

FILE 显示你的工作的這首歌的file名称叹号( ! )表示更改时,已作出一首歌

2 ,file管理器是仅适用于烧录卡有1兆位的静态存储或以上的。当您的烧录卡没有一兆位的静态存储的情况下此按钮将被RESET MEMORY记忆按钮取代。

LOAD 负荷一首歌按下A,选择该文件加载,并再次按下A

S***E 储存的歌曲。按下A选择保存位置,并输入档案名称

DEL 删除一首歌。按下A选择文件删除,并按下A再次回到回去project屏幕。

如果您想要取消一项行动在此画面,只要按下B

?有一个有用的文件管理器在可得到

Song列表出现了歌曲名称版本号码和档案大小。当储蓄时这首歌将会被压缩,因此所产生的文件夶小会有所不同如果你想开始一个新项目,从(EMPTY)插槽装载

2.13 屏幕边缘上的信息

许多有用的数据显示在屏幕上边缘上(图2.18 ) 。

5. 目前的节奏在每分钟(BPM ).

Little Sound Dj有一个剪贴板可用于临时数据存储。按压B +A将剪切的值下光标和它储存在剪贴板。按SELECT + A值便可以贴上

在大多数屏幕上是囿可能的,按SELECT+ B和移动光标标记blocks当有标记了一个 block,,它可以被复制到剪贴板按下B或削减到剪贴板按SELECT + A剪贴板内容再贴上由按下SELECT + A

一些快速标记按鈕实施按压:

由按压A +光标 你可以改变所有的block

克隆是一个捷径可以为您节省许多不必要的复制和粘贴行动。它可让您直接从chains 和phrases屏幕 创建的副本chains 和 phrases

让我们说你有一个旋律在chains00 ,你想要继续这种旋律但变动不大。然后你复制00(SELECT + B, B) 和粘贴一列下来(SELECT + A), 这样,您就能:

第3章先进的技术, 33

现在把光标在第二个00上,并按下SELECT + (B, A). 您将得到一个新的chain (可能叫做 01) 这是一个00的副本 由于这是复制,您可以复制许多00

有两种不同的模式的克隆:slim-cloning(微小)的克隆和deep-cloning(深度)克隆您可以选择模式,在project 屏幕

deep-cloning(深度)克隆是在旧的phrases里没有修改的风险缺点是可能产生一些多余的记憶,因此您可能使phrases用的更快此外,您最喜爱的歌曲最多可能需要更多的blocks时被保存使用在file屏幕

一些明智的话,从许多国家的人辛苦赚来嘚经验:如果您使用的Game Boy烧录卡上的Little Sound Dj研究备份选项可能是一个好主意, transferer 或记忆卡

Game Boy烧录卡往往相当不稳定,因为它们是根据一个内部电池很可能出现一发不可收拾的事。如果您是认真的态度你的音乐你应该做定期备份,或至少尝试记录您的歌曲

3.4静音,独奏和平移

临时嘚静音通道按B +选择

如果的B按钮在SELECT之前被释放,该通道将留在静音直到B在被压一次。

相应地一个被播放的独奏靠按压B + START

如果的B按钮在START被湔释放,其他通道将留在静音

如果START按钮是首先被释放,所有的通道都将会再一次被打开

第3章。先进的技术 34

这是也有可能的,在song屏幕pan通道,向左或向右由按压B + LEFT / RIGHT

Live模式是song屏幕的一种特殊的风格,当在song屏幕时按压SELECT + LEFT就会出现在Live模式下,是有可能的开始和停止播放一个又一個的chains在与平常的song屏幕相比,不同的渠道可以独立开始和停止。当播放的时候它也有可能跳在不同的歌曲位子不会造成音频故障或失詓同步。

播放一个chain将光标移动到chain上然后按START,停止播放一个chain去别的通道按SELECT + START,如果另一个chain开始播放启动和停止将排队直到chain已发播放完了偠等待直到下一个phrase被替换,轻拍STAR两次以加快开关

当LSDJ在live模式而没有播放的时候,按SELECT + START将启动所有的四个通道。

在live模式使用chain loops是一个有用的技術这项技术是基于以下事实:这首歌编曲(时在live模式)将不会倒带这首歌的位子,所有的方式最多的第一首歌编曲的一步时遇到了年底的轨道,而是站复卷尽快它遇到的一个空洞的一步

例如:我们有一个***程序看起来像图3.1 。

第4章 先进的技术, 35

创造好的鼓音色乐器而不使用采样鼓包,可能有点棘手如果您已经没有以前的经验与鼓的合成。不过这是非常有用的技术,一旦你知道它这里有一些起出的想法。

使用pulse channel 1建立一个相应的鼓声音振幅包络线因该有强烈的attack和迅速的降调 $C1.Wave应该是50-50 high/low,即使其他波可用于制作更扭曲的声音乐器在荿功的创造了一个kick instrument时候,sweep值可能是最重要的部分,它应该有一个高的初始频率和老化的问题尝试设置一个$E3的值,同时播放在note C-6的乐器創造一个更漂亮的kick声音,尝试与实验envelope的长度参数

它也可以使用noise channel,创造相应的鼓

使用noise channe创造snare drum的声音,振幅包络线有强烈的attac和快速的衰减,尝试设置$C1用长度参数去创造更多的漂亮音色的小军鼓。

第3章先进的技术, 36

第3章先进的技术, 37

Shape参数可用于调整音色Shape值接近$EC可能证奣是有益的。

Hi-hats被创建使用noise 通道l$FF的shape值,选择一个音色与高频率的内容。改变envelope和 length参数为创造理想的envelope。为学习cymbals使用一种形状的值接近 $EE去創造一个有点粗糙的音色电子工程专辑。

这里表达的是用作因此尽可能在整个计划

A 在一个空的位子上插入 note

Commands可以被用在phrases 和 tables,可以改变声音有很大的力量隐藏在命令中,因此它是建议您速读这一章至少一次以得到他们所可以为您做一个什么样的想法,

轻轻敲击command将会显示┅个滚动的说明文字在屏幕顶部,A+光标可以用来浏览现有的命令SELECT.可以被暂停文本

运行table,这个命令可以用在phrases或在table之内,或跳跃到另一个table

靠一些简单的琶音创造和弦,扩展base note与给半音

延迟触发了一个note,特定的 ticks数目

这个命令的功能在两种不同的方式这取决于其中的乐器类型。

改变wave frame的播放在wave channel. 这个命令是相对的也就是说,该命令的值将被添加到当前frame帧的数目这可以用于播放通过手动声音合成器。

由于合成聲音包含16 ($10) waves,发出F10命令的效果将在跳转到下一合成声音

H命令是用于跳到一个新的播放位子,它也可以被用来停止播放(HFF )

H工程有点不同要看是否这是用在phrase 或 table屏幕

在phrase屏幕,H是用于跳跃到下一个phrase不播放目前的phrase到结束。

HFF 停止播放歌曲(或通道如果在live模式)

如果你想要创作在三拍子(3/4),把H00命令进入 $C i在每一个phrase.

在table屏幕中H是用来创造table loops,首套数字设定多少倍和跳跃被执行之前之前移动0表示“永远” ,第二位数字设定table的步骤Loops循环可以嵌套,那就是你可以有较小的loops替代大的loops

当它达到某一特定的note时 L命令执行弯音停止,

这将导致在一个弯音开始,从C - 4 弯臸F – 4,速度4 然后弯回的C – 4,速度3

警告:如果使用L命令并且下一个空的音符值(”—”),,结果将不明确。

这个命令的变化主输出音量。首先数字修改左边的输出再次的数字的权利。音量设置也可以通过与一个绝对的值,或改变由一个相对价值。

值0-7可用来指定绝对的量值8-$F给予的数量相对变化; 8 是没有改变, 9-$B 增加, $D-$F 减少.

M08 关闭左输出通道留下右输出通道,

M99 增加主音量用一步

MFE 减少做音量,用一步右音量用兩步。

Pan channe以左右,没有或两者产出

该弯音命令只在pulse 和 wave通道被执行当它被用在KIT instruments上,这听起来更像一个音高移动

P02 向上弯音,速度2

再次播放朂新的note首先数字调节的声音(0=没改变, 1-7=增加 8-$F=减少).,数字建立一个重新启动的时期如果一个值大于零,重新启动将会反复的n ticks,对一些鼓机这种作用是所谓的”flam.” 。

RF3 中速的flam,减少幅度(回声效果

这个命令有不同的效果,不同的instrument类型

s调节音高使用的Game Boy硬件。它是被用于创造低喑鼓和打击乐首先数字影响pitch(音高),其次改变弯音的变化速度

注意:当pulse channel 2正在被使用时, S的时不会产生影响!

S改变了loop points.首先的数字调节抵消了值,其次数字调制loop长度(1-7=增加, 9-$F=减少.) 创造性地使用,这个命令可以非常有用对于创造一个广泛的撞击和timbral的效果

在noise 通道, S像一个形状过濾器。首先数改变音高其次数字的改变噪音调制。命令是相对的

也就是说pitch/noise(音高/噪音)调制的值将被添加到目前使用的值内

在W命令是鼡来选取其中的四个预设的pulse波形选取一个。它只能用于pulse instruments.

Z命令可以用在这phrase屏幕上它重复了以前的non-Z命令在phrase上被发出,那是用过的命令值增加┅个随机的数介于0和z命令的值

注意: 目前Groove 和 Delay命令在, 随机和Hop(跳跃)不工作

Little Sound DJ可以同步与其他装置,因此它是有可能的运行两者是一个精確一样的节奏中这可以是有用的,Little Sound DJ BPM 仪表不是百分之百的准确在project 屏幕,您也可以激活同步通过改变SYNC模式来实现

重要说明:在同步工作時,则您应该首先确保您在Little Sound Dj的复制/副本都被设置为使用一个groove based上6 ticks/step.否则,由此产生的速度可能是错误的

sync works(同步工程)在两种不同的方式,這取决于编曲是否在live模式

两个Game Boys像往常一样运转不同的存在还是保持同步:

同步只是在一个时刻会失效,当master(主要的)Game Boy被停止slave (次要的)Game Boy仍然播放。那时停止slave (次要的)Game Boy ,然后再次开始

MIDI sync需要一个专门的MIDI同步电缆链接Game Boy 。有关如何建立一个MIDI到Game Boy适配器的信息请参阅网站在

KEYBD sync模式是不是真正关于同步的。相反它允许连接一个标准的PC键盘到的Game Boy ,因此PC键盘可以作为一个钢琴键盘那样的使用这有益的现场表演和即兴创作的。有关如何建立一个电脑键盘上的Game Boy适配器请参阅网站在

重要说明:在键盘上,要获得一个声响时音序器必须已经运行。 (艏先按下START! )在下一步在语音序器上notes将被播放。获得更精细的时机使用更快的groove在phrase上

一些基本的语言学概念,将有助于您建立自己的语境第一,在写信和讲话上没有一对一的对应关系;

第二讲话声音是听觉上的不同取决于他们的立场,

第一个问题问题孩子会遇到时当怹学习阅读。每在他语言中的每一个声音都可能被表现的超过一封信反过来说,每一封信都可能被描绘的超过一些话语由于拼写无规律,就声音方面来说是很必要的当使用allophones.(音位变体)

第二,同样重要的是指出被理解的是听觉符号,

举例来说,在coop里最初的k的声音将在听觉上的不同,从K的在所听的和所说的不同

第7章。讲话编程 53

为了容易记忆由轻敲speech instrument屏幕,并重新命名如果你想剪切和粘贴words,在speech instrument屏幕有音位变体将带有loop标记循环下去

第7章讲话编程 54

第7章.讲话编程 55



嗨大家好,我叫 Dan Reed, 是暴雪娱乐的遊戏工程师(gameplay engineer译注:游戏机制工程师,或者游戏工程师都可以),今天主要跟大家分享《守望先锋》(后面统一用Overwatch表示)中的网络脚夲化的武器和技能系统

那么这里先简单介绍下我在Overwatch中的主要工作。

Statescript脚本系统这也是今天我们要讲到的;

抛射物和单局游戏模式;

同时峩也参与了一些特殊武器、技能和运动系统的设计;

再有就是一些我自己都不记得的工作了。

概览(译注:这种黑体顶头格式用于每一页幻灯片上方提领下文)


Overwatch实现了一套自己的脚本系统来编写包括武器和技能在内的高层逻辑, 这套暴雪自有(proprietary)的脚本系统叫做Statescript

今天分享的内容包括关于Statescript的“为什么” 、“是什么”以及“如何做到的”。为什么我们决定实现这么特殊的一套系统Statescript到底是什么?以及它背后嘚技术细节这部分大约会耗时15分钟。

另外会讨论网络通信需求及解决方案包括脚本系统在Internet环境下遇到的那些限制,以及我们是怎么应對的约30分钟。

然后会分析一下这种方法的好处和挑战这部分大约5分钟。

最后声明一下本次分享不会包括的内容: 抛射物、命中检测以忣一些特殊技能的实现不是说这些不重要,这些都是很棒的特性值得作为独立的议题来讨论只是超出了今天的分享范围 。


我们需要给“非程序员”提供开发上层逻辑的能力因为我们知道需要创建大量的游戏逻辑,又不希望每个需求都要靠程序员手动编写解决方案

我們希望这个解决方案允许用户“定义”新的游戏状态,而不仅仅是“响应”这些状态一般典型的游戏脚本系统都有一个相当不透明的游戲模拟过程,其中脚本也能编写逻辑以响应事先定义好的事件通过用户自己定义变量、函数调用来微调,执行的结果最后都会消失回到嫼盒状态而我们更需要的是一个形式化的、明确的方式,使得脚本开发者(译注:scripter下面统一用开发者)对状态和状态转移能直接地、唍全地掌控。

我们想要模块化的代码尽可能多地被复用我们不会把一个特性(feature,也可译作功能)需求看作是一组垂直功能的堆叠而是會去设计并实现那些这个特性所需的基础功能组件。

我们需要一个无痛的、稳定的方式来实现一个能够通过网络同步的状态机手写这些玳码费时费力而且容易出错,所以最好让计算机来替你完成这些工作。

另外这个方案需要能够与项目引擎的其余部分协同工作我们也對比了很多第三方脚本引擎,但是最终还是决定自己去开发一套能嵌入到我们的游戏引擎中的脚本语言以得到最好的结果。


Statescript是一个可视囮的脚本语言;每一个脚本都是一组互相连接的节点(node)形成的图(graph)代表了一段游戏逻辑的实现;这里举几个脚本的例子:猎空的“閃回”技能,卢西奥附近队友受到的加速、治疗buff所有英雄都有的UI控件等。
当一个脚本运行时它会创建一个运行时对象,这里称之为脚夲实例(instance)每一个实例都被一个实体(entity,不懂的同学可以参考另一篇分享:Overwatch Gameplay Architecture and Netcode)所拥有例如每个“英雄”都是一个“实体”。如果你听過Tim Ford的分享你肯定知道这是什么。

实体上的脚本实例可以被动态地添加和删除例如,无论何时你被麦克雷的闪光弹晕到一段能够阻止伱移动、瞄准行为的脚本实例就会动态加在你的身上,并且在一段时间内起作用直到它被移除。

同一个实体上可以同时运行同一脚本的哆个实例


在所有的节点中,首先我们有入口(Entry)Entry是脚本执行的起始点,它的作用很基础就是在脚本开始执行时,触发一个脉冲给到咜的输出(Output)当然也有好多其他类型的Entry会等待特定的消息(Message)才触发。

然后是条件(Condition)Condition会影响脚本执行流程,上图中的布尔Condition仅仅基于┅些表达式的结果来输出“真”或者“假”

接下来是动作(Action),Action基本上就是C++函数调用这些函数在触发输出以前,会做一些立即完成的笁作像这个SetVar就是目前最常用的一个Action。

最后是状态(State)State代表一些正在进行中的工作。一个State一直是处于未激活(Inactive)状态直到它的Begin插头(Plug,可译作接口但是会有概念混淆)上收到脉冲信号,它才激活自己然后State就会一直保持在这个激活状态中,直到它自己决定关闭(Deactivate)戓者是因为外部原因而被动结束。

在这背后每个State类型都是一个带有一堆虚函数的C++类(class),这些虚函数提供了一系列接口包括OnActivate(激活)、OnDeactivate(关閉)、OnTick(轮询)、OnDependencyChange()等。这里面最重要的部分是他们都代表某种持续性的行为(behavior)而且这些行为都会在持续一段时间后停止。这个WaitState很简单就像咜的名字所描述的:“等待3秒钟就结束”。

(译注:所有的节点类型为了避免误解,后面统一用英文单词)


Statescript提供了大量的变量包括“實例变量”和“所有者变量”来存放数值。

每个实例都有只属于自己的一堆变量叫实例变量。

而实例所属的实体(译注:就是实例的“所有者”)一般也含有一堆共享变量。上图中运行在猎空的脉冲***脚本上的子弹(Ammo)和弹夹(Clip)变量,就是这个脚本的私有变量但“AbilityLock”变量却可以被猎空英雄实体的所有Statescript实例共享,这就是“所有者变量”

一个变量既可以是单个的基本类型,也可以基本类型的数组對于大部分需求来说,这已经足够了但是至少还有一些时候,我们希望能支持嵌套结构体(nested struct)和集合(bags)我们将来会考虑实现这个功能在哪里。

变量可以是“state-defined”(状态定义)的它们当前的值是根据当前的StatescriptState来确定的,所以基本上可以通过询问State来得到一个变量的值


Statescript节点嘚行为是根据属性定义的;从上图右边部分中能都看到,开发者可以从事先配置好的变量(Config Vars, 译注:翻译成配置参数比较好)列表里选择需偠的变量来给每个“属性”赋值;

Config Vars可以包含嵌套的属性例如图中右上方有个“HeadPosition”配置变量里就有一个嵌套属性,你可以从另外一个Config Vars里选擇哪个实体会被赋予这个位置属性,在这里例子里就是此脚本的所有者实体

每一个Config Vars类型都是通过C++中的一个函数来实现的,这个函数可鉯把这些变量的值返回给这些脚本下图是一些Config Vars的例子:


常见Config Vars有:字面类型,变量Utilities(基本就是一些C++函数)和表达式。表达式除了能做一些“foo是不是大于3”的无聊事情以外还能够引用嵌套Config Vars列表,以支持更复杂的逻辑例如:“源实体位置和目标实体位置之间的离是否大于3”。

其他Statescript功能 大部分其他功能今天没时间讲了但是有几个我认为值得一提的还是想拿出来说一下。第一个是Subgraph(译注:子图指的是每个節点还可以包含一个图)。


每一个State都有一个Subgraph的输出在State激活时就会产生脉冲,而在State关闭(Deactivated)时所有Subgraph中的State也会随之关闭。有些State会包含其他類型的Subgraph插头会在特定的时刻激活或者关闭State。
我们有不同的Containers变种灰色边框的是最基本类型的Container,几乎没怎么组织不会影响Behavior(行为);红銫边框的Container定义了哪些State是Subgraph的一部分,否则的话Subgraph只会跳转一次State;蓝色边框的Container是客户端专用的;紫色的是Server端专用的这些可以在必要的时候,在愙户端和Server端生成不同功能的Behavior

在我讲解第一个真实的脚本例子以前,我想简要的介绍一下两个重要的Statescript Theme(主题)


简单来说,也就是State的自我清理在一个State关闭时,它的逻辑behavior执行完成所以需要停止播放动画,清除它拥有的全部特效重置所有改变过的变量,并关闭它激活的Subgraph等等。

一个实例被删除时会关闭所有状态

一个实体销毁时它会删除所有实例。

游戏结束时它会销毁所有实体

这些都是显而易见的,但昰当整个class都有bug时开发者也不用担心,因为每个State的合约都是:如果需要清理它自己必须实现完整的OnDeactivate接口。


Statescript既支持指令式(Imperative)脚本:先做這个再检查那个,再做那个;也支持声明式(Declarative)脚本无论何时告诉电脑做什么,它就做什么能做到这一点的部分原因是因为我们有苼命周期管理。

我们发现针对大型、复杂的需求建模声明式脚本是最明智的选择。但是指令式也有它自己的一席之地通常被用在声明式脚本的指令树的叶子节点上。

这就引出了我们第一个Statescript实例



“死神”本来不能用右键开火所以现在让我们赋予他一个新的技能,流程如丅:玩家按住右键1秒钟摄像机就切入第三人称视角,代表技能现在已经开始准备然后玩家释放按键,死神就被发射到半空中注意,洳果玩家按住右键少于1秒钟的话什么都不会发生。

现在我们在编辑器里来搞定这个技能


先增加一个Entry,当“死神”出生时脚本就可以開始执行了。然后增加一个叫“LogicalButton”的State当右键被按住时触发一个Subgraph,还有另外一个在右键没有被按住时执行的Subgraph当右键已经被按住了1秒钟,紦“ReadyToLaunch”变量设置为True然后进入第三人称视角。
然后呆在这个状态直到右键被释放注意:这里用来演示操作过程的视频已经被加速到2倍,實际上我是没办法弄得这么快的(众笑)
一旦右键被释放,我们立即就会去检查ReadyToLaunch是否为True如果按住右键足够长时间的话,那它就一定是True而且如果我们真的这么做了,就一定能把自己发射到空中

正如你所见到的,这个脚本例子混合了一个声明式风格:这个行为当且仅当按键被按下时才激活和一个指令式风格:等待一秒钟,把变量ReadyToLaunch设为True然后进入第三人称视角。

然后来测试这个新的技能


一切正如我们所期望的那样:右键按下,1秒钟以后ReadyToLaunch变成True,然后进入第三人称视角右键抬起,我被升到空中同时ReadyToLaunch变成False。如果我只是轻轻点一下按键则什么都没有发生。我至少要按住右键1秒钟才能进入准备发射状态并进入第三人称视角。

下面来做一个更加复杂的脚本这是“猎空”的脉冲***,嗯这里我没时间讲解所有关于它如何运作的细节,但是你也能看出同样的原则在起作用声明式脚本:这个为True的时候,这些事一定会发生;以及指令式脚本:先做这个事情接着等待1秒钟然后做其他事。


在我们进入到网络部分以前再花5分钟的时间来快速地過一遍整个Statescript系统是如何用C++实现的。
整个Overwatch的计时器都是基于整数的Command Frames(命令帧也可译作指令帧,代表服务器下发到客户端的数据单位)的所以Statescript也利用了这个特性。

每一帧是16毫秒一秒钟刚好60帧;

每个实体都需要挂载一个Statescript组件才能执行脚本。假如你错过了之前那个很重要的分享(Overwatch Gameplay Architecture and Netcode) 那我告诉你,实体以及Overwatch是建造在一系列组件之上的,这些组件允许系统可以执行特定的操作这一切就是“实体组件系统模型”,簡称ESC


Statescript组件包含了所有在一个实体上执行脚本所必需的数据,会简单浏览一遍

客户端上会有内部命令帧(Internal Command Frame),这个内部命令帧与当前正茬模拟的来自Server的命令帧有所区别后面会详细讲到。

我们有一个Statescript实例数组和一堆所有者变量还有同步管理器(sync manager),后面会深入讲

每一個Statescript实例都是在脚本开始、停止时动态分配的;都有唯一的实例ID用来做网络序列化;它还有一个指向Stu(译注:结构化数据的缩写,后面还会提到) Graph Asset资源的指针Stu Graph对象里都是静态数据,不会在运行时改变;还有一个Statescript State数组State是多态的,在脚本中首次用到时通过一个工厂方法创建,然后就一直存在直到脚本被销毁

这里有一个未来事件Event的列表,这些都是准备好在将来的某个时刻在某个State或者是实例自己身上执行的倳件经常在与自己入队列时相同的命令帧上被触发,有时候会带有权重在未来触发

另外每个实例上都会有一堆实例变量。

顺便说一句這只是数据的粗略描述,真正深入到一个运行时的Statescript里 会看到更多标志(flags)、缓存对象列表(cached list)来优化性能。我上面列出的仅是一些最重偠的数据而且与我后面讲到的内容会有关联。


Statescript的State基类提供了一些实用函数例如“访问属性数据”、“事件调度”和“注册轮询回调(registering forticking)”。

这个基类还提供了一些虚拟函数留给派生类去实现所以我们就有了OnActivate,OnDeactivateOnTimerEvent,OnFrameTick这些接口如果State注册了轮询回调,那这些接口会在每个命令帧被调用到

最后三个虚函数是用来隐藏网络抖动的,稍后也会讲到


它是一个指向StatescriptDependencyProvider的指针数组,反过来每一个Provider也都有一个指回Listener的指针数组,这就形成了一个多对多的关系

运行的时候,Listener是在某些需要特定Providers的属性第一次被计算的时候懒加载的所以,如果一个属性请求查询某些实体的Health(血量)State的Listener就会获得一个指向那个实体的Health组件的Provider的指针,显然这个Provider也会同时指回Listener。

字典每个成员的key都是一个16位ID映射到我们的Asset库中某个已注册的asset(译注:这里需要了解暴雪的Asset管理系统)。

StatescriptVar可以是以下2种类型的任意1种:基本类型和基本类型的数组每个基本类型都是一个128位(bit)长的联合体(Union),可以存下整形、动态数组、字符串指针等;

StatescriptVar也可以引用一个Statescript的State可以获取到State的当前值。所以如果你想知道一个变量的值只需要调用GetState即可获取当前引用的State上该变量的state-defined值。关于这一点最常见的用法是ChaseVar State,这个State可以持续追踪变量的值变囮

继续其他议题以前,说两句关于结构化数据Stu


Overwatch中的很多资源(assets)都是用一种我们称之为结构化数据的格式定义的简称Stu。 这里会有一个步骤来把这些.stu文件编译成代码我们的编辑器editor、资源编译器complier和运行时runtime都能够理解并使用这些代码。对类(class )类型和数据成员添加属性、反射也昰支持的这些属性对于Statescript编辑器和资源编译器(后面我会讲到)都是很有用的。
这个例子里不好意思,“在”这个例子里有一个关于Wait State嘚结构化数据的定义,这里提醒一下这个不是C++代码,而是Stu标记语言Stu标记是用来生成描述这些数据对象的class的。

现在看下这个Stu class的第一个成員它只有一个属性(property),就是m_timeout持续时间代表这个Wait State的超时结束 时间。

它上方的Constraint标签告诉编辑器把这个属性的下拉选择内容限制为那些能够提供数值型结果的ConfigVar,可以是整形或者浮点型

在底部我们还添加了2个插头(plug),一个是用来在State被提早撤销时触发,另外一个是在等待结束时触发

顶部的宏定义DECLARE_STATESCRIPT_RTTI用来设置一些运行时类型信息(RTTI)。这个类的大部分代码都是关于重载函数OnActivate的

首先我们定义了一个指向Stu对象的指针,Stu对象包含了这个State所需的数据这些数据需要在编辑器里填充。

Stu对象的类是在上一页幻灯片中定义的

接下来又是2个宏定义,用来保證Abort和Finish这两个插头能够在期望的时间内触发

最后一行还是宏定义,是用来把运行时类型和Stu结构化数据类型关联起来这样的话,Statescript系统在代碼执行到这个阶段时就知道用哪个class来初始化。

显然我没有任何一个例子可以用来说明ActionsConditions和ConfigVars是如何实现的,但是你们可以稍微把他们想象荿State的更简化版本他们每一个都有且仅有一个被调用的函数,而且他们的运行时版本不包含任何数据在脚本执行时也不需要实例化任何東西,所以更简单

以上就是关于Statescript的简单介绍了。

现在是时候来说明如何用Statescript来做一个网络游戏了

我们的第一个需求是“可用性”


它不能干擾使用者并且抽象了全部的网络通信细节 。最早的时候我们不想区分服务器和客户端脚本,这种恐惧来自于即使听起来很简单的Behavious行為,实现起来也需要大量额外的脚本来同步数据写这样的代码很乏味也容易出错。我们的游戏开发团队对于那些本应由计算机完成的工莋容忍度是很低的所以很自然地也把这个原则应用到了Statescript网络版中。
结果就是我们可以在服务器和客户端运行同样的脚本我们发现其实吔给开发者提供在必要时分离的脚本行为,但是这样做的机会不多
必须能够适应快速响应的游戏。这意味着无论延迟有多高玩家的操莋必须能够立即有响应。这一点无需多言否则的话,假设你开了一***、用了一下技能或者开始冲刺然后等待服务器回包才能收到视觉仩的反馈,你一定会觉得这游戏逊毙了
安全性是必须的,我们必须防止玩家通过发送恶意数据包来影响其他玩家的行为没有人喜欢***者。
它必须足够高效允许游戏在弱网络环境中正常进行。因为Overwatch需要运行在全世界的网络上这就意味着有时必须面对“高延迟”、“丟包”等网络问题。
它必须是无缝的能够最小化那些可察觉的、来自网络的影响。最开始我们只是想着在遇到问题时能够有办法处理就恏了但是当我们实现了越来越多的新节点类型(node types,就是上文中提到的stateaction,condition等等)以后清晰地感觉到,我们需要一个更加正规的方法來处理那些因为使用特定武器和技能时遇到的肉眼可见的,丑陋的拉扯、卡顿问题
那现在来讲一下我们是如何满足这些需求的。首先让峩们来澄清一下对于一个特定的Statescript实例,“网络同步”意味着什么
经过同步化以后,服务器和客户端可以在使用逻辑上相同的实例就昰说,因为无需关注网络细节大家可以公平地讨论服务器和客户端都在模拟(simulate,译注:后面会多次提到这里采用的翻译是模拟,用在夲文里有运行、执行游戏逻辑代码的含义)的同一个逻辑实例

同步的结果是最终一致的,所以无论客户端做过什么样的预表现(Prediction译注:翻译成预测、预演、预表现都可以),无论发生什么样的网络异常服务器和客户端都能修正并最终回到彼此一致的状态。

另外还有非哃步的实例这些实例依然可以收到来自同步化实例的消息,也可以从同步化实例读取变量但除此以外,他们的内部逻辑又是完全独立嘚

下面是一些同步化、非同步化脚本的例子


对于同步化的实例,我们有武器、技能、表情、单局游戏模式和地图实体(大门、血包等)

对于非同步化的实例,我们有菜单、英雄收藏品、单局结束流程和音乐

再说一次,正因为脚本中可以在实例之间发送消息甚至是同步化实例和非同步化实例之间,所以我们可以做到让单局游戏模式实例控制音乐实例来播放不同的音乐

在我们更加深入网络部分以前,關于实例还有最后一个定义


任何一个给定的客户端上, 任何一个网络化的、可以被玩家直接控制的实体例如:你可能正在玩猎空或者源氏,我们把这个实体和它身上的Statescript实例叫做该客户端上的local所有其他的网络化实体都叫该客户端上的remote。

注意local实体并不是必须的例如当播放死亡回放时,或者当前游戏内玩家没有任何可以操作的对象时这时并没有local实体,你仅仅是在观看已经发生的一切

服务器会跟踪记录哪些实体对于哪些客户端是local的。

现在开始讨论一下服务器权威


网络版Statescript 就是服务器权威的这意味着服务器对于所有发生的事情,具有最终裁决权通信通常是从服务器到客户端单向进行的,唯一的从客户端到服务器的通信就是按键输入和瞄准

接着简单说一下从客户端到服務器的输入操作


如果你听过Tim的分享,你肯定已经看过这个流程图了而且是更加细节的。

注意:这里的水平轴是现实世界的时间首先,垺务器下发一次更新这是它处理过的最新的一个命令帧,在这个例子里帧号是100。客户端收到以后发现为了让自己可以对服务器正在發生的事情有影响,它的输入必须及时到达服务器以被正确处理这就意味着它不能仅仅把输入操作作为100帧的回包发给服务器,因为服务器上的时间会一直流逝所以它需要把输入作为未来的某个时刻发给服务器。但是应该有多“超前”呢

服务器和客户端形成了一个反馈環,服务器会分析命令帧到达时有多提前或者延后然后通知客户端这些计算后的往返时延,简称

RTT(round-triptime)所以这个例子里,假如客户端想偠发送针对100帧加上RTT的时延的回包那就是105帧,因而也就能及时到达服务器并处理

在实践中,我们实际上是在网络条件的基础上再超前┅点点。例如如果你的RTT频繁变化,我们的补偿就会再超前一点点来确保输入及时到达服务器

本来我们应该再回头讲讲客户端的,但是現在我们已经知道服务器如何从客户端获取输入那么我们可以更深入了解服务器的同步响应性。


首先服务器从客户端收集当前命令帧的所有实体的操作然后我们在所有的实体上执行这个命令帧,并把所有发生的变化储存在StatescriptDeltas中最后把这些Delta(直译为“变化”,这里不做翻譯了直接用Delta表示)发给所有的客户端
如果你还能记起早前讲过的,Statescript组件都包含一个Sync Manager用来在服务器和客户端之间对实体保持同步。在服務器端Sync Manager持续追踪一个StatescriptDeltas的数组,这些Delta代表了实体在一个特定命令帧上经历的变化注意,我们只在那些有变化的帧上创建Delta对象最后来看,这部分比例很小因为大部分时候对于一个实体来说很少发生变化。

现在过一遍StatescriptDeltas的数据结构首先我们有命令帧,注意我们的Delta代表是一個实体在命令帧开始和结束之间的那些变化;我们还有一个包含所有发生变化且已经同步了的实例的数组对于这个数组的每一个成员,嘟有这些属性:Instance ID;创建/销毁标志;以及所有发生变化的实例变量(Variable)数组对于每个实例变量都有一个ID字段,对于数组类型实例变量我們有一个字段代表“发生变化的数组下标范围”,通过追踪记录这个范围我们可以避免传输整个数组;还有一个数组记录了所有发生变囮的State的索引;再有一个数组记录了所有执行过的Action的索引;最后还是一个数组,记录了在一个给定命令帧上发生过变化的所有者变量(Owner

每┅个StatescriptDeltas在所有客户端都确认收到对应的命令帧前会一直保存在服务器,确认后就没必要在保存了可以很安全地删除它。


现在我们已经知道發生了哪些变化但是到底应该把哪些变化发送给谁呢?这就是StatescriptGhosts的用处所在了
StatescriptGhosts跟踪记录每个客户端对于服务器上的每一个实体的信息了解程度。现在看一下它的数据结构:客户端编号;最后一次确认的命令帧编号证实客户端确实拥有了现在这个及之前命令帧的全部信息;一个指针数组,指向外部的StatescriptPackets数据包这里的“外部”的意思是,我们已经发送了数据包但是还没有得到对方是否收到的答复注意,当┅个数据包被客户端确认接收(简称Ack)或者超时未接收表示发生丢包(简称Nack),Overwatch的网络底层会分别通知每一个系统模块也包括Statescript系统。峩们利用这个特性来维护StatescriptGhost对象:一旦我们得到某个数据包的Ack或者Nack我们就把它从外部数据包列表中移除。
还是先看数据结构:一个Local/Remote的标志根据牵涉到的实体相对于接受者是否为Local,包数据格式会有所不同;命令帧范围起始和结束编号;最重要的payload(直译为有效载荷指协议外嘚有效数据)字段,代表要传输的实际内容为了生成这个payload,我们创建了一个命令帧范围内全部StatescriptDeltas的并集这里的并集就是数学上的概念,基本上我们需要知道命令帧范围内的全部变化然后我们对这个并集中引用到的所有对象的值进行序列化。

如果命令帧范围是从0开始那咜肯定是一个刚刚建立连接的客户端,那就仅仅需要发送全部对象的“当前值”即可我们把这叫做全量更新(full update),这种情况下完全不需偠关心Delta

数据包在发送后会暂存。另外在命令帧范围相同Local/Remote标志也相同的情况下,数据包可以重复利用这是一个优化点:不需要花时间偅新创建完全相同的payload了。

与StatescriptDeltas的工作方式类似一个数据包也是会一直保存,直到所有客户端都已经确认收到其中的“结束帧”

下面是个Demo,用来演示某个具体实体的网络同步流程


在顶部的时间线(timeline)上能看见2个不同的Delta,对应于期间 Statescript实体发生过变化的命令帧第一个Delta发生在100帧,峩们立即创建了一个数据包并下发到客户端经过一段时间后,在103帧上这个实体产生了另外一次Delta。由于之前的数据包还在传输过程中沒必要重传,所以我们创建了只包含103帧Delta的数据包并下发

等到第106帧的时候,服务器发现出问题了:它可能不会收到100帧的数据包的确认消息叻这种情况下服务器就要做决定了:重发哪些包呢?它至少必须重发100的包但是是否重发103,现在决定还为时过早

在这个案例中,我们朂终决定多走一步还是发送100和103两个帧包的并集,避免因为103帧也发生丢包而引发的问题但这就意味着客户端可能收到两次103包,如你所见確实发生了如果说冗余可以帮助一个客户端更快地从一连串的丢包中恢复过来的话,那它就完全是值得的

客户端也懂得这种重复是服務器的策略之一,所以它不会处理第一个103包 因为这样做不但会导致错误的执行状态(illegal simulationstate,只有103的变化缺失了100的变化,这种状态在服务器仩根本不存在)而且也没必要(后面无论如何都还会收到一次包含103包的合集,已经是最新的了根本不需要第一个103包 )。

最后回到服务器端收到了来自客户端关于2个数据包(译者注:一个是103的,一个是100和103合集的)的确认收到信息事实上收到第一个确认包并不会对服务器有任何帮助,因为仅仅能够知道100包还在路上;第二个确认包则会让服务器很开心了因为它知道100和103都确实被客户端收到了,一切都很顺利


客户端当前Local实体在模拟(运行)时会缓存按键输入和预表现。正如你还能记得起来的那样Local实体是运行在一个相对于下行包更加未来嘚时间线上的,我们发给服务器的上行包会在服务器处理该命令帧之前到达Local实体跑在未来,所以它用预表现来保存未确认的操作

当收箌一个来自服务器的StatescriptPacket时,首先发送一个确认收到的Ack信息如果是超时冗余或者乱序的包,就整个忽略掉正如之前的幻灯片中展示的例子那样。

否则如果是Local,首先回滚所有已经执行的预表现复制数据,然后使用之前缓存的输入重新模拟执行到当前时刻,我们有时候管這个过程叫前滚(Roll forth)这里要注意,执行前滚时尽管我们使用了之前缓存的输入和瞄准操作,但新的预表现又需要被加进来 另外,整個回滚、前滚过程都是实时发生在同一帧玩家是发现不了的 。

我们确实需要给Statescript  State和Action添加一些实用函数来保持这个过程是无缝的我等下就會再详细讲讲这个。

收到一个Remote包的处理过程


从上图中可以看到客户端有一个Remote实体,从服务器收到几个StatescriptPackets以后接受这些更新(Update),就这么簡单

注意,在大多数情况下Remote Statescript实例既不触发节点(Node)间的link,也不处理事件他们仅仅是轮询(Tick)那些需要刷新的State。在这个例子里他们嘟是“哑”的,依赖服务器告诉他们所有的事情这里唯一的例外是Client专有的Subgraph,只要拥有这个Subgraph的State认为它是激活的它就会一直全量地模拟执荇。

收到一个预表现包的处理过程


上图显示了客户端的一个Local实体进行一次预表现,并收到了一个StatescriptPacket回包图中的灰色条代表一个按键被按住不放,灰色虚线是客户端把这个输入发回给服务器哦对不起,是发给服务器

可以看见客户端在100帧做了一些预表现行为,来响应玩家按键服务器上也是在同一帧执行同样的过程,然后下发一个StatescriptPackets类似的事情也发生在103帧。

等到105帧的时候客户端收到一个描述活动的100帧回包,所以它回滚所有在103和100帧做过的预表现图中用洋红色表示的,直接丢弃它们然后复制服务器版本的100帧数据,图中是用青色表示的嘫后重新执行从101到105帧的全部过程(虽然作者没说明,但明显是绿色表示的)这个过程中重新构造了103帧。

最后当客户端收到来自服务器的苐二个活动时我们会在108帧得到一些类似的过程。

收到预测错误的包如何处理


在这个例子里 客户端发生了一些没做预表现的事情,所以吔无法进行回滚操作引起这些的原因可能是外部的,例如被“眩晕”或者被“击杀”;假如在103帧客户端做了预表现执行了一些操作但昰服务器上并没有做,有可能是因为另外一个外部原因阻止服务器这样做了一旦客户端意识到它在103帧上做的预表现永远收不到确认回包叻它就会回滚,然后从104帧开始重新模拟到现在

现在回头看看这些同步是如何作用于咱们刚刚给死神新增加的右键技能上的。


(译注:下媔很长一段时间都是动态演示过程最好结合视频,仅仅靠幻灯片是比较难以理解的)

现在按住右键等待,切换到第三人称释放,跳箌空中现在请把注意力放到屏幕右边的垂直方向的条上,这是Statescript调试器的时间线我现在暂时停止收集数据,并回滚时间到过去来看看发苼了什么事情

屏幕左上角,你可以看见View:Server字样说明现在显示的内容是服务器上发生过的事情,接下来我们开始对整个命令帧单步调试当我放开右键的时候,可以看到下面的Subgraph关闭了包括Camera 3P这个State也是,然后就能看见bool condition的ReadyToLaunch变成True了然后我们执行这个MovementMod

现在来看一下客户端都发生叻什么。


还是屏幕左上角切换View到Client。我们还是单步跟踪发射技能的模拟预表现可以看见时间线是绿色的。如果你观察时间线上光标旁边可以看到CF字样,CF代表命令帧(Command Frame)这就是死神这个实体当前正在进行模拟的一帧,ICF代表内部命令帧(Internal CommandFrame),这是Statescript系统正在进行模拟的一帧那么现在,因为我们已经执行一次预表现这两个值(CF和ICF)是相同的,但是当我们前进几帧以后再看看会发生什么光标进入洋红色区域,这就意味着我们从服务器收到了一个StatescriptPacket而且正在执行回滚你会注意到现在ICF刚好在我们第一次做预表现的那一帧上。回滚完成以后我们實际上已经处于更早的命令帧的开始阶段上了。

接下来我们会进入青色区域复制操作开始了。注意复制不需要跟随links。为了节省带宽盡量做到最小化:设置变量然后更新State。

如果你很好奇为什么这些Action没有被复制那是因为如果执行复制的话,SetVar和MovementMod这两个Action会冗余前者是因为其中的变量已经被复制过了;后者是因为它会执行自己的复制操作。关于这些优化我会再多讲一些

在现在的情形下,我们需要模拟回到當前这就需要执行“前滚”。但是因为什么都没做调试器什么也没记录,这就是为什么看起来它好像不见了但是我们肯定会确保回箌现在的。现在可以看到命令帧和内部命令帧完全相同

那么,难道回滚和前滚不会使得程序员开发新节点(Node)类型变得更困难吗毕竟誰也不想仅仅就是因为从服务器收到了一个包,就得重新开始播放动画或者重复播放一段声音或者生成额外的粒子特效!


***是:是的,它的确使得开发变难了尽管Statescript很大程度上把开发者从网络细节下保护起来了,C++程序员还是偶尔不得不处理这种问题为了帮助改善,State提供了很多实用函数例如每个State的激活和关闭都有一个Reason参数,Reason可以是“服务器回滚复制”、“实体被销毁”等State还提供了一些函数来帮助了解模拟过程当前处于哪个阶段,例如:“访问某一帧的某个State的所有活动和关闭信息”

Manager在你的State上调用当你的State仅仅处理输出(例如特效或者聲音或者UI)而且不需要自己反馈结果给到Statescript去模拟时,这就会很有用这种情况下,完全不用担心OnActivate和OnDeactivate的实现 只要等到一帧的最后对这些做響应就行了,这些可以帮助在回滚和前滚场景下避免因为状态关闭开启时带来干扰(pops)和额外影响

最后我们还有2个函数 PutUpdate和GetUpdate,用来从服务器向客户端传输State的数据虽然很有用,但是这种函数写起来很乏味又容易出错我们应该能够做到更好,后面会继续讲

Action也有一些实用(Utilities)函数,可以执行单独的回滚和访问临时回滚存储这里需要有存储是因为Action都是单例(Singleton)对象没有自己的存储区。然而我们是需要存点东覀的来避免在复制或者前滚期间播放声音。这是对于整个Action的无状态原则的一种破坏不怎么理想,但是看起来是值得让步的

幸运的是,我们不需要经常写这类可预测(predictable)的Action


即使有了这些实用函数,我没还是觉得编写同步化的State有点困难所以我们又想了另外一个办法。
峩们没有用PutUpdate和GetUpdate而是用了结构化镜像数据库自动从服务器到客户端复制数据,自动处理回滚有了这个以后,程序员从此不再需要手动编寫传输State数据的代码实现起来更快了,bug也少了

更好的是,程序员甚至都不需要编写定制化的逻辑来处理回滚时State的内部数据了

现在来看叧外一个例子:猎空开***时的回滚和前滚


这里可以看到WeaponVolley这个State,在我们做本地预表现时忽略掉了所有单次的射击(译注:这个忽略过程一萣要配合视频来理解)。

这里开始回滚因为收到服务器回包了。青色的这些是数据复制然后这是服务器视图。最终我们模拟回到了现茬注意看,尽管WeaponVolly State在全力更新每个内部命令帧回到现在它又是如何还能够重新处理那些已经忽略的单次射击呢?那是因为“抛射物”类型的子弹的模拟和同步都是由一个外部系统处理的回滚的处理方式和Statescript是不同的。而WeaponVolly State需要知道这一点(译注:这里没太弄清作者意图:獵空的手***明显是hitscan类型的,这里提到projectiles抛射物类型仅仅是用来对比嘛?)

虽然Statescript提供了实用函数和功能来帮助State很好地处理回滚前滚场景但朂终能否处理正确还是依赖于每一个State自己。

最后当处理重新模拟和前滚回到现在时,清楚地知道每个正在处理的命令帧的哪些历史数据昰精准的就比较重要了。


对于历史数据包括Local实体的全部变量和状态,这很容易理解毕竟我们正在处理的就是这些;还有按键输入和瞄准;以及所有实体的位置和姿态,位置对于技能系统来说尤其重要因为技能施放成功或失败很依赖实体的相对位置。

值得注意的是垺务器也有关于位置和姿态的历史数据,而且它还知道我们的RTT时间所以它在执行模拟期间,是可以获取当时客户端位置和姿态的确切值嘚

在服务器和客户端同时记录这些数据,对于避免预测错误是至关重要的

我们还有些不需要历史数据的,包括Remote实体的变量和状态;其怹实体组件(例如血量和过滤器)的数据

最后发现,只访问这些数据的最新、最全版本是ok的因为不像位置和姿态信息,服务器是不会對它进行倒带(Rewind)的无论如何,在重新模拟期间换个方法访问数据的历史版本反而让我们更容易错误预测。

现在总结下我们都是怎么莋的


首先需要我们有足够的可用性开发者不用关注网络细节。

我们还有响应性武器和技能的立即对玩家的输入做出预表现,然后再根據服务器的更新信息来回滚到正确状态

安全性也有保障,因为唯一需要发给服务器的就只有按键和瞄准欺骗服务器权威性是不可能的。

说到无缝核心系统和Sync Manager(同步管理器)提供了多种方法来帮助工程师实现无缝的回滚、复制和前滚,在同一帧内可以全部完成

现在就呮剩下“高效率”这一条还没有实现了。其实在讨论StatescriptDeltas、StatescriptGhosts和StatescriptPackets的时候已经覆盖到了一点点但是还是有一些需要讲的。


StatescriptDeltas、Deltas、Ghosts能够容忍丢包并能夠从中恢复而无需重发所有的中间状态的数据包,如果你还能记起来的话这都是使用并集(Union)来包含多个Deltas带来的好处。
为了使得带宽占用尽可能的低Statescript在编译脚本期间,会自动分析并发现“同步”需求对于Local实体,Statescript必须打包所有的东西这样预表现就会很精确。下发给愙户端的时候任何内容都不能忽略,因为我们假定其中所有得都是为了做到精确模拟所必需的

现实中,我们或许还可以做得更多例洳找出哪些State变量和Action仅仅影响服务器,但是写个算法来证明这一点的话编译过程就会变得复杂。

另一方面针对Remote对象进行优化对我们来说昰更重要的,至少对于英雄来说只有一个是Local的,其他11个都是Remote这是我们必须考虑的实情。

对于Remote实体这就是我们能看到的带宽优化空间嘚所在。Remote实体不会模拟他们的同步化Statescript实例他们仅仅是持有已注册需轮询的激活的State,而不会触发任何脚本中同步部分的下游链接

那么Statescript如哬知道该同步哪些内容呢?


为了指出哪些State和Action是Remote实例必须的我们在Node类型的结构体里增加了一个属性,你可以看见就叫SYNC_ALL。
有了这个属性State囷Action在被同步到客户端的时候,除非他们特意指明要在运行时同步给Remote否则就会被优化掉。

有些时候如果他们知道哪些修改过的变量不是Remote實例必需的,就可以优化掉如果没有这个属性的话,State就会只下发给Local客户端;Action则完全不会下发

Graph资源,都通过反射来判断哪些节点是需要哃步给Remote客户端然后我们分析每个节点引用了哪些变量,用一个列表存起来这些都是潜在的可能被Remote需要的变量。然后计算一下在创建Statescript数據包时如果引用这些节点和变量需要多少个字节。这样就可以很快得到一个每个Statescript脚本对象唯一的优化协议我们可以用这个协议来进行哃步。

因为服务器和客户端共享同样的脚本资源所以我们可以做到这一点。

现在来看看这些优化是如何进行的



首先来观察一下这个Remote猎涳,在服务器视图上开火你可以看见所有节点都点亮了,因为服务器在模拟所有的操作在右上角,可以看见这个实例正在使用的全部變量为了Demo演示的需要,我隐藏了全部“所有者(Owner)变量”这样你可以前后对比看得更清楚一些。

现在切换到客户端视图


可以看到点煷的节点很少,而且实例使用的变量也没几个需要执行的节点更少了,这不仅仅是带宽的极大降低也是客户端性能的胜利。

我会切回箌回滚和前滚状态一次以便你能看出区别。

现在是服务器(译注:还是配合视频吧下同)。

我知道你们中肯定有人对这里的流量很好渏下面是猎空打完一个弹夹然后花2秒钟换弹的操作的Local和Remote消耗的字节数。


如果你还记得起来的话我们已经对Remote做过优化了,因为客户端只囿一个Local实体但Remote实体要多得多。

如果包括Local在内你一共有12个猎空一起按下主武器开火按钮的话,把上面的数字累加一下总的字节数是大約,美国最大的bbs)有一整版的bug我们都没怎么见过现在我想说请不要再访问我们的Reddit上的论坛了(众笑)。只是开玩笑啦我们爱你,Reddit!

首先运行时、编辑器和调试器都是有实现成本的


我们花了一大块时间去开发这些。从2013年晚期Overwatch开始预热起一直到今天,我个人基本上有一半时间都是花在开发Statescript运行时、Sync Manager、编译器、调试器和一些核心特性上了

同样的时间,我们的游戏工程师和服务器程序员开发了无数的节点類型说到编辑器,我们的工具工程师(tools engineer)花了好几个人月开发一整年的时间来维护。真的是大投入


这个系统对于工程师来说,是有學习成本的尤其是要决定某个特性的哪个部分该放到代码里,哪块放到脚本里Overwatch里我们有许许多多不同的系统,当一个新的复杂的特性偠被加进来的时候系统需要用Statescript来配置它,并在Statescript里创建一个节点来驱动整个特性

有时候一个特性过于复杂了,你可能会想要把它拆分成哆个Statescript节点这样节点的一部分都可以被重用。

如果特性依赖状态机那你就会很希望它是用脚本实现,而不是C++毕竟这就是Statescript被创造出来的目的。

然而我一直都有愧疚感这可能有点扯远了,有时候你真的只需要一个节点就够了但还是忍不住写更多代码增加了复杂性,同时保持表面上看起来还是个相对简单的任务所以,需要花时间找到一个平衡点

对于程序员和策划来说,要适应我们的脚本主要使用的强夶的”声明式”编程风格还是有一些学习成本的。声明式逻辑编写方式区别于程序员日常使用的指令式编码风格这种转变需要花点时間和努力。


“最终一致性”网络模型并不保证完美的100%(blow by blow)数据复制意思是说,为了得到正确的结果脚本里需要添加一些临时的解决方案。对于一个基于“状态”而不是“事件”的复制模型来说这是个不幸的后果。

简单来说我们没有足够的带宽,来把服务器上所有的Φ间变化步骤都同步给客户端取而代之的是,我们只在一帧的末尾才下发这些变量

下面是个例子,可以用来说明为什么“顺序”很偅要


“秩序之光”的右键开火,包含两个阶段:按下充能和抬起发射按下时间越长,击中伤害越高开火时我们会立即把变量Scalar设置为0,這在服务器上跑的好好但是当所有的Remote客户端复制这个操作时,抛射物的体积都随着Scalar变成0了

幸运的是,这个bug修复起来很容易需要做的僦是加一个新的变量,存储VolleyWeapon State在客户端需要用到的“视觉缩放”信息

然而,这是反直觉的存在这种事让人很紧张。幸运的是这种事情發生的次数,用一只手就能数的过来它实在没什么大不了的,只是有点烦人


那么,考虑到所有的这些挑战对于一个3A级游戏来说,开發一个自动同步的基于状态的脚本系统,真的是个好主意嘛我们觉得是。声明式编程看起来也很好地适应了我们的最终一致性网络模型有了这一切,我们交付的游戏能够做到高性能、低带宽我们的团队也变的足够高效,能快速开发出一些很酷的玩法如果必须从头來过,我们可能还是会选择这样做
Q: 有没有什么时候,你觉得用代码来实现一个行为会比脚本更好能给一个例子嘛?

A: 通常在有复杂的循環或者有性能顾虑时我们都倾向于用代码实现。用脚本也可以做循序但是会有点混乱,要用links互相前后乱指说到性能考虑,射线检测(Raycasting)就是个最好的例子你一般不会想在脚本里这么做。所以不会有一个Raycast State或者ActionAction需要每帧被触发,Raycast State每帧都给你一个计算结果取而代之的昰计算消耗巨大的Raycast是用C++实现的。

Q: 对于Statescript二进制资源假如多人工作在相同领域,你有什么办法来帮助合并(merging)嘛避免依赖,方便检查(review)變化甚至说回头还能记得一个Statescript到底是干嘛的嘛?

out)的所以,实际上(同一资源)在同一时间只有一个人有权限修改和提交实际上我們也注意到了,确实有的时候你的一个资源的本地修改只是想临时试一下,不想提交的但是最后被误提交了。但其实也没问题因为還是只有一个人能够成功提交。你提到的问题在过去,确实一直都是个问题我们的确有多个人需要操作同一个脚本。所以解决方案僦是把它***为多个更小的脚本。这不是个完美方案但是它也能在一定程度缓解这个问题。我们一直都注意到这个问题了

Q: (还是刚才那个人)你遇到过依赖性问题吗?就是有人修改了我依赖的脚本但是他们的修改和我想做的冲突了。

A: 这绝对不会发生因为,通常你如果做了修改你会主动告诉你周围跟你一起工作的同事的。所以这种特殊的情况没怎么发生过

Q: 最近多人游戏的开发已经趋向用“基于状態”的系统来实现玩法部分,我很好奇你们是在多么早期的时候决定这么做的?

A: 我们从2011年就开始开发Statescript了在Overwatch以前,还在做“Titan”的时候峩们就在用Statescript。我想我们在早期就已经完成了一些原型了总结下来,我想你也知道手写一个同步化的状态机实在是太痛苦了。所以我们嫃的需要一些能够自动完成这些工作的方式了Paul Keita和我,Paul是暴雪的另外一个开发我们有了状态机系统(也就是Statescript)的时候就决定这么做了,峩主要负责其中“同步”的工作

Q: 当你想到这个脚本方案时,在开发团队这边有很多反对意见吗每个人都同意并认为是正确方向嘛?讨論过程是怎样的

A: 简单来说,是的!确实有那么一段时间来争取支持的最是最终,感谢我的领导Tim Ford的勇气嗯,我不知道他有没有在这里哦,他在在后排呢。他勇敢的支持了我让每个人都知道这真的是个好主意,然后我们最终把这个系统做出来了。

Q: 客户端回滚的代價有多大包括每个State的开发期和执行期。为什么不是仅仅把服务器最新的Update存下来然后恢复就行了

先回答第二部分吧,基本上会有大量的狀态需要保存需要花时间去反序列化或者复制。无论你用什么方法每一个State都有它自己的类要去实现,其实最主要的担心还是怕太慢所以我们最终的方法是存储预表现,有点像是:嘿这是先前的值,拿去用吧!这会比不停对我们积累的列表进行回滚更快速它试图找箌全量状态,然后变成那个状态就行了仅需要一些内存拷贝。可能也有其他的方式值得思考但基本上这是我们能够想到的最快的方式叻。

现在回答第一部分实际上,你能重复一下第一部分问题吗

“在客户端上做回滚有多难?”

确实很昂贵它和“模拟快进”差不多昂贵,幸运的是你只需要对一个Local实体做这个操作,不用对Remote实体做而实际上对Remote实体的处理才是更费时的。

“需要写很多额外代码去处理嘛”

就一个文件,大概2000行都是代码,我不知道算不算多(众笑)

Q: 在早先的一页幻灯片里,看起来你好像能够对节点分组并指定他們是Server-Only或者Client-Only。这好像与你的初衷是相反的:脚本开发者无需理解底层复制逻辑的本质!

A: 这的确是我们的目标之一但是谁也无法100%实现目标,對吧有时候你确实需要这样,尤其是很多时候混合了UI、Local之类的问题处理UI时,你可能倾向于使用Client-Only的Subgraph因为你肯定知道本地玩家

Q: 这个系统嘚可移植性高吗?如果你现在想做个新项目需要重新开发引擎吗?

A: Statescript非常依赖我们自己的资源(Asset)系统、我们的网络层和结构化数据(也昰资源系统的一个组成部分)所以,现在来说可移植性不好。我想其实你可以自己实现一个更加通用的版本反正我们还没这么做。

Q: 純用脚本实现的业务逻辑的比例有多高(Dan大神sorry了两次都没听懂问题,提问者听起来是个印度哥哥)

A: 想要精确测量有点困难我们开发一个新渶雄时,基本上却都是用脚本写的只有新的State、新的Node和类似的东西,才需要用C++实现显然,在游戏过程模拟以外还是需要大量代码的,洏且渲染和网络底层服务器侧,也都需要很多编码工作

Q: 感谢分享,去年的Overwatch分享里提到了“满足进攻者的精彩时刻”这个是怎么和回滾已经预表现结合起来的呢?是像你说的那样嘛不信任客户端是一件优先级很高的事情,现在还是这样吗

我想我明白你的问题了,确實有一些技能能够阻止”倒带”(Rewind)关于倒带,我解释一下有人想要射击你,你用的是死神的幽灵形态你不会受到伤害,虽然对手認为已经打中你了甚至是服务器倒带回到他射你的那一帧,我们也不会让它发生因为这都是你希望“缓和”的时刻,允许被攻击者有機会逃跑实际上我们的Statescript里有个Node专门用了阻止倒带。不许倒带不许倒带,因为我已经用了逃脱技能

Q: 能简单说一下Debuger调试器嘛?有bug出现时模拟游戏,单步跟踪是怎么做的?

A: 是的Debuger存储了所有的历史数据,记录了每个实体都发生了什么它也是用C++写的。没有什么外部工具就是直接开发出来的。它实际上会记录每个实体做的每件事情为了查明bug原因,调试它们你可以直接跳到你认为有问题的实体上,然後遍历历史过程在服务器和客户端视图之间切换,这个回答你满意吗


好吧,请确保你们已经做好准备接下来是David Clyde的关于“Data Build Pipeline of Overwatch”的精彩分享!如果你们有进一步的问题,随时找我就行了咱们可以去外面聊!

参考资料

 

随机推荐