查看原文
其他

Unity的50个使用技巧(2016 Edition)(下)

2016-11-08 翻译:高磊 Gad-腾讯游戏开发者平台


21 需要更新的自定义类不应该访问全局静态时间。

相反,它们应将增量时间作为它们Update方法的一个参数。当你如上所述实施一个暂停系统,或者当你想要加快或减慢自定义类的行为时,这样使这些类变为可用。


22 使用常见结构进行WWW调用。

在拥有很多服务器通信的游戏中,通常有几十个WWW调用。无论你是使用Unity的原始WWW类还是使用某个插件,你可以从生成样板文件的顶部写入一个薄层获益。

我通常定义一个Call方法(分别针对Get和Post),即CallImpl协程和MakeHandler。从本质上来说,Call方法通过采用MakeHandler法,从一个解析器,成功和失败的处理器构建出一个super hander。此外,它也调用CallImpl协程,创建一个URL,进行调用,等待直至完成,然后调用super handler。

其大概形式如下:


它具有一些优点:

它允许你避免编写大量样板代码。

它允许你在中央位置处理某些事项(例如显示加载的UI组件或处理某些通用错误)。


23 如果你有大量文本,将它们放在同一个文件中。

不要将它们放入inspector将编辑的字段中。使其在无需打开Unity编辑器,尤其是无需保存场景的前提下易于更改。


24 如果你想执行本地化,将所有字符串分离到同一个位置。

有很多方法可以实现这一点。一种方法是针对每个字符串定义一个具有public字符串字段的Text类,例如默认设为英文。其他语言将其子类化,并使用同等语言重新初始化这些字段。

一些更复杂的技术(其适用情形是正文本较大和/或语言数量较多时)将读取到一个电子表格中,并基于所选语言提供选择正确字符串的逻辑。



类的设计


25 确定实现可检查字段的方法,并将其确立为标准。

有两种方法:使字段public,或者使它们private并标记为[可序列化]。后者“更正确”但不太方便(当然不是Unity本身常用的方法)。无论你选择哪种方式,将它确立为标准,以便于团队中开发人员知道如何解释一个public字段。

可检查字段是public的。在这种情况下,public表示“设计师在     运行时更改此变量是安全的。避免在代码中设置该值”。

可检查字段是private,并被标记为“可序列化”。 在这种情  况下,public表示“在代码中更改此变量是安全的”(因此,你不应该看到太多,并且在MonoBehaviours 和ScriptableObjects中不应该有任何public字段)。


26 对于组件,切勿使不应在inspector中调整的变量成为public。

否则,它们将被设计师调整,特别是当不清楚它是什么时。在某些罕见的情况下,这是无法避免的。此时,使用两条,甚至四条下划线对变量名添加前缀以警告调整人员:

public float __aVariable;


27 使用Property Drawers使字段更加用户友好。

可以使用Property Drawers自定义inspector中的控制。这样可以使你能够创建更适合数据性质的控制,并实施某些安全保护(如限定变量范围)。


28 相较于Custom Editors,更偏好采用PropertyDrawers。

Property Drawers是根据字段类型实现的,因此涉及的工作量要少得多。另外,它们的重用性更佳—一旦实现某一类型,它们可应用于包含此类型的任何类。而Custom Editors是根据MonoBehaviour实现的,因此重用性更少,涉及的工作量更多。


29 默认密封MonoBehaviours。

一般来说,UnityMonoBehaviours的继承友好不高:

类似于Start和Update,Unity调用信息的方式使得在子类中难以使用这些方法。你稍不注意就可能调用错误内容,或者忘记调用一个基本方法。当你使用custom editors时,通常需要对editors复制继承层次结构。任何人在扩展某一类时,必须提供自己的editor,或者凑合着使用你提供的editor。

在调用继承的情况下,如果你可以避免,不要提供任何Unity信息方法。如果你这样做,切勿使他们虚拟化。如果需要,你可以定义一个从信息方法调用的空的虚拟函数,子类可以覆盖此方法来执行其他工作。


这样可以防止某一类意外地覆盖你的代码,但是仍能够赋予其挂钩连接Unity信息的功能。我不喜欢这种模式的一个原因是事项次序发生问题。在上述示例中,子类可能想在此类自行更新后直接执行。


30 从游戏逻辑分离接口。

一般来说,接口组件不应该知道任何关于所应用游戏的任何内容。向它们提供需要可视化的数据,并订阅事件以查出用户与它们交互的时间。接口组件不应该创建gamelogic。它们可以筛选输入,从而确认其有效性,但是主规则处理不应在其他位置发生。在许多拼图游戏中,拼图块是接口的扩展,同时不应该包含任何规则。

(例如,棋子不应该计算自身的合法移动)。

类似地,输入应该从作用于此输入的逻辑分离。使用一个通知你的actor移动意图的输入控制器;由actor处理是否实际移动。

这里是一个允许用户从选项列表中选择武器的UI组件的简化示例。这些类知晓的唯一游戏内容是武器类(并且只是因为武器是这个容器需要显示数据的有用源)。此外,游戏也对容器一无所知;它所要做的是注册OnWeaponSelect事件。




31 分离配置,状态和簿记。

配置变量是指一类被inspector调整从而通过其属性定义对象的变量。如maxHealth。

状态变量是指一类可完全确定对象当前状态的变量,以及如果你的游戏支持保存操作,你需要保存的一类变量。如currentHealth。

簿记变量是指用于速度、方便或过度状态。它们总是完全可以通过状态变量确定。如previousHealth。

通过分离这些变量类型,你可以更容易知道哪些是可以更改的,哪些是需要保存的,哪些是需要通过网络发送/检索的,并允许你在某种程度上强制执行此类操作。下面给出了一个关于此设置的简单示例。



32 避免使用public索引耦合数组。


例如,不要定义任何武器数组,任何子弹数组,以及任何颗粒数组,从而使你的代码类似于:


这类问题不出在代码中,而是在inspector进行设置时不发出错误。相反,定义封装三个变量的类,并创建下述数组:



33 避免使用除序列以外的结构数组。

例如,玩家可能有三种攻击类型。

每种类型使用当前武器,但生成不同的子弹和不通过的行为。

你可能会尝试将三个子弹转储到某个数组中,然后使用此类逻辑:


最好使用分离变量以便于名称辅助显示将放入的内容。使用一类使其整洁。


它假设不存在其他火、冰和风的数据。


34 将数据集中在可序列化类中,以使inspector中的事项更整洁。

一些实体可能有几十个可调分。对于在inspector寻找正确的变量,它可能成为一个噩梦。要使事项更简便,请遵循以下步骤:

对于各变量组定义分离类。使它们公开化和可序列化。

在主类中,对上述每个类型的变量定义为公开。

切勿在Awake或Start中初始化这些变量;由于它们是可序列化的,Unity会对它进行处理。你可以通过在定义中分配值来指定先前的默认值;

这将变量集中到inspector中的可折叠单元,从而更容易进行管理。



35 使非MonoBehaviours的类可序列化,即使它们不用于public字段。

当 Inspector处于Debug模式下,它允许你查看inspector中的类字段。这同样适用于嵌套的类(私密或公开)。


36 避免通过代码修改那些在Inspector中可编辑的变量。

Inspector中可调整的变量即为配置变量,且不应该视为运行期间的常量,更不能作为一个状态变量。按照这种操作使得将组件状态重置为初始状态的编写方法更加简便,同时使变量动作更清楚。



模式


模式是指一种按标准方法解决常见问题的途径。Bob Nystrom著有的《游戏编程模式》(免费在线阅读)为如何将模式应用于游戏编程中出现的问题提供了一种有效的观察资源。Unity本身使用了许多模式:Instantiate是原型模式的一个示例;MonoBehaviours遵循样板模式的一个版本,UI和动画使用了观察者模式,而新的动画引擎利用了状态机。

这些技巧均涉及到Unity模式的具体应用。


37为了方便考虑,使用单例模式。

下述类将从其自身继承的任何类自动转换为单例模式:


单例模式对于ParticleManager or AudioManager or GUIManager等管理器很有用。

(许多程序员对模糊命名为XManager的类报警,这是因为它指向一个命名不当,或者设计有太多不相关任务的类)。一般来说,我同意这种做法。但是,我们在每个游戏中只有少量的管理器,并且它们在每个游戏中都做同样的事情,因此这些类实际上是常用语。)

避免对非管理器(如玩家)的Prefabs独特示例使用单例模式。若不遵守这一原则会使继承分层复杂化,并使某些变更类别更困难。而是保持引用你的GameManager(或者其他合适的超级类)。针对常在类外部使用的public变量和方法定义静态属性和方法。这允许你编写GameManager.Player,而不是GameManager.Instance.player。

如其他技巧中所述,单例模式也可用于创建持续在追踪全局数据的场景加载之间的默认派生点和对象。


38使用状态机获取不同状态下的不同行为或者执行状态转换时的代码。

一个轻量级状态机具有多种状态,并且每个状态允许你指定进入或存在状态的运行动作,以及更新动作。这可以使代码更清洁,同时具有较少的错误倾向。如果你的Update方法代码有一个改变其动作或者下面的变量的if-或者switch语句,那么你将从状态机受益:


若存在更多状态,这种类型的代码可能变得非常混乱;状态机可以使它变得非常清洁。


39使用类型UnityEvent的字段在inspector中设置观察者模式。

UnityEvent类允许你将占用四个参数的方法链接到使用与Buttons上事件相同UI界面的inspector。


40当一个字段值发生变化时,使用观察者模式以检测。

只有当游戏中频繁发生变量变化时才会发生执行代码的问题。我们已经在一个通用类中创建一种关于此模式的通用解决方案,这样允许你无论何时发生值变化时注册事件。以下是一个health示例。其创建方式为:


你现在可以在任何位置更改它,而无需在每个检查位置执行检查,例如:

if(hit)health.Value -= 10;

无论何时health值低于0,调用Die方法。更多讨论和实施,请参考此。


41在prefabs上使用Actor模式。

(这不是一个“标准”模式。其基本理念来自于本文所提及的Kieran Lord。)

Actor是Prefab中的主要组件;通常是提供prefabs“标识”的组件,较高级的代码将与其经常交互。Actor使用同一对象上(有时在子类上)的其他组件—Helpers—执行工作。如果你通过Unity的菜单创建一个Button对象,它将使用Sprite和Button组件创建一个游戏对象(用Text组件创建一个子类)。在这种情况下,Button是一个actor组件。同样,除了附连的Camera组件之外,主摄像机一般有多个组件(GUI图层,Flare图层,音频监听器)。Camera即为一个actor。

Actor可能需要结合其他组件才能正常工作。你可以通过使用下述在actor组件上属性使prefab更稳健和有用:

1)使用来指示actor对于相同游戏对象所需的所有组件。(然后你的actor总是安全地调用GetComponent,而无需检查返回的值是否为空。)

2)使用防止附加相同组件的多个实例。然后你的actor总是可以调用GetComponent,而无需担心当有多个组件附加时应产生什么行为)。

3)若你的actor对象有子类时,使用。这会使你在场景试图更容易选择。



42对随机和模式化数据流使用Generators。

(虽然这不是一个标准模式,但我们发现它非常有用。)

Generator类似于随机生成器:它是一种具有可以被调用获取特定类型新项目的Next方法的对象。在构建期间可以操纵Generators生成各种模式或不同类型的随机性。它们很有用,因为它们保持生成新道具的逻辑与你需要的项目分离,从而使代码清洁多了。

这里有几个实例:


我们使用Generators派生障碍,改变背景色,程序性音乐,生成可能在文字游戏中生成字母的字母序列,等等。此外,Generators在控制以非恒定间隔重复的协程方面也有效,其构造如下:


更多关于Generators的讨论,请参考此发布。

Prefabs和Scriptable object


43对任何事物使用prefabs

你的场景中唯一的游戏对象不应该是prefabs(或者prefabs的一部分),而应该是目录。即使仅使用一次的唯一对象应该是prefabs。这使得更容易进行无需场景变换的变更。


44 对prefabs之间互相链接;而不要对实例对象互相链接。

当prefab放置到某个场景中时,维护prefabs链接;对于实例链接则无需保持。尽可能的使用Prefab之间的链接可以减少场景创建的操作,并且减少场景的修改。

如有可能,在实例对象之间自动创建链接。如果你需要在实例之间链接,则在程序代码中创建链接。例如,玩家prefab在启动时需要把自己注册到GameManager,或者GameManager可以在启动时去查找玩家prefab。


45若需要添加其他脚本,不要将Mesh放置在prefabs的根节点上。

当你需要从Mesh创建一个prefab时,首先创建一个空的GameObject作为父对象,并用来做根节点。把脚本放到根节点上,而不要放到Mesh节点上。通过采用这种方法,更容易替换Mesh,而不会丢失所有你在Inspector中设置的值。


46对共享配置数据,而不是prefabs使用Scriptableobject

若是如此:

  1. 场景较小

  2. 你不能错误地对单个场景(prefab实例上)进行更改。


47 对level数据使用scriptableobjects。

关卡数据常存储在XML或JSON中,但使用scriptable objects具有一些优点:

1)它可以在Editor中编辑。这样更容易验证数据,并且对非技术领域的设计师更友好。此外,你可以使用自定义编辑器使编辑更容易。

2)你不必操心读取/编写和解析数据。

3)它更容易分拆和嵌套,同时管理生成的assets,因此是从构建块,而非大型配置组成关卡。


48 使用scriptable objects配置inspector中的行为。

Scriptableobjects通常与数据配置相关,但它们也支持将“方法”用作数据。

考虑一个场景,其中你有一个Enemy类型,并且每个敌人有一堆SuperPowers。如果它们在Enemy类中,你可以创建这些常规类,并生成一个列表……若没有自定义编辑器,你便无法在inspector中设置一个包含不同superpowers的列表(每个具有自身属性)。但如果你创建这些super powers assets(将它们实现为ScriptableObjects),你就可以进行上述设置!

其构造为:


  1. 无法可靠地使Scriptable objects抽象化。相反,需要使用具体的基类,并使用抽象方法抛出NotImplementedExceptions。此外,你也可以定义Abstract属性,并标记应为抽象的类和方法。当遵循这一模式时,需注意以下几点:

  2. Scriptableobjects是指无法序列化的通用对象。然而,你可以使用通用基类,并且只对指定所有通用对象的子类抽象化。


49 使用scriptable objects对prefabs特殊化。

若两个对象的配置仅在某些属性上不同,则通常在场景中放置两个实例,并调整这些实例上的属性。通常较好的做法是创建一个单独的属性类,它可以区别两种类型为一个单独的scriptableobject类。

这可以提供更多的灵活性:

  1. 你可以利用从特殊类的继承,向不同对象类型提供更具体的特定属性。

  2. 场景设置更安全(你只要选择正确的scriptable object,而无需调整所有属性,便可以创建所需类型的对象)。

  3. 运行期间,通过代码更容易操纵这些对象。

  4. 如果你有这两种类型的多个实例,你就会知道当进行更改时,它们的属性将总是保持一致。

  5. 你可以将配置变量集分拆为可以混合和匹配的集合。

下面举出了一个关于此设置的简要示例:


如果特殊化类的数量较大,你可能要将特殊化类定义为普通类,并使用链接到一个包含某些特殊化类的列表,这些特殊化类是链接到你可以获取的适当位置的scriptable object中。



50 使用CreateAssetMenu属性自动向Asset/Create菜单添加ScriptableObject创建。

调试



51学习如何有效地使用Unity的调试工具。


  1. 语句添加上下文对象以查看它们的生成位置。

  2. 在编辑器中使用暂停游戏(例如,当你想产生错误条件,并且在该帧上检查部件属性时,它很有用)。

  3. 针对可视化调试使用功能(例如,当调试为什么没有光影投射时,DrawRay非常有效)。

  4. 针对可视化调试使用。此外,你可以通过使用属性提供mono behaviours外部的gizmo渲染器。

  5. 使用debug inspector试图(使用inspector查看运行中的私密字段的值)。


52 学习如何有效地使用调试器。

详见Visual Studio中的“调试Unity游戏示例”。


53 使用一个随着时间的推移绘制数值图形的可视化调试器。

这对于调试物理,动画和其他动态进程,尤其是偶然性错误非常有用。你将能够从图中找出错误,并能够同时有哪些其他变量发生了变化。另外,可视化检查也使某些异常行为变得更明显,比如说数值变化太频繁,或者不具明显原因地发生偏移。我们使用的是,但也有几种可用的方案。


54 使用改进的控制台记录。

使用一个可以根据类别进行颜色编码输出,同时可以根据这些类别筛选输出的编辑器扩展。我们使用的是但也有几种可用的方案。


55 使用Unity的测试工具,特别是测试算法和数学代码。

详见Unity测试工具教程,或者使用Unity测试工具以光速进行事后单元测试。


56 使用Unity的测试工具以运行“scratchpad”测试。

Unity的测试工具不仅适合正式测试,而且还可以便于进行可以在编辑器中运行,同时无需场景运行的scratch-pad测试。


57实现截屏快捷键。当你截屏拍照时,许多错误是可见的,并且更容易报告。

理想化的系统应该在PlayerPrefs保持一个计数器,从而使连续截屏不会被覆盖。截屏应保存在项目文件夹外,以避免人员将它们误提交到存储库。


58 实现打印重要变量快照的快捷方式。当你可以检查的游戏期间发生未知错误,这样更容易记录一些信息。

当然,记录哪些变量是取决于你的游戏。实例是玩家和敌人的位置,或者AI演员的“思维状态”(例如尝试行走的路径)。


59 实现一些方便测试的调试选项。

下面举出了一些示例:

  • 解锁所有道具。

  • 禁用敌人。

  • 禁用GUI

  • 让玩家无敌。

  • 禁用所有游戏逻辑。

要注意,切勿不慎提交调试选项;更改调试选项可能会迷惑团队中的其他开发人员。


60 定义一些Debug快捷键常量,并将它们保存到同一个位置。

通常(为方便起见)在一个位置处理Debug键,如同其它的游戏输入一样。为了避免快捷键冲突,在一个中心位置定义所有常量。另一种方法是在某个位置处理所有按键输入,无论它是否是Debug键。(其负面效果在于,此类可能需要引用更多的其它对象)。


61 在程序网格生成时,在顶点绘制或派生小球体。

这将帮助你在使用三角形和UVs以显示网格之前,确定顶点处在期预期的位置,并且网格是正确的尺寸。


性能


62请注意关于效能原因设计和构造的通用建议。


  1. 这些建议通常是基于虚构的,而不是由测试支持的。

  2. 即便有时建议是由测试支持的,但测试存在错误。

  3. 有时建议是由正确的测试支持,但它们处在不真实的或不同的环境之中。(例如,很容易展现如何比通用列表更快地使用数组。然而,在真实游戏环境中,这种差异几乎总是可以忽略不计。同样,若测试适用于除目标设备以外的不同硬件时,它们的结果可能对你无意义。)

  4. 有时建议是良好的,但却过时。

  5. 有时,建议是适用的。然而,存在权衡关系。航运慢速游戏有时要好于非航运快速游戏。而高度优化的游戏更可能包含可以延迟航运的复杂代码。

效能建议可能有助于记忆,帮助你通过下述进程更快地追踪实际问题源。


63 从早期阶段对目标设备进行定期测试。

不同的设备可能具有显著不同的效能特性;不要对它们感到吃惊。越早知道问题,你就能越有效地解决问题。


64知道如何更有效地使用效能评测器以追踪导致效能问题的原因。

如果你刚接触效能分析,请参阅效能评测器简介。

学习如何针对精细度分析来定义你自己的框架(使用 和)。

学习如何使用平台特定的效能分析,如iOS系统的内置效能分析器。

学习分析内置玩家中的文件,并显示效能分析器中的数据。


65 在必要时,使用自定义分析器进行更准确的分析

有时,Unity的效能分析器无法清楚地展示发生的事物;它可能消耗完分析框架,否则深度分析可能减慢游戏速度,以致于测试没有意义。我们对此使用自有的内部分析器,但应该可以在Asset Store中找到其他替代工具。


66衡量效能增强的影响。

当你作出更改提升效能时,衡量它确保该更改着实有效。如果这个更改是不可衡量或凌乱的,请撤销更改。


67不要编写可读度减低的代码,以保证更佳的效能。

除非有下述任一情况:

你碰到了一个问题,使用效能分析器识别出问题源,同时相较于可维护性损失,获得的增益足够高。或者你清楚自己在做什么。


命名规范和目录结构


68 遵循一个命名规范和目录结构。

保持命名和目录结构的一致性可以方便查找,并明确指出具体内容。

你很有可能想要创建自己的命名规范和目录结构。下面举出了一个例子。


命名的一般原则


  1. 按事物本身命名。例如,鸟应该称为Bird。

  2. 选择可以发音,方便记忆的名字。如果你在制作一个与玛雅文化相关的游戏,不要把关卡命名为QuetzalcoatisReturn。

  3.  保持一致性。如果你选择了一个名字,就坚持用它。不要在一处命名buttonHolder,而在其它位置命名buttonContainer。

  4. 使用Pascal风格的大小写,例如ComplicatedVerySpecificObject。不要使用空格,下划线,或者连字符,但有一个例外(详见为同一事物的不同方面命名一节)。

  5. 不要使用版本数字,或者表示其进度的名词(WIP,final)。

  6. 不要使用缩写:DVamp@W应该写成DarkVampire@Walk。

  7. 使用设计文档中的术语:如果文档中将一个动画命名为Die,则使用DarkVampire@Die,而不要用DarkVampire@Death。

  8. 保持细节修饰词在左侧:DarkVampire,而不是VampireDark;PauseButton,而不是ButtonPaused。举个例子,在Inspector中查找PauseButton,这要比所有按钮都以Button开头更加方便。(很多人倾向于相反次序,认为这样可以使名称自然分组。然而,名称不是用来分组的,目录才是。名称是用于在同一类对象中快速辨识的。)

  9. 某些名称形成一个序列。在这些名称中使用数字。例如PathNode0, PathNode1。永远从0开始,而不是1。

  10.  对于非序列的情况,不要使用数字。例如 Bird0, Bird1, Bird2,本应该是Flamingo, Eagle,Swallow。

  11. 为临时对象添加双下划线前缀,例如__Player_Backup


命名同一事物的不同方面


在核心名称与描述“对象”的事物之间添加下划线。例如:

GUIbuttons states EnterButton_Active,EnterButton_Inactive

Textures DarkVampire_Diffuse,DarkVampire_Normalmap

Skybox JungleSky_Top,JungleSky_North

LODGroups DarkVampire_LOD0, DarkVampire_LOD1

不要只是为了区分不同类型的项目而使用此类规范,例如Rock_Small, Rock_Large,本应该是SmallRock,LargeRock。


结构

场景,项目目录和脚本目录的结构应遵循一个类似的模式。下面列举了一些精简示例。

目录结构


场景结构


脚本目录结构


【版权声明】

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


点击一下立即阅读相关好文章


《贪婪》谈贪婪设计腾讯GAD游戏创新大赛


游戏美术3D设计干货回顾为VR优化UE4渲染器


这么做设计才好玩Unity教程


MOBA类游戏核心设计分析

......

近期热文

Unity的50个使用技巧(2016 Edition)

工作效率UP!Unity3D 手游版本构建之路


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存