当前位置:   article > 正文

Unity 安卓游戏开发学习手册(三)

Unity 安卓游戏开发学习手册(三)

原文:zh.annas-archive.org/md5/2F967148E2CB27E3CC5D9AF5E1B4F678

译者:飞龙

协议:CC BY-NC-SA 4.0

第八章:特效 - 声音与粒子

在上一章中,我们从 Monkey Ball 游戏中短暂休息,以了解 Unity 中的物理和 2D 游戏。我们创建了一个愤怒的小鸟的克隆版。这些鸟利用物理原理在空中飞行并摧毁猪和它们的结构。我们利用视差滚动制作了一个令人愉悦的背景效果。我们还创建了一个关卡选择屏幕,通过它可以加载游戏的各种场景。

在本章中,我们将回到 Monkey Ball 游戏。我们将添加许多特殊效果,以丰富游戏体验。首先,我们会了解 Unity 在处理音频时提供的控制方法。然后,我们将在游戏中添加背景音乐和猴子移动的声音。接下来,我们将学习粒子系统,为猴子创建尘埃轨迹。最后,我们将结合本章介绍的效果,为用户收集香蕉时创建爆炸效果。

在本章中,我们将涵盖以下重要主题:

  • 导入音频剪辑

  • 播放音效

  • 理解 2D 和 3D 音效

  • 创建粒子系统

打开你的 Monkey Ball 项目,让我们开始吧。

理解音频

与其他资源一样,Unity 团队努力工作,使得处理音频变得简单且无忧。Unity 能够导入和利用广泛的音频格式,让您可以在其他程序中以可编辑的格式保存文件。

导入设置

音频剪辑有一系列重要的设置。它们让你可以轻松控制文件类型和压缩。下面的截图展示了我们在导入音频剪辑时要处理的一些设置:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前面截图中的选项如下:

  • 强制单声道:这个复选框将导致 Unity 将多声道文件更改为单个声道的音频数据。

  • 后台加载:这将导致在将音频文件加载到内存时,不会暂停整个游戏。对于不需要立即使用的大型文件,最好使用这个选项。

  • 预加载音频数据:这将导致音频信息尽可能快地加载。这对于需要几乎立即使用的小文件来说是最好的。

  • 加载类型:这控制了在游戏播放时文件如何被加载;你可以从以下三个可用选项中选择:

    • 加载时解压缩:在第一次需要时从文件中移除压缩。这个选项的开销使得它非常不适合大型文件。这对于你经常听到的短声音来说是最好的选择,比如射击游戏中的枪声。

    • 内存中压缩:只有在播放时才会解压缩文件。当文件在内存中暂存时,它保持压缩状态。这对于不常听到的短到中等长度的声音来说是一个好选项。

    • 流式传输:这将在播放时加载音频,例如从网络流式传输音乐或视频。这个选项最适合背景音乐等事物。

  • 压缩格式:这允许你选择用于减少音频文件大小的压缩格式类型。PCM格式将为你提供最大的文件大小和最佳的音频质量。Vorbis格式可以为你提供最小的文件大小,但随着大小的减小,质量也会降低。ADPCM格式会根据音频文件的布局进行调整,以使文件大小处于中等水平。

  • 质量:仅当选择Vorbis作为压缩格式时使用。降低此值可以减少项目中文件的大小,但同时也会使音频引入越来越多的失真。

  • 采样率设置:这让你可以确定 Unity 中维护的音频文件的细节程度。保留采样率选项将保持原始文件中使用的设置。优化采样率选项将允许 Unity 为你的文件选择一个合适的设置。覆盖采样率选项将让你访问采样率的值并为你音频选择一个特定的设置。较小的值可以减少整个文件的大小,但会降低质量。

音频监听器

为了在游戏中实际听到声音,每个场景都需要一个音频监听器组件。默认情况下,任何新场景中首先包含的主相机对象以及你可能创建的任何新相机都附有音频监听器组件。你的场景中一次只能有一个音频监听器组件。如果有一个以上的组件,或者在没有组件的情况下尝试播放声音,Unity 将在你的控制台日志中填满抱怨和警告。音频监听器组件还为任何 3D 声音效果提供精确的位置定位。

音频源

音频源组件就像一个扬声器,它控制用于播放任何声音效果的设置。如果剪辑是 3D 的,此对象的位置与音频监听器组件以及所选模式的相对位置决定了剪辑的音量。以下屏幕截图显示了音频源组件的各种设置,随后是它们的解释:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 音频剪辑:这是此音频源组件默认播放的音频文件。

  • 输出:对于复杂的音频效果,可以将 Unity 的新音频混合器对象之一放在这里。这些允许你在音频最终播放之前,对音频及其可能应用的效果或混合进行具体控制。

  • 静音:这是一种快速切换播放声音的音量开关的方法。

  • 绕过效果:这允许你切换应用于音频源组件的任何特殊滤镜。

  • 绕过听众效果:这允许音频忽略可能应用于音频监听器的任何特殊效果。这对于不应该被世界扭曲的背景音乐来说是一个好的设置。

  • 绕过混响区域:这允许你控制是否让混响区域(控制环境音频的过渡区域)影响声音。

  • 唤醒时播放:这将导致音频剪辑在场景加载或对象生成时立即开始播放。

  • 循环:这将导致播放的剪辑在播放时重复。

  • 优先级:这决定了播放文件的相对重要性。值0表示最重要的,最适合音乐,而256表示最不重要的文件。根据系统不同,一次只能播放如此多的声音。播放文件的列表从最重要的开始,当达到这个限制时结束,如果有更多的声音超过限制,则排除那些值最低的。

  • 音量:这决定了剪辑播放时的音量大小。

  • 音调:这缩放了剪辑的播放速度。

  • 立体声平衡:这调整了声音在左右扬声器中均匀输出的程度,向左或右扬声器倾斜。

  • 空间混合:这是应用于音频源组件的 3D 效果的百分比。这影响诸如衰减和多普勒效应等因素。

  • 混响区域混合:(混响区域用于创建环境音频效果之间的过渡。)这个设置让你调整这些区域将对来自这个音频源的声音产生多大影响。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前述截图中的设置如下:

  • 3D 声音设置:这包含了一组特定于播放 3D 音频剪辑的设置。音量空间扩散混响选项可以通过使用组末的图表进行调整。这允许你创建更动态的过渡,当玩家接近音频源组件时:

    • 多普勒级别:这决定了移动声音需要应用多少多普勒效应。多普勒效应是当声源向你靠近或远离你时,你所经历音调的变化。一个典型的例子是一辆汽车在疾驰而过时鸣喇叭。

    • 音量衰减:这控制了声音随距离减小的音量。有三种类型的衰减:

      • 对数衰减:这是在声源中心较近的距离处声音突然快速衰减。

      • 线性衰减:这是一种与距离成正比的衰减方式,声音最大值为最小距离,最小值为最大距离

      • 自定义衰减:这允许你通过调整组末的图表来创建自定义衰减。当图表被更改时,它也会自动被选择。

    • 如果音频监听器组件比最小距离值更近,音频将以当前音量水平播放。在此距离之外,声音将根据衰减模式逐渐减小。

    • 扩散:这调整了声音在扬声器空间中覆盖的区域量。当使用一个以上的扬声器时,它变得更加重要。

    • 超过最大距离值后,声音将停止过渡,基于组底部图表的情况。

添加背景音乐

既然我们已经了解了可用的音频设置,现在是把知识付诸实践的时候了。我们将从添加一些背景音乐开始。这将必须是一个 2D 音效,这样无论音频源组件在哪里,我们都能舒适地听到它。我们还将创建一个简短的脚本来淡入音乐,以减少音效对玩家突然而至的冲击。我们将使用以下步骤来完成这个任务:

  1. 我们将从创建一个新脚本开始,并将其命名为FadeIn

  2. 这个脚本从四个变量开始。第一个变量是脚本需要达到的目标音量。第二个是过渡所需秒数。第三个变量是过渡开始的时间。最后一个变量跟踪与脚本同一对象上附加的音频源组件,允许我们定期更新它,如下所示:

    public float maxVolume = 1f;
    public float fadeLength = 1f;
    private float fadeStartTime = -1f;
    private AudioSource source;
    
    • 1
    • 2
    • 3
    • 4
  3. 接下来,我们利用Awake函数。它首先检查是否有附加的音频源组件,并用它来填充我们的source变量。如果找不到,则销毁游戏对象并退出函数:

    public void Awake() {
      source = gameObject.GetComponent<AudioSource>();
      if(source == null) {
        Destroy(gameObject);
        return;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  4. Awake函数通过将音量设置为0来结束,并在尚未播放时开始播放音频:

    source.volume = 0;
    
    if(!source.isPlaying)
      source.Play();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
  5. 为了随时间引起过渡,我们使用Update函数。它会检查fadeStartTime变量的值是否小于零,如果是,则将其设置为当前时间。这样可以避免场景初始化可能引起的卡顿:

    public void Update() {
      if(fadeStartTime < 0)
        fadeStartTime = Time.time;
    
    • 1
    • 2
    • 3
  6. 接下来,函数检查过渡时间是否已经结束。如果结束了,将音频源组件的音量设置为maxVolume,并销毁脚本以释放资源:

    if(fadeStartTime + fadeLength < Time.time) {
      source.volume = maxVolume;
      Destroy(this);
      return;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
  7. 最后,通过计算自淡入开始以来经过的时间和过渡的长度之间的比例,来计算当前的进度。进度的百分比乘以maxVolume的值,并应用于音频源组件的音量:

    float progress = (Time.time – fadeStartTime) / fadeLength;
    source.volume = maxVolume * progress;
    }
    
    • 1
    • 2
    • 3
  8. 回到 Unity,我们需要创建一个新的空游戏对象并将其命名为Background

  9. 将我们的FadeIn脚本和一个音频源组件添加到我们的对象中;可以通过导航到组件 | 音频 | 音频源来找到这些。

  10. 如果你还没有这样做,请在你的 项目 面板中创建一个 Audio 文件夹,并导入本章 起始资源 文件夹中包含的音频文件。由于这些文件体积小,且当前游戏的需求,它们的默认导入设置将完全适用。

  11. 层次结构 窗口中选中你的 Background 对象,并将 Background 音频拖到 AudioClip 插槽。

  12. 确保在 音频源 组件中勾选了 唤醒时播放循环 复选框。音量空间混合 选项也需要设置为 0,以使文件在游戏中全程播放,但在开始时不会发出声音。

我们为游戏添加了背景音乐。为了让声音保持恒定且不具有方向性,我们将音乐作为 2D 声音使用。我们还创建了一个脚本,以便在游戏开始时渐入音乐。这为玩家提供了平滑过渡到游戏的方式,防止声音的突然冲击。如果你的背景音乐太大以至于无法听到游戏中的其他声音,请在你的 Background 对象的 检查器 面板中降低 最大音量 值,以获得更愉悦的体验。

背景音乐对游戏体验有很大的贡献。没有一些恐怖的音乐,恐怖场景几乎就不那么可怕了。没有那令人敬畏的音乐,老板们也就显得不那么威严了。为你的其他游戏寻找一些好的背景音乐。对于 愤怒的小鸟 来说,一些轻松愉快的音乐非常适合;而对于坦克大战游戏,则应该选择一些更具工业感且快节奏的音乐,以保持心跳加速。

戳香蕉

为了理解 3D 音频效果,我们将为香蕉添加一个声音,每当玩家触碰它们时就会触发。这将使玩家在成功触摸到香蕉时获得额外的反馈,同时还能指示被触摸香蕉的距离和方向。让我们按照以下步骤来创建这个效果:

  1. 首先,我们需要一个名为 BananaPoke 的新脚本。

  2. 这个脚本有一个变量 source,用于跟踪附加到对象上的 音频源 组件:

    private AudioSource source;
    
    • 1
  3. 与我们之前的脚本一样,我们使用 Awake 函数找到对 音频源 组件的引用,为我们节省了一些在编辑器中的工作:

    public void Awake() {
      source = gameObject.GetComponent<AudioSource>();
    }
    
    • 1
    • 2
    • 3
  4. 当玩家在屏幕上触摸香蕉时,会向香蕉发送一条消息,调用Touched函数。我们在第六章《移动设备的特性——触摸和倾斜》中创建的BananaBounce脚本中使用了这个函数来调整其生命值。如果我们有音频源组件,可以再次使用它来播放音效。PlayOneShot函数使用音频源组件的位置和设置来播放快速音效。如果没有这个,我们将无法从同一个音频源组件中快速连续播放许多音效。我们需要传递给它的只是要播放的音频剪辑。在这种情况下,音频剪辑已经附加到音频源组件本身:

    public void Touched() {
      if(source != null)
        source.PlayOneShot(source.clip);
    }
    
    • 1
    • 2
    • 3
    • 4
  5. 然后,我们需要在项目面板中将新的脚本和音频源组件添加到Banana预设中。

  6. 需要将BananaPoke声音文件从Audio文件夹拖拽到新的音频源组件的音频剪辑槽中。

  7. 为了让游戏一开始不会听到烦人的爆音,取消勾选唤醒时播放选项。

  8. 接下来,我们想要听到触摸香蕉时距离上的差异。将空间混合设置改为1,以便将 2D 音效转变为 3D 音效。

  9. 最后,我们需要将音量衰减的值更改为线性衰减,并将最大距离设置为50。这让我们根据距离舒适且容易地听到音效的音量变化。

在 3D 世界中,我们期望大多数声音来自一个特定的方向,并且随着距离的增加而衰减。在 3D 游戏中创建类似效果,玩家能够轻松判断游戏世界中事物的位置以及它们可能有多远。这对于需要玩家能够听到潜在的敌人、障碍物或奖励的游戏尤为重要,以便他们能够找到或避开它们。

我们的坦克大战游戏有许多可以轻易潜行接近我们的敌人,因为它们在接近时没有声音。坦克通常不被认为是安静的机器。找一个引擎轰鸣声或者制作一个,并将其添加到敌方坦克中。这将给玩家一些关于敌人可能在哪里以及他们有多远的指示。此外,不同类型的坦克有不同的引擎类型。每个引擎的声音都有点不同。因此,在处理这件事时,为每种类型的坦克找到不同的引擎噪音,给玩家提供更多关于角落处可能存在的危险指示。

了解粒子系统

粒子系统为游戏的最终外观增添了很多效果。它们可以表现为火、魔法波、雨或许多其他你能想到的效果。它们通常很难制作得很好,但如果做得好,它们是值得努力的。特别是在使用移动平台时,请记住,少即是多。较大的粒子比大量粒子更有效。如果你的粒子系统在一个小空间内包含成千上万的粒子,或者为了增强效果而复制自身,你需要重新考虑设计并找到更有效的解决方案。

粒子系统设置

每个粒子系统都包含大量组件,每个组件都有自己的设置。大多数可用的设置有常量曲线两个常量之间的随机两个曲线之间的随机等选项。常量选项将是一个特定的值。曲线选项将是一个随时间沿曲线变化的设定值。两个随机设置在相应的值类型之间选择一个随机值。这在一开始可能看起来有些令人困惑,但随着你使用它们,它们会变得更加易懂。

正如你将在下面的屏幕截图和描述中看到的,我们将逐一了解粒子系统的每个部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 粒子系统中的第一部分,即初始模块,包含了 Unity 中每个发射器使用的所有设置:

    • 持续时间:这表示发射器持续的时间。循环系统在此时间后会重复自己。非循环系统在此时间后停止发射新粒子。

    • 循环:这个复选框决定了系统是否循环。

    • 预加热:如果勾选此复选框,如果循环系统已经有机会循环一段时间,它将开始循环。这对于应该已经点燃的火把来说很有用,而不是在玩家进入房间时开始。

    • 启动延迟:当粒子系统首次触发时,这将阻止粒子系统在给定的秒数内发射粒子。

    • 起始生命周期:这是一个单独的粒子将持续的秒数。

    • 起始速度:这是粒子生成时最初移动的速度。

    • 起始大小:这决定了粒子生成时的大小。使用较大的粒子总是比使用较小的粒子更好,因此需要更多的粒子。

    • 起始旋转:这将旋转发射的粒子。

    • 起始颜色:这是粒子生成时的颜色色调。

    • 重力修改器:这会给粒子一个更大或更小的重力效果。

    • 继承速度:如果粒子系统在移动,这将导致粒子获得其变换动量的一部分。

    • 模拟空间:这决定了粒子是随游戏对象移动而移动(即局部)还是保持在它们在世界中的位置。

    • 唤醒时播放:如果勾选此复选框,发射器将在生成或场景开始时立即开始发射粒子。

    • 最大粒子数:这限制了该系统在单一时间内支持的粒子总数。只有当粒子的发射速率(或其生命周期)足够大以至于超过其销毁速率时,这个值才会起作用。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 发射模块控制粒子的发射速度:

    • 速率:如果设置为时间,它表示每秒创建的粒子数。如果设置为距离,它表示系统移动时每单位距离的粒子数。

    • 爆发:这仅在将速率选项设置为时间时使用。它允许你在系统的时序中设置特定数量的粒子发射的点。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 如前一个截图所示,形状模块控制系统如何发射粒子。它具有以下选项:

    • 形状:这决定了发射点将采取的形式。每个选项都附带一些决定其大小的附加值字段。

    • 球体:这是粒子向所有方向发射的点。半径参数决定了球体的大小。从壳体发射选项指定粒子是从球体表面发射还是从球体内部体积发射。

    • 半球体:顾名思义,这是球体的一半。半径参数和从壳体发射选项在这里与球体的工作方式相同。

    • 圆锥体:这在一个方向发射粒子。角度参数决定形状更接近圆锥体还是圆柱体。半径参数决定了形状发射点的大小。当选项设置为体积体积壳体时,使用长度参数来指定可用于生成粒子的空间量。选项将决定粒子从哪里发射。基础从形状的底圆盘发射。基础壳体选项从圆锥体的底部但在形状的表面周围发射。体积将从形状内部的任何位置发射,而体积壳体从形状的表面发射。

    • 盒子:这从类似立方体的形状发射粒子。盒子 X盒子 Y盒子 Z选项决定了盒子的大小。

    • 网格:这允许你选择一个模型作为发射点。然后你可以选择从组成网格的每个顶点三角形发射粒子。

    • 圆形:这从单个点沿 2D 平面发射粒子。半径决定了发射的大小,弧度决定了使用圆的多少。从边缘发射决定粒子是从圆的内边缘还是外边缘发射。

    • 边缘:这会沿着一条线从单一方向发射粒子。半径参数决定了发射区域的长度。

    • 随机方向:这决定了粒子的方向是由所选形状的表面法线确定,还是随机选择。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 生命周期内速度变化模块允许你在粒子生成后控制它们的动量:

    • XYZ:这些定义了粒子动量沿每个轴的每秒单位数。

    • 空间:这决定了速度是局部应用于系统的变换还是相对于世界。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 生命周期内限制速度模块如果粒子的移动超过指定值,则会减弱其移动:

    • 独立轴:这允许你为每个轴定义一个独特的值,以及该值是局部的还是相对于世界的。

    • 速度:这是粒子在施加阻尼之前需要移动的速度。

    • 阻尼:这是粒子速度减少的百分比。它的值可以是零到一之间的任何值。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 生命周期内力变化模块为每个粒子在其生命周期内添加一个恒定的移动量:

    • XYZ:这些定义了需要沿每个轴施加的力。

    • 空间:这决定了力是局部应用于系统的变换,还是在世界空间中应用。

    • 随机化:如果XYZ是随机值,这将导致每一帧随机选择施加的力的大小,从而产生随机值的统计平均。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 生命周期内颜色变化模块允许你为粒子在生成后过渡的一系列颜色进行定义。

  • 按速度着色模块导致粒子在其速度变化时通过定义的颜色范围过渡:

    • 颜色:这是过渡的一系列颜色。

    • 速度范围:这定义了粒子必须达到的速度,以便在颜色范围的最小和最大端。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 生命周期内尺寸变化模块会改变粒子在其生命周期内的尺寸。

  • 按速度调整尺寸模块根据粒子的速度调整每个粒子的大小,如下所示:

    • 尺寸:这是粒子过渡时调整的大小。

    • 速度范围:这定义了尺寸值的每个最小和最大值。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 生命周期内旋转模块在粒子被生成后随着时间的推移对粒子进行旋转。

  • 按速度旋转模块使得粒子在速度更快时旋转得更多:

    • 角速度:这是粒子旋转的每秒度数速度。

    • 速度范围:这是如果角速度值未设置为恒定时的最小和最大范围。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 外部力模块增强了风区对象的影响效果。风区模拟了风对粒子系统和 Unity 中树木的影响。

  • 碰撞模块允许粒子与物理游戏世界发生碰撞和交互:

    • 如果设置为平面,你可以定义多个平面供粒子碰撞。这比世界碰撞的处理速度更快:

      • 平面:这是一个定义碰撞表面的变换列表。粒子只会与变换的本地、正 y 侧发生碰撞。任何在点另一侧的粒子将被销毁。

      • 可视化:这为你提供了将平面显示为实体表面或网格表面的选项。

      • 缩放平面:这调整了可视化选项的大小。它不会影响实际碰撞表面的尺寸。

      • 粒子半径:这用于定义用于计算粒子与平面碰撞的球体的大小。

    • 如果设置为世界,则粒子将与场景中的每个碰撞器发生碰撞。这对处理器来说可能是一个很大的负担。

      • 碰撞层:这定义了一个粒子可以与之碰撞的层列表。只有在此列表中勾选的层的碰撞器将用于碰撞计算。

      • 碰撞质量:这定义了此粒子系统的碰撞计算的精确度。选项将精确计算每一个粒子的碰撞。选项将使用近似值,并在每个帧中限制新的计算次数。选项的计算频率低于选项。如果碰撞质量设置为,则体素大小参数决定了系统估算碰撞点的精确度。

    • 阻尼:当粒子与表面碰撞时,这会从粒子中移除定义的比例速度。

    • 弹跳:这允许粒子保持其定义的速度比例,特别是沿着被撞击表面的法线方向。

    • 生命周期损失:这是生命周期的百分比。当粒子发生碰撞时,会从这个百分比中移除粒子的生命周期。随着时间的推移,或者通过碰撞,粒子的生命周期降至零时,它将被移除。

    • 最小销毁速度:如果粒子在碰撞后的速度低于这个值,粒子将被销毁。

    • 发送碰撞消息:如果勾选此复选框,则附加到粒子系统以及与之发生碰撞的对象上的脚本将在每一帧被告知发生碰撞。每帧只发送一条消息,而不是每个粒子。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 子发射器模块允许在粒子系统的每个粒子的生命周期中的点产生额外的粒子系统:

    • 出生列表中的任何粒子系统将在粒子首次创建时产生,并跟随粒子。这可以用来创建火球或烟雾轨迹。

    • 碰撞列表在粒子撞击某物时产生粒子系统。这可以用于雨滴飞溅效果。

    • 死亡列表在粒子被销毁时产生粒子。它可以用来产生烟花爆炸效果。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 纹理图动画模块使得粒子在其生命周期内翻动一系列的粒子。所使用的纹理在渲染器模块中定义:

    • 瓷砖:这定义了图中的行数和列数。这将决定可用的总帧数。

    • 动画:这为您提供了整张图单行的选项。如果此选项设置为单行,则所使用的行可以随机选择或通过使用随机行复选框和的值来指定。

    • 随时间帧:这定义了粒子在帧之间的过渡方式。如果设置为常数,系统将只使用一个帧。

    • 循环:这是粒子在其生命周期内循环动画的次数。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 渲染器模块决定了每个粒子在屏幕上的绘制方式,如下所示:

    • 渲染模式:这定义了粒子在游戏世界中定位自己的方法:

      • 广告牌:这将始终直接面向相机。

      • 拉伸广告牌:这将使粒子面向相机,但会根据相机的速度、粒子的速度或特定值来拉伸它们。

      • 水平广告牌:这在游戏世界的 XZ 平面上是平的。

      • 垂直广告牌:这将始终面向玩家,但沿 Y 轴始终保持直立。

      • 如果设置为网格,您可以定义一个模型作为粒子使用,而不是平面。

    • 法线方向:这用于通过调整每个平面的法线来对粒子进行光照和阴影处理。值为1时,法线直接指向相机,而值为0时,法线指向屏幕中心。

    • 材质:这定义了用于渲染粒子的材质。

    • 排序模式:这决定了绘制粒子的顺序,按距离或年龄排序。

    • 排序微调:这导致粒子系统比正常情况下更早地被绘制。值越高,它将在屏幕上越早被绘制。这影响了系统是出现在其他粒子系统或部分透明物体的前面还是后面。

    • 投射阴影:这决定了粒子是否能够阻挡光线。

    • 接收阴影:这决定了粒子是否会被其他物体投射的阴影影响。

    • 最大粒子尺寸:这是单个粒子允许占满的屏幕空间总量。无论粒子的实际大小如何,它都不会占据超过这个屏幕空间。

    • 排序层层内顺序:这些在使用 2D 游戏时很有用。它们分别决定了粒子处于哪个层级以及在该层级中的绘制位置。

    • 反射探针:这些也可以用来反射世界,而不仅仅是粒子。当反射的是世界而不是粒子时,可以使用锚点覆盖来定义一个自定义的位置来采样反射。

这里有大量的信息。你将最常使用初始发射形状模块。它们控制任何粒子系统的主要特性。其次,你可能会使用渲染器模块来改变粒子系统所使用的纹理,以及生命周期颜色模块来调整褪色效果。当这些部分有效地结合在一起时,将为你的游戏带来非常棒的效果,完善游戏的外观。学习它们能做什么的最好方法就是玩转这些设置,看看会发生什么。实验和一些教程,比如接下来的几节,是成为粒子系统创建专家的最佳途径。

创建灰尘轨迹。

为了让玩家更好地感受到角色实际上是处于世界中并与世界接触的,他们常常被赋予在环境中移动时能够踢起小灰尘云的能力。这是一个小效果,但为任何游戏增添了不少润色。我们将给我们的猴子球增加踢起小灰尘云的能力。让我们按照以下步骤进行:

  1. 首先,我们需要创建一个新的粒子系统,通过导航到GameObject | Particle System。将其命名为DustTrail

  2. 默认情况下,粒子系统会以圆锥形状发射小白球。对于灰尘效果,我们需要更有趣的东西。将本章Starting Assets文件夹中的纹理导入到你的项目中的Particles文件夹里。这些是由 Unity 提供的粒子纹理,它们在引擎的旧版本中出现过。

  3. 接下来,我们需要在Particles文件夹中创建一个新的材质。将其命名为DustPoof

  4. 要更改新材质的Shader属性,请转到Particles | Alpha Blended,并将DustPoof纹理放入Particle Texture图像槽中。这样可以将材质设置为部分透明,并且能够与世界以及其他正在发射的粒子良好融合。

  5. 要更改我们的DustPoof粒子系统的外观,请将材质放入Renderer模块的Material槽中。

  6. 系统中的粒子存在时间过长且移动距离太远,因此将Start Lifetime设置为0.5Start Speed设置为0.2。这样粒子会在消失前仅从地面稍微升起一点。

  7. 我们还需要使粒子更适合我们猴子的大小。将Start Size设置为0.3,以使它们大小适中。

  8. 看到所有粒子都是完全相同的方向有点奇怪。为了使方向不同,将Start Rotation更改为Random Between Two Constants,方法是点击输入字段右侧的小下拉箭头。然后,将两个新的输入字段设置为-180180,使所有粒子具有随机的旋转。

  9. 粒子的棕色是可行的,但并不总是与我们的关卡地形的颜色和性质相匹配。点击Start Color旁边的颜色字段,并使用弹出的Color Picker窗口选择基于环境的新颜色。这将使粒子在从游戏场地表面被踢起时更有意义。

  10. 最后,对于Initial模块,我们需要将Simulation Space设置为World,这样粒子就会随着猴子移动而留在原地,而不是跟随他。

  11. Emission中,我们需要确保有足够的粒子以产生适量的扬尘。将Rate设置为20以产生轻微的扬尘效果。

  12. 接下来,我们将调整Shape模块,使粒子能够在球的整个区域下发射。确保将Shape设置为ConeAngle设置为25Radius设置为0.5

  13. 使用颜色随生命周期变化模块,我们可以平滑粒子的突然出现和消失。点击模块名称左侧的复选框以激活它。点击颜色右侧的白条,打开渐变编辑器窗口。在渐变编辑器中,点击颜色条上方将添加一个新的标志,该标志将控制粒子在其生命周期内的透明度。此条形的左侧对应于粒子生命的开始,右侧对应于粒子生命的结束。我们需要总共四个标志。最开始的标志,将Alpha值设置为0,第二个标志,位置值为20Alpha值为255,第三个标志在位置50处,Alpha255,最后一个标志在最后,Alpha值为0。这将使尘埃粒子在开始时快速淡入,之后慢慢淡出,平滑它们的出现和消失过渡。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  14. 我们可以通过使用大小随生命周期变化模块,使粒子在出现和消失时增大和缩小,从而进一步平滑过渡。确保通过其名称旁边的复选框激活它。点击大小右侧的曲线条,粒子系统曲线编辑器将在检查器面板底部的预览区域中打开。在这里,我们可以调整任何小钻石形状的键,以控制粒子在其生命周期中的大小。与渐变编辑器的情况一样,左侧是粒子生命的开始,右侧是结束。右键点击它,我们可以添加新的键来控制曲线。要创建弹出效果,请将第一个键放在最左侧底部。第二个键应该放在顶部,与底部的0.2值相对应。第三个在顶部和底部的0.4值处效果很好。第四个应该在最右侧,大约设置为左侧的0.6,这些数字表示我们在初始模块中设置的开始大小的百分比,如下截图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  15. 最后,为了完成我们的粒子系统的外观,我们将使用旋转随生命周期变化模块,为粒子增加一点旋转。将值更改为两个常数之间的随机值,并将两个值字段设置为-4545,使粒子在其生命周期中稍微旋转。

  16. 为了让我们的猴子使用粒子系统,将其设置为MonkeyPivot对象的子对象,并将其位置设置为X0Y-0.5Z0。同时,确保旋转设置为X270Y0Z0。这将使其位于猴子球的底部并向空中抛出粒子。由于它是MonkeyPivot的子对象,它不会随着球的旋转而旋转,因为我们已经使对象补偿了球的旋转。

  17. 尝试一下。当我们的猴子四处移动时,他在身后留下了一条很好的灰尘轨迹。如果根据关卡的材质进行定制,这种效果可以非常出色,无论是草地、沙地、木材、金属还是其他任何材质。

  18. 你可能会注意到,即使我们的猴子从地图边缘飞出去,效果仍然在持续播放。我们将创建一个新脚本来根据猴子球是否真正接触地面来切换粒子效果。现在创建一个名为DustTrail的新脚本。

  19. 这个脚本的第一个变量将保存对我们试图控制的粒子系统的引用。第二个变量将是一个标志,表示球是否真正接触地面:

    public ParticleSystem dust;
    private bool isTouching = false;
    
    • 1
    • 2
  20. 我们使用OnCollisionStay函数来判断球是否触碰到了任何物体。这个函数与上一章中使用的OnCollisionEnter函数类似。不过,那个函数是在我们的鸟撞击到某物的那一刻被 Unity 调用的,而这个函数则是在每一帧球持续接触另一个碰撞体时被调用。当它被调用时,我们只需设置一个标志来标记我们正在触碰某物:

    public void OnCollisionStay() {
      isTouching = true;
    }
    
    • 1
    • 2
    • 3
  21. 因为物理系统只在FixedUpdate循环中改变,所以我们使用这个函数来更新我们的粒子系统。在这里,我们首先检查是否正在触碰某物,并且粒子系统当前没有发射任何东西,这由其isPlaying变量指示。如果条件满足,我们使用Play函数开启粒子系统。然而,如果球没有触碰任何物体,并且粒子系统当前正在播放,我们使用Stop函数来关闭它:

    public void FixedUpdate() {
      if(isTouching && !dust.isPlaying) {
        dust.Play();
      }
      else if(!isTouching && dust.isPlaying) {
        dust.Stop();
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  22. FixedUpdate函数的最后,我们将标志设置为false,这样它就可以在下一帧更新我们是否需要开启或关闭粒子系统:

      isTouching = false;
    }
    
    • 1
    • 2
  23. 接下来,将新脚本添加到MonkeyBall对象上。正如上一章所学的,如果我们没有将它附加到与球的Rigidbody组件相同的对象上,我们将无法接收到使脚本正常工作的碰撞信息。

  24. 最后,将你的DustTrail粒子系统拖放到Dust槽中,这样你的脚本才能真正控制它。

  25. 再试一次。现在我们的猴子可以轻松地四处移动并产生一些灰尘轨迹,直到它从关卡的边缘掉落,跳下平台,或者以其他方式悬在空中。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们让我们的猴子球具有扬起灰尘的能力。我们还根据球是否真正接触地面来控制灰尘的开启和关闭。这个小效果使角色在游戏中显得更加脚踏实地。它还可以根据拖尾持续的时间,让你感受到角色的速度。

我们之前讨论过的使角色更加接地气的另一个好效果是阴影。如果你还没有这样做,请确保为你的环境添加一些阴影细节。不过,你可能注意到,由于球体部分透明,实时阴影无法在其上生效。这时,我们之前在坦克上使用的 blob 阴影就派上用场了。

即使球体没有移动,我们的效果也会持续运行。尝试调整粒子系统是否播放,基于其刚体组件的速度。在上一章中,我们稍微调整了刚体组件的速度,如果你需要复习可以看看。作为一个额外的挑战,查看粒子系统的emissionRate变量。尝试让球体速度加快时,效果产生更多的粒子。

组合在一起

到目前为止,我们学习了各自独立的声音效果和粒子系统。它们各自可以为场景增添很多,设定氛围,并赋予游戏独特的润色。然而,有许多效果是无法独立存在的。例如,爆炸效果,如果没有视觉和听觉效果的结合,就不会那么令人印象深刻。

爆炸的香蕉

当事物爆炸时摧毁它们会让人感到更加满足。要制造一次恰当的爆炸,需要同时具备粒子效果和声音效果。我们将从创建一个爆炸预设开始。然后,更新香蕉,使它们在摧毁时产生爆炸。以下步骤将帮助我们创建香蕉爆炸效果:

  1. 首先,我们需要创建一个新的粒子系统,并将其命名为Explosion

  2. 我们希望我们的爆炸效果看起来更像是一次真正的爆炸。这时,我们的第二个粒子纹理就发挥作用了。为其创建一个新材质,命名为Smoke

  3. 这次,通过选择粒子 | 附加来设置着色器属性。这将使用一种附加混合方法,使粒子整体看起来更亮,同时仍然将粒子的 alpha 与背后的物体混合。

  4. 确保将新材质的粒子纹理属性设置为Smoke

  5. 同时,将你的Smoke材质拖放到粒子系统的渲染器模块中的材质槽内。

  6. 我们不希望这次爆炸持续得太久。因此,在初始模块中,将持续时间设置为0.5,并将开始生命周期设置为1,使其比原来的时间短得多。

    注意

    当处理像爆炸这样短暂爆发的效果时,可能很难看出我们的更改如何影响粒子系统的外观。完成这个粒子系统后,我们将不得不取消勾选循环复选框,但现在保持勾选状态会使得查看和工作变得更加容易。

  7. 接下来,为了防止粒子飞得太远,将起始速度设置为0.5,使爆炸效果集中且局限于一个较小的区域。

  8. 为了让爆炸有足够的粒子,需在发射模块中将速率设置为120

  9. 为了让爆炸看起来更真实,需要在形状模块中将形状改为球体。同时,将半径设置为0.5。如果你对改变爆炸的大小感兴趣,可以调整半径发射速率。两者都增加会得到更大的爆炸效果,而两者都减少则得到较小的爆炸效果。

    注意

    这种基本的爆炸效果仅仅是一种视觉上的爆炸,大多数情况都是如此。要制作根据环境改变或受环境影响而改变外观的爆炸效果,将需要额外的脚本编写和模型考虑,这超出了本书的范围。

  10. 我们游戏中的爆炸效果仍然不像真正的爆炸,所有的粒子都从边缘突然出现。这时就需要用到生命周期颜色模块。首先,我们需要通过在 alpha 通道添加新标志来消除粒子的突现。在大约边缘向内20%的位置添加两个新标志,并调整所有四个标志,使粒子在开始时淡入,结束时淡出。

  11. 渐变编辑器的渐变条底部的标志控制粒子在其生命周期中过渡的颜色。为了得到一个像样的爆炸效果,我们需要再添加两个标志,一个放在三分之一的位置,另一个放在三分之二的位置,将所有四个标志均匀地间隔开。爆炸通常开始时颜色较亮,接着在爆炸能量达到顶峰时颜色更亮,然后随着能量开始消散时颜色再次变亮,最后能量完全消失时为黑色。你选择的每种颜色都会影响爆炸的颜色。对于普通爆炸,可以选择黄色和橙色。对于科幻空间爆炸,可以选择蓝色或绿色。或者,如果是异形孢子云,可以使用紫色。发挥你的想象力,选择适合你想要爆炸效果的色彩。![爆炸的香蕉]

  12. 现在我们已经设置好所有参数,确保勾选了Play On Awake,这样爆炸在创建的那一刻就会开始,并取消勾选Looping,这样它就不会永远播放。如果你想在这个时候测试你的粒子系统,可以查看当选择任何粒子系统时,在Scene窗口右下角出现的StopSimulatePause按钮。这些按钮就像你的音乐播放器按钮一样,控制粒子系统的播放。

  13. 如果我们现在开始创建爆炸效果,它们在生成初始粒子群后会仅仅停留在场景中,尽管玩家永远看不到它们。这就是为什么我们需要一个新的脚本来在它们完成作用后摆脱它们。创建一个新的脚本,并将其命名为Explosion

  14. 这个脚本有一个单一的变量,即跟踪表示其存在的粒子系统:

    public ParticleSystem particles;
    
    • 1
  15. 它也只有一个函数。Update函数每一帧都会检查粒子系统是否存在或者是否已经停止播放。在任一情况下,整体对象都会被销毁,这样我们可以节省资源:

    public void Update() {
      if(particles == null || !particles.isPlaying)
        Destroy(gameObject);
    }
    
    • 1
    • 2
    • 3
    • 4
  16. 然后,我们需要将我们的新脚本添加到Explosion对象中。同时,将Particle System组件拖到Script组件中的Particles槽位。

  17. 为了让爆炸声能被听到,我们还需要在Explosion对象上添加一个Audio Source组件。

  18. 确保勾选了其Play On Awake选项。为了让声音在 3D 空间中有意义,将Spatial Blend属性设置为1。同时,设置为Linear Rolloff,并将Max Distance设置为50,这样我们可以听到它。

  19. 我们的香蕉拥有和汽车一样的爆炸声音是没有意义的。相反,我们有一个很好的小爆裂声,这将使最终效果与那些仅仅减少香蕉健康值的效果区分开来。为此,在Audio Source组件的AudioClip槽位上设置BananaPop音频文件。

  20. 在我们设置好所有爆炸参数后,使用Explosion对象创建一个新的预制体,并将其从场景中删除。

  21. 接下来,我们需要更新BananaBounce脚本,当它失去健康时实际生成爆炸效果。现在打开它。

  22. 首先,在脚本开始部分添加一个新的变量。这将简单地跟踪我们希望在香蕉失去健康后生成的预制体:

    public GameObject explosion;
    
    • 1
  23. 接下来,我们需要在Touched函数中使用Destroy函数后立即添加一行。这行代码仅仅在香蕉的位置创建一个新的爆炸实例:

    Instantiate(explosion, transform.position, transform.rotation);
    
    • 1
  24. 最后,在Project面板中找到你的Banana预制体,并将Explosion预制体拖到新的Explosion槽位中。如果你不这样做,将永远不会创建爆炸效果,而且每当香蕉失去健康时 Unity 都会报错。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如前所述截图所示,我们已经创建了一个爆炸效果。在 Unity 旧的粒子系统的一些纹理的帮助下,我们让它看起来像真正的爆炸,而不是仅仅是一团彩色的球。我们还为爆炸效果添加了声音。结合粒子系统和音频源,我们可以创建许多效果,比如我们的爆炸效果,如果只使用其中一种,效果就会显得较弱。我们还更新了香蕉,使其在被玩家摧毁时产生爆炸。尝试调整香蕉音频的平衡,每次触摸香蕉之间的音量差异以及爆炸本身。我们通过粒子系统在视觉上和通过音频源在听觉上为玩家提供的信息越多,效果就会越好。

香蕉并不是这个世界上唯一可以爆炸的东西。在我们的第二款游戏中,我们摧毁的坦克只是消失了。尝试为《坦克大战》游戏添加一些新的爆炸效果。每次坦克被摧毁时,都应该以壮观的方式爆炸。此外,无论坦克的炮弹击中什么,炮弹往往会爆炸。尝试在炮弹射击点产生爆炸效果,而不是移动红色球体。这将给玩家更好的射击目标和感觉。

《愤怒的小鸟》游戏也可以加入一些爆炸效果,尤其是黑色的小鸟。每当有东西被摧毁时,都应该释放出一些粒子效果,并产生一些声响。否则,当物体突然消失时,游戏会看起来有些奇怪。

总结

在本章中,我们了解了 Unity 中的特效,特别是音频和粒子系统。我们从了解 Unity 如何处理音频文件开始。通过为球添加背景音乐和一些吱吱声,我们将所学内容付诸实践。然后我们继续了解粒子系统,并为球创建了尘埃轨迹。最后,我们将这两种技能结合在一起,为收集香蕉时创建爆炸效果。粒子系统和音频效果为游戏的最终润色和外观增添了很多。

在下一章中,我们将通过查看 Unity 中的优化来共同完善我们的游戏体验。我们将了解一些用于追踪性能的工具。我们还将创建自己的工具来追踪脚本特定部分的性能。我们将探讨资源压缩以及我们可以更改的其他点以最小化应用程序的占用空间。最后,将讨论在使用游戏和 Unity 时最小化延迟的关键点。

第九章:优化

在上一章中,我们学习了关于游戏特效的知识。我们为 Monkey Ball 游戏添加了背景音乐。我们还为我们的猴子创建了尘埃轨迹。通过结合音频效果和粒子系统,当玩家收集香蕉时我们创建了爆炸效果。这些共同丰富了游戏体验,使我们的游戏看起来非常完整。

在本章中,我们将探讨优化的各种选项。我们从应用程序占用空间着手,探讨如何减少它,然后进一步查看游戏性能,最后探索可能导致延迟的关键区域,以及如何减少它们的影响。

在本章中,我们将涵盖以下主题:

  • 最小化应用程序占用空间

  • 跟踪性能

  • 减少延迟

  • 遮挡剔除

在本章中,我们将同时处理我们的 Monkey Ball 和 Tank Battle 游戏。首先打开 Monkey Ball 项目来开始本章的学习。

最小化应用程序占用空间

游戏成功的关键之一在于游戏本身的大小。许多用户会迅速卸载那些看起来不必要的大的应用程序。此外,所有移动应用商店都根据应用程序本身的大小对游戏如何提供给用户设置了限制。熟悉缩小游戏大小的各种选项是控制游戏分发方式的关键。

当致力于最小化占用空间时,首先需要注意的是 Unity 在构建游戏时如何处理资源。只有那些在构建中至少一个场景中使用过的资源才会被实际包含在游戏中。如果资源不在场景本身或者不在场景中引用的资源中,那么它就不会被包含。这意味着你可以拥有资源的测试版本或不完整版本;只要它们没有被引用,它们就不会影响你游戏的最终构建大小。

Unity 还允许你以你需要的工作格式保存资源。当最终构建时,所有资源都会转换为适合其类型的适当版本。这意味着你可以将模型保存在与你的建模程序本机格式中,它们将在构建游戏时转换为 FBX 文件。否则,你可以将图像保存为 Photoshop 文件,或你工作的任何其他格式,并在构建游戏时适当转换为 JPG 或 PNG。

编辑器日志

当你准备好最终处理游戏的占用空间时,可以确切地找出导致游戏比预期更大的原因。在控制台窗口的右上角有一个下拉菜单按钮。这个菜单中有打开编辑器日志

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

编辑器日志是 Unity 在运行时输出信息的位置。这个文件会记录有关当前 Unity 编辑器版本的信息,执行对你的许可证的任何检查,并包含一些关于你导入的资源的详细信息。日志还将包含有关构建后游戏中包含的文件大小和资源的详细信息。以下屏幕截图显示了编辑器日志的一个示例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里,我们可以看到最终构建方面的细分。每个资源类别都有其大小以及占总构建大小的百分比。同时,我们还获得了一个列表,列出了实际包含在游戏中的每个资源,按文件大小进行了组织,在添加到构建之前。当你寻找可以缩小的资源时,这些信息会非常有用。

资源压缩

在模型、纹理和音频的导入设置窗口中,有一些影响导入资源的尺寸和质量的选项。通常,受影响的是质量的降低。然而,特别是在为移动设备开发游戏时,资源质量可以在达到计算机所需水平以下很多,而不会在设备上注意到差异。一旦你了解了每种资源类型可用的选项,你将能够就游戏的质量做出最佳决策。在使用这些选项中的任何一个时,寻找一个在引入不需要的伪影之前能最小化尺寸的设置。

模型

无论你使用什么程序或方法来创建你的模型,最终总会有一个顶点位置和三角形的列表,以及一些对纹理的引用。模型的大部分文件大小来自顶点位置列表。为了确保你的游戏中的模型具有最高质量,从你选择的建模程序开始。删除所有额外的顶点、面和未使用的对象。这不仅能让你在构建最终游戏时得到较小的文件,还能减少你在编辑器中的导入时间。

模型的导入设置窗口由三个页面组成,提供了更多调整质量的选项。每个页面标签对应于模型的相应部分,允许你微调每一个部分。

模型标签页

模型标签页上,你可以影响网格的导入方式。在优化模型使用方面,这里有许多关键选项。一旦你的游戏看起来和玩起来的效果如你所愿,你应该始终仔细查看这些设置,看看是否能让它们工作得更好:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以下是模型标签页中的各种设置:

  • 缩放因子文件缩放:这些选项允许您控制模型的默认视觉大小。文件缩放参数是 Unity 在导入模型时计算的大小。缩放因子参数允许您调整 Unity 在导入模型时应用的额外缩放。

  • 网格压缩:此选项允许您选择对模型应用多少压缩。压缩效果相当于合并顶点以减少必须为网格存储的细节总量。如果过度使用此设置,可能会在网格中引入不希望出现的异常。因此,应始终选择不会引入任何伪影的最高设置。

  • 读写启用:此选项仅在您想在游戏运行时通过脚本操作网格时有用。如果您从未用任何脚本接触网格,请取消勾选此框。尽管这不会影响最终构建的大小,但它会影响运行游戏所需的内存量。

  • 优化网格:此选项使 Unity 重新排序描述模型的三角形列表。此选项始终是一个好的选择,应该勾选。唯一可能需要取消勾选的情况是,如果您基于三角形的特定顺序操作游戏或网格。

  • 导入混合形状:混合形状与普通动画中的关键帧相似,但它们作用于网格细节本身,而不是骨骼的位置。通过取消勾选此框,您可以节省游戏和项目中的空间,因为 Unity 将不需要计算和存储它们。

  • 生成碰撞器:此选项几乎总是建议不勾选。此选项将为模型中的每个网格添加网格碰撞器组件。这些碰撞器在处理游戏中的物理时计算相对昂贵。如果可能,您应该始终使用一组明显更简单的盒子碰撞器球体碰撞器

  • 交换 UV:Unity 支持具有两组 UV 坐标的模型。通常,第一组用于普通纹理,第二组用于物体的光照图。如果您生成自己的光照图 UV,Unity 可能会识别错误的顺序。勾选此框将强制 Unity 改变它们的使用顺序。

  • 生成光照图 UV:仅当您处理需要静态阴影的物体时,才应使用此选项。如果物体不需要,这将会引入过多的顶点信息并增加资源的大小。

  • 法线:此选项用于计算或导入法线信息。法线被材质用于确定顶点或三角形面向的方向以及光照应该如何影响它。如果网格从未使用需要法线信息的材质,请确保将其设置为

  • 切线:这个选项用于计算或导入切线信息。切线被材质用于通过凹凸贴图和类似的特效来模拟细节。就像法线设置一样,如果你不需要它们,就不要导入它们。如果法线设置为,这个设置会自动变灰,并且不再导入。

  • 平滑角度:在计算法线时,这个选项允许你定义两个面之间的角度需要多接近,才能在它们共享的边缘上平滑着色。

  • 分割切线:这会导致你的网格在 UV 接缝处重新计算切线。这对于修复高细节模型中的一些光照不规则性非常有用。

  • 保持四边形:Unity 通常会将所有面转换为三角形进行渲染。如果你使用 DirectX 11 进行渲染,这个选项将保持你的面作为四边形进行镶嵌。

  • 导入材质:这个选项允许你控制导入模型时是否创建新材质。如果取消勾选,导入时不会创建新模型。

  • 材质命名:这允许你控制导入的模型命名任何新创建的材质的方式。

  • 材质搜索:Unity 可以使用多种方法来查找已经创建的模型上要使用的材质。本地材质文件夹选项只会在导入模型的旁边名为Materials的文件夹中查找。递归向上选项会从模型所在的文件夹以及通过父级向上的根资源文件夹中查找。全项目选项会在整个项目中搜索具有正确名称的材质。

“绑定”标签页

如以下截图所示,动画绑定调整的选项非常少:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在优化你的动画绑定时,你真正需要记住的只有两件事。第一,如果资源不进行动画处理,那么就不要导入它。将动画类型设置为,Unity 就不会尝试导入绑定或任何无用的动画。第二件需要记住的事情是移除所有不必要的骨骼。一旦导入 Unity,删除那些实际上对动画或角色没有影响的绑定中的所有对象。Unity 可以将你可能用于动画的反向运动学转换为正向运动学,因此在 Unity 启动后,可以删除用于它的引导。

那里的优化游戏对象复选框实际上并不帮助游戏的整体优化。它只是在层次窗口中隐藏额外的绑定对象,这样你就不必处理它们。当在编辑器中处理复杂的绑定时,这个复选框也可以非常有帮助。

“动画”标签页

绑定标签一样,如果模型没有动画,不要导入动画。在首次导入资源时取消勾选导入动画复选框,可以防止在 Unity 中向你的GameObject组件添加任何额外的组件。此外,如果任何额外的动画意外地被添加到你的最终构建中,它们可能会迅速使你的应用程序变得过大。以下截图突出了动画标签:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 动画压缩:此选项调整 Unity 处理动画中多余关键帧的方式。在大多数情况下,默认选项效果很好。可用的各种选项如下:

    • 关闭:只有当你需要高精度动画时,才应使用此选项。这是最大且成本最高的设置选择。

    • 关键帧减少:此选项将根据以下错误设置减少动画使用的关键帧数量。本质上,如果一个关键帧对动画没有明显的影响,它将被忽略。

    • 最佳:此选项与上一个选项相同,但此外它还会压缩动画的文件大小。然而,在运行时,动画仍然需要与上一个选项相同的处理器资源来进行计算。

  • 旋转误差:此选项是在执行关键帧减少时,关键帧之间将被忽略的度数差。

  • 位置误差:此选项是在执行关键帧减少时,关键帧之间将被忽略的移动距离。

  • 缩放误差:此选项是在执行关键帧减少时,关键帧之间将被忽略的动画大小调整量。

纹理

很难想象一个高质量的游戏里面没有大量的图像。纹理有一系列选项来控制在使用游戏时保留多少细节。通常,最好选择不会在图像中引入明显瑕疵的最低质量设置。此外,最好使用大小为 2 的幂次的纹理以提高处理速度。而且,很少有处理器能够处理大于1024像素大小的纹理。通过将图像大小控制在或低于这个尺寸,你可以在最终游戏中节省大量的内存和空间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 纹理类型:此选项影响图像将被视为哪种类型的纹理。最好选择最适合图像预期用途的类型。以下选项展示了可以使用各种类型的纹理:

    • 纹理:此选项是在处理 3D 游戏时最常见和默认的设置。这应该用于你的普通模型纹理。

    • 法线图:这个选项用于特殊效果,如凹凸贴图。使用这种类型纹理的材料还需要从模型的导入设置中获取法线和切线信息。

    • 编辑器 GUI 和旧版 GUI:除非你在使用特殊的编辑器脚本或其他特殊情况,否则你不会使用这个设置。这非常类似于精灵设置。

    • 精灵(2D 和 UI):这个选项在处理 2D 游戏时是最常见和默认的设置。这应该始终用于你的平面 2D 角色和 UI 元素。

    • 光标:这个设置对我们的 Android 平台来说并不是特别相关。它允许你创建自定义鼠标指针,这对于大多数 Android 设备来说并不常见。

    • 立方体贴图:当你在处理自定义反射或天空盒类型的材质时,你的图像应该使用这个选项。这会自动将图像环绕,使其像球面或立方体的边缘一样重复。

    • Cookie:这些纹理用于灯光上,它们改变光线从光源物体的发射方式,就像我们用于坦克车头灯的那种。

    • 光照图:我们在坦克大战游戏中使用了 Unity 的光照图系统。然而,这个系统并不总是适用于所有情况。因此,当你需要在 Unity 外部制作自定义光照图时,请选择这个选项。

    • 高级:这个选项让你能够完全控制所有与导入图像相关的设置。只有当你对你的纹理有特殊用途或需要精确控制它们时,你才需要这个设置。

  • 读写启用:当纹理类型设置为高级时,此复选框可用。只有当你计划在游戏运行时通过脚本操作纹理时,才应该勾选此项。如果未勾选,Unity 不会在 CPU 上维护数据副本,从而为游戏的其他部分释放内存。

  • 生成 Mip Maps:这个选项是另一个高级设置,它允许你控制纹理较小版本的创建。当纹理在屏幕上显示得很小的时候,这些较小版本的纹理就会被使用,从而减少绘制纹理及其在屏幕上使用的对象所需的处理量。

  • 过滤模式:这个选项适用于所有纹理类型。它影响当你非常接近图像时图像的显示效果。点过滤会使图像看起来块状化,而双线性三线性则会模糊像素。通常,点过滤是速度最快的模式;三线性是速度最慢的模式,但能提供最佳视觉效果。

  • 最大尺寸:此选项调整图像在游戏中使用时可以有多大。这允许你处理非常大的图像,但以适当的小尺寸导入到 Unity 中。一般来说,大于1024的值都不是好选择,不仅因为内存需求增加,而且由于大多数移动设备根本无法处理更大的贴图。通常,1024 大小的纹理应该保留给你的主要角色和其他非常重要物体。对于中等和低重要性物体,在移动设备上 256 大小表现良好。对于你的所有物体,如果能将它们的纹理合并到共享的 1024 纹理中,它们对游戏的影响会比它们有单独的小纹理要小。选择尽可能小的尺寸将大大影响最终构建中纹理的占用空间。

  • 格式:此选项调整图像的导入方式以及每个像素可以保留的细节量。压缩格式最小,而真彩提供最多的细节。

音频

为游戏提供高品质的声音总是会增加游戏最终的大小。音频是游戏不可或缺的资产之一,但合适的包含水平可能难以把握。在音频程序中处理声音时,尽量保持简短,以减小其大小。此外,要考虑到大多数玩家并没有高级耳机或扬声器来听你的音频,因此在他们注意到差异之前,音频质量可以大幅度降低。音频导入设置都会影响它们在构建大小中的占用空间或运行游戏所需的内存。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 强制单声道:此设置将多声道音频转换为单声道。虽然大多数设备技术上能够播放立体声,但它们并不总是具有让声音产生差异所需的多个扬声器。勾选此框可以显著减小音频文件的大小,通过将所有声道合并为单个较小的声道。多声道音频文件用于根据声音来自哪个扬声器来制造方向感的错觉。这实际上需要为每个扬声器使用单独的音效文件。单声道音频文件对所有扬声器使用相同的音效文件,因此在游戏中需要的数据和空间要少得多。

  • 后台加载预加载音频数据:这两个设置共同定义音频信息的加载和准备播放时间。后台加载参数决定游戏是否在其他游戏数据加载前等待文件加载完成。对于长或大的文件,如背景音乐,勾选此框是个好主意。预加载音频数据参数决定文件是否应尽快加载。对于你马上需要使用的任何音频剪辑,应该勾选这个选项。

  • 加载类型:此设置影响游戏运行时,系统内存将使用多少来处理音频文件的加载。加载时解压缩选项使用最大内存,最适合小而短的声音。内存中压缩选项仅在播放时解压缩文件,使用中等数量的内存,最适合中等大小的文件。流式传输选项意味着只有当前正在播放的文件部分存储在运行时内存中。这就像从互联网上流式传输视频或音乐。这个选项最适合大文件,但一次应该只由少数几个使用。

  • 压缩格式:这决定了要对音频文件应用哪种数据缩减,使其足够小以包含在游戏中。PCM格式将保留大部分原始音频,因此文件大小也将是最大的。ADPCM格式将提供中等程度的压缩,但也会因此降低一些质量。Vorbis格式可以为你提供尽可能小的文件大小,但以最大程度降低质量为代价。

  • 质量和采样率设置:这些控制当你应用前一个选项的压缩时,将保留多少细节。如果文件大小仍然过大,你可以降低整体质量以使其在可接受范围内。然而,降低质量会牺牲声音质量。在目标设备上出现可听见的伪迹之前,始终寻求最低的设置。

玩家设置

通过转到 Unity 的工具栏,导航到编辑 | 项目设置 | 玩家,打开你的游戏的玩家设置窗口。在针对 Android 的平台特定设置中,我们在其他设置下还有几个选项,这些选项将影响我们游戏的最终大小和速度。

渲染

渲染设置组控制你的游戏如何在屏幕上绘制游戏。这控制了使用的光照和阴影计算类型。它还允许你优化绘制构成游戏场景的许多对象所需的计算数量。以下是渲染窗口的截图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

渲染窗口中看到的设置如下:

  • 渲染路径:这一组选项主要控制光照和阴影渲染的质量。渲染路径下的选项如下:

    • 正向渲染:这将是你的最常见设置。它支持来自单个方向光的实时阴影。这个选项是 Unity 中渲染光照的正常基准。

    • 延迟渲染:这将为你提供最高质量的光照和阴影,但系统处理它的成本最高。并非每个系统都能支持它,而且它恰好是 Unity Pro 独有的功能。

    • 传统顶点光照:这种渲染方法是旧系统的一部分。它也是处理成本最低的方法。这种方法没有实时阴影,光照计算也高度简化。较旧的机器和移动设备将默认使用此模式。

    • 传统延迟渲染(光照预通过):这种方法也是旧系统的一部分。较新的延迟方法对此进行了高度改进,通常来说,不应当使用这种方法。只有在有特殊案例或需要支持特定平台时,你才需要选择这种方法。

  • 多线程渲染:运行程序的过程和步骤系列称为线程。可以启动许多这样的线程,并让它们同时处理程序的不同部分。Unity 利用了编程的这一点,以提高渲染系统的速度和质量。然而,这需要一个更强大的处理器才能有效运行。

  • 静态批处理:这是 Unity Pro 的一个功能,通过将标记为静态的相同对象分组,可以显著提高渲染速度。对于每组,它然后在一个地方渲染一个对象,而不是单独渲染每个对象。这个设置可能会增加最终构建的大小,因为 Unity 需要保存关于静态对象的额外信息以实现这一功能。

  • 动态批处理:这与静态批处理的工作方式相同,但有两个主要区别。首先,它适用于 Unity Pro 和 Basic 用户。其次,它将未标记为静态的对象分组。

  • GPU 蒙皮:这个设置对于较旧的移动设备不太适用,它更多地用于最新的移动设备和其他同时具有 CPU 和 GPU 的系统。这允许通常在网格上进行的计算,如通过骨骼进行动画和变形的计算,在 GPU 上进行而不是 CPU。这将释放资源以处理游戏的其他部分,为玩家提供最佳体验。

优化

优化设置组允许你调整 Unity 编译项目及涉及资源的方式。在接近游戏最终构建时,每个设置都应该仔细考虑。总的来说,这些设置有可能极大地影响你的游戏运行效果。以下是优化窗口的截图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • API 兼容性级别:此设置决定了最终构建中包含哪一组.NET 函数。“.Net 2.0"选项将包括所有可用的函数,产生最大的占用空间。”.Net 2.0 子集"选项是函数的一小部分,仅包括你的编程最有可能使用的函数。除非你需要一些特殊功能,否则应始终选择".Net 2.0 子集"选项。

  • 预烘焙碰撞网格:此选项通过将物理计算从场景加载移动到游戏构建来节省你加载关卡时的时间。这意味着你的构建大小会增大,但处理速度会降低。

  • 预加载着色器:当一个网格使用尚未在游戏场景中使用的新着色器时,系统需要处理并计算该着色器将如何渲染物体。此选项将在场景开始时处理该信息,以避免在尝试进行计算时可能导致游戏停滞。

  • 预加载资源:此选项与之前的选项相同,但它是为着色器以外的资源和预制件而设的。当你首次实例化一个对象时,它需要被加载到内存中。这将改变为在场景开始时加载此列表中的所有资源。

  • 剥离级别:此设置是 Unity Pro 版独有的功能。它允许你在编译前通过移除所有多余的代码来减少最终构建的大小。系统功能被分组到所谓的库中以便于引用。"Strip Assemblies"选项会从最终构建中移除未使用的库。"使用微型的 mscorlib"选项执行与前一选项相同的操作,但使用的是库的最小化形式。尽管这个库显著较小,但它可供你的代码使用的函数较少。然而,除非你的游戏非常复杂,否则这不应造成影响。

  • 启用内部分析器:此选项允许你获取关于游戏在设备上运行的信息。这确实会在游戏运行时处理信息的过程中引入一些开销,但其影响小于 Unity 编辑器引入的开销。通过在命令提示符中使用adb logcat命令可以获取这些信息。

  • 优化网格数据:此设置将从所有网格中移除任何未由应用在它们上面的材质使用的额外信息。这包括法线切线以及其他一些信息。它还会导致构成网格的三角形数据为最佳处理和渲染而重新排序。除非你有非常特殊的情况,否则这是一个始终应该勾选的好选项。

跟踪性能

Unity 为我们提供了许多工具,让我们可以确定游戏运行得有多好。我们将要介绍的第一款工具对 Unity 专业版和基础版用户都是现成的。然而,这些信息相当有限,尽管它仍然有用。第二款工具仅对 Unity 专业版用户开放。它提供了更多关于性能的详细信息和数据。最后,我们将创建自己的工具,让我们可以详细查看脚本的性能。

编辑器统计

游戏窗口的右上角,有一个标有统计的按钮。点击这个按钮会打开一个窗口,为我们提供有关游戏运行情况以及处理所需时间的信息。这个窗口中的大多数信息关注的是游戏渲染的好坏,主要涉及到当前屏幕上的对象数量、正在动画的对象数量以及它们占用的内存量。此外,还有一些关于游戏中声音以及可能发生的任何网络流量的信息。以下截图显示了统计标签:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 音频部分关注场景中播放的各种音频剪辑。它包含有关游戏音量以及处理所有这些音频所需的内存信息。音频部分包括以下详细信息:

    • 级别:这是游戏音量的大小,以分贝为单位。它实际上只是一种特殊的音量测量形式,并代表游戏中正在播放的每个音频剪辑的总和。

    • DSP 负载:这是处理场景中数字音频剪辑的成本。它表示为游戏使用的内存的百分比。

    • 剪辑:这是由于系统过载而没有播放的音频文件百分比。根据设备处理器的性能,设备一次只能播放有限数量的音频剪辑。根据检查器面板中音频源组件的优先级设置,任何额外的音频剪辑都会被忽略。

    • 流加载:这是处理任何必须边播放边流的音频所需的成本。它同样是使用内存的百分比。

  • 图形部分关注的是游戏的渲染以及进行此操作所需的内存。它包含有关游戏运行速度、正在渲染的对象数量以及对象细节程度的信息。大多数时候,在使用统计窗口时,你会查看这个部分。此分组标题右侧的FPS值是估计游戏运行速度的一个很好的指标。这是每秒处理的帧数,后面是处理游戏中单个帧所需的时间(毫秒)。图形部分包括以下详细信息:

    • CPU:这一部分分为两个小节。主要的部分是处理运行游戏所使用代码所需的时间。渲染线程的部分是在屏幕上绘制游戏所有部分所需的时间。结合起来,你可以了解到游戏中运行最耗时的部分。

    • 批处理:当使用玩家设置渲染组内的静态动态批处理时,第一个数字表示为批渲染过程创建了多少组,通过批处理节省的值是因为批处理过程而避免的绘制调用次数。节省的越多,意味着在屏幕上绘制游戏所需的工作量越少。

    • 三角形:最终,3D 图形中的每个模型都是由一系列三角形组成的。这个值是场景中相机看到并渲染的三角形总数。三角形越少,图形处理在屏幕上绘制模型时所需的工作量就越少。

    • 顶点:模型文件中的大部分信息与每个顶点的世界位置、法线方向和纹理位置有关。这个值是相机看到并渲染的顶点总数。每个模型顶点的数量越少,计算渲染的速度就越快。

    • 屏幕:这是当前游戏窗口的宽度和高度,以像素为单位。同时显示该尺寸渲染所需的内存量。较小的尺寸会减少游戏的细节,但也使得游戏更容易渲染。

    • SetPass 调用:这基本上是绘制场景中所有内容在屏幕上时,需要调用着色器不同部分的次数。它更多地基于场景中不同材质的数量,而不是物体的数量。

    • 阴影投射器:当你使用实时阴影时,会用到这个统计信息。实时阴影是昂贵的。如果可能,不应在移动设备上使用。然而,如果你必须使用它们,请尽量减少投射阴影的物体数量。仅限于那些用户能够看到阴影的大物体。特别是小型静态物体不需要投射阴影。

    • 可见的蒙皮网格:这是当前在相机视图中带有骨骼的物体总数。蒙皮网格通常是你的角色以及任何会动的东西。由于需要额外的计算来使它们移动和随动画变化,所以它们比静态网格更昂贵。

    • 动画:这只是场景中正在播放的动画总数。

  • 只有当在多人游戏中连接到其他玩家时,网络统计信息组才会可见。这些信息通常包括游戏连接的人数以及这些连接的速度。

性能分析器

分析器窗口,在 Unity 的工具栏中通过导航到窗口 | 分析器找到,是分析游戏运行情况的一个很好的工具。它为我们提供了系统每个部分及其工作量的多彩分解。这个工具唯一真正不幸的部分是它仅对 Unity Pro 用户可用。以下截图显示了分析器窗口:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先打开分析器窗口,然后我们可以在窗口中玩游戏,并观察工具为我们提供相当详细的正在进行中的情况分解。我们可以点击任何点,并在窗口底部查看有关该帧的详细信息。提供的信息与您点击的点的特定信息有关,如CPU 使用率渲染内存等。

CPU 使用率信息在尝试查找游戏中处理时间过长的部分时特别有用。处理成本的高峰非常容易凸显出来。点击一个高峰,我们可以看到游戏中的每个部分在使这一帧变得昂贵时的分解情况。对于这些部分中的大多数,我们可以深入到导致问题的确切对象或函数。然而,我们只能定位到函数级别。仅仅因为我们知道代码中问题的大概位置,分析器窗口并不会告诉我们具体是函数的哪部分导致了问题。

为了实际工作,分析器需要挂接到游戏的每个部分。这会在游戏速度上引入一些额外的成本。因此,在分析提供的信息时,最好考虑相对成本,而不是将每个成本视为一个确切值。

跟踪脚本性能

Unity 提供的所有这些工具都很好,但它们并不总是正确的解决方案。Unity 基础用户无法访问分析器窗口。此外,分析器编辑器统计相对泛化。我们可以通过分析器获得更多细节,但信息并不总是足够,除非你不得不浏览一堆菜单。在下一部分中,我们将创建一个特殊的脚本,能够跟踪任何脚本特定部分的性能。它绝对应该成为您开发工具包中的常备部分。让我们按照以下步骤在我们的 Monkey Ball 游戏中创建脚本:

  1. 首先,我们需要一个特殊的类来跟踪我们的性能统计数据。为此,创建一个新脚本,并将其命名为TrackerStat

  2. 要开始这个脚本,我们需要启用与各种 GUI 元素交互的能力。转到脚本的最顶部,并在以using开头的其他行旁边添加这一行:

    using UnityEngine.UI;
    
    • 1
  3. 接下来,我们需要更改类定义行。我们不希望或需要扩展MonoBehaviour类。因此,找到以下代码行:

    public class TrackerStat : MonoBehaviour {
    
    • 1

    然后,将其更改为以下代码:

    public class TrackerStat {
    
    • 1
  4. 这个脚本从四个变量开始。第一个变量将用作 ID,通过提供不同的键值,我们可以同时跟踪多个脚本。第二个变量将跟踪被跟踪代码段平均所需的时间。第三个变量只是被跟踪代码被调用的总次数。第四个变量是代码执行所需的最长时间:

    public string key = "";
    public float averageTime = 0;
    public int totalCalls = 0;
    public float longestCall = 0;
    
    • 1
    • 2
    • 3
    • 4
  5. 接下来,我们还有两个变量。它们将实际跟踪脚本执行所需的时间。第一个变量包括跟踪开始的时间。第二个变量是一个标记,表示跟踪已开始。

    public float openTime = 0;
    public bool isOpen = false;
    
    • 1
    • 2
  6. 本脚本的第三组也是最后一组变量用于存储实际显示我们状态信息的 Text 对象的引用:

    private Text averageLabel;
    private Text totalLabel;
    private Text longestLabel;
    
    • 1
    • 2
    • 3
  7. 本脚本的第一个函数是Open。当我们想要开始跟踪一段代码时,会调用这个函数。它首先检查代码是否已经被跟踪。如果是,那么它会使用Debug.LogWarning控制台窗口发送警告。接下来,它设置标记表示代码正在被跟踪。最后,该函数通过使用Time.realtimeSinceStartup跟踪调用它的时刻,其中包含自游戏开始以来的实际秒数。

    public void Open() {
      if(isOpen) {
        Debug.LogWarning("Tracking is already open. Key: " + key);
      }
    
      isOpen = true;
      openTime = Time.realtimeSinceStartup;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  8. 下一个函数Close起到了前一个函数的相反作用。当我们要跟踪的代码结束时会被调用。跟踪应该停止的时间被传递给它。这是为了尽量减少执行多余的代码。与上一个函数一样,它会检查是否正在跟踪,如果该函数没有被跟踪,它会发出另一个警告并提前退出。接下来,通过将isOpen标志设置为false来清除它。最后,计算自跟踪开始以来的时间,并调用AddValue函数。

    public void Close(float closeTime) {
      if(!isOpen) {
        Debug.LogWarning("Tracking is already closed. Key: " + key);
        return;
      }
    
      isOpen = false;
      AddValue(closeTime – openTime);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  9. AddValue函数接收callLength,即跟踪的代码段执行所需的时间长度。然后它使用一些计算将值添加到averageTime中。接下来,该函数将当前的longestCall与新的值进行比较并更新它,如果新的值大于当前的值。然后函数增加totalCalls,最后在屏幕上更新显示新值的文本。

    public void AddValue(float callLength) {
      float totalTime = averageTime * totalCalls;
      averageTime = (totalTime + callLength) / (totalCalls + 1);
    
      if(longestCall < callLength) {
        longestCall = callLength;
      }
    
      totalCalls++;
    
      averageLabel.text = averageTime.ToString();
      totalLabel.text = totalCalls.ToString();
      longestLabel.text = longestCall.ToString();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  10. 我们的脚本中最后一个函数CreateTexts在我们首次创建此类实例以跟踪某段代码时被调用。它首先计算 GUI 元素的垂直位置。通过使用我们将在下一个脚本中创建的ScriptTracker.NewLabel函数,我们可以节省一些工作量;它会自动处理创建和基本设置显示状态信息的 Text 对象。我们只需传递一个名称以在层次结构窗口中使用它,并在它给我们新对象时设置位置和大小。

    public void CreateTexts(int position) {
      float yPos = -45(30 * position);
    
      Text keyLabel = ScriptTracker.NewLabel(key + ":Key");
      keyLabel.text = key;
      keyLabel.rectTransform.anchoredPosition = new Vector2(75, yPos);
      keyLabel.rectTransform.sizeDelta = new Vector2(150, 30);
    
      averageLabel = ScriptTracker.NewLabel(key + ":Average");
      averageLabel.rectTransform.anchoredPosition = new Vector2(200, yPos);
      averageLabel.rectTransform.sizeDelta = new Vector2(100, 30);
    
      totalLabel = ScriptTracker.NewLabel(key + ":Total");
      totalLabel.rectTransform.anchoredPosition = new Vector2(200, yPos);
      totalLabel.rectTransform.sizeDelta = new Vector2(100, 30);
    
      longestLabel = ScriptTracker.NewLabel(key + ":Longest");
      longestLabel.rectTransform.anchoredPosition = new Vector2(200, yPos);
      longestLabel.rectTransform.sizeDelta = new Vector2(100, 30);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  11. 接下来,我们需要创建另一个新脚本,并将其命名为ScriptTracker。这个脚本将允许我们进行实际性能跟踪。

  12. 正如我们对上一个脚本所做的那样,我们需要在脚本顶部的其他using行旁边添加一行,以便脚本可以创建和与 GUI 对象交互。

    using UnityEngine.UI;
    
    • 1
  13. 这个脚本从一个单一变量开始。这个变量维护当前正在跟踪的所有状态。注意这里使用的static;它允许我们从游戏中的任何地方轻松更新列表:

    private static TrackerStat[] stats = new TrackerStat[0];
    
    • 1
  14. 本脚本的第一个函数Open允许我们开始跟踪代码的执行。它使用static标志,因此任何脚本都可以轻松调用该函数。一个key值被传递给函数,允许我们将跟踪调用分组。函数首先创建一个变量来保存要开始跟踪的状态的索引。接下来,它遍历当前的状态集以找到匹配的key值。如果找到,将更新index变量并退出循环。

    public static void Open(string key) {
      int index = -1;
    
      for(int i=0;i<stats.Length;i++) {
        if(stats[i].key == key) {
          index = I;
          break;
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  15. Open函数继续检查是否找到了状态。只有当我们遍历完当前状态列表并且找不到匹配的key时,index变量才会小于零。如果没有找到,我们首先检查状态列表是否为空,如果为空,我们通过调用CreateLabels函数创建一些显示标签。然后我们调用AddNewStat来设置新的跟踪状态。我们很快就会创建这两个函数。然后index被设置为新的状态的索引。最后,通过使用状态的Open函数触发状态开始跟踪。

      if(index < 0) {
        if(stats.Length <= 0) {
          CreateLabels();
        }
    
        AddNewStat(key);
        index = stats.Length1;
      }
    
      stats[index].Open();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  16. AddNewStat函数接收要创建的状态的键。它首先将状态列表存储在一个临时变量中,并将状态列表的大小增加一个。然后,每个值从临时列表转移到更大的状态列表中。最后,创建一个新状态,并将其分配到状态列表的最后一个槽位。然后,设置key并调用其CreateTexts函数,以便它可以在屏幕上显示。

    private static void AddNewStat(string key) {
      TrackerStatp[] temp = stats;
      stats = new TrackerStat[temp.Length + 1];
    
      for(int i=0;i<temp.Length;i++) {
        stats[i] = temp[i];
      }
    
      stats[stats.Length1] = new TrackerStat();
      stats[stats.Length1].key = key;
      stats[stats.Length1].CreateTexts(stats.Length1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  17. 接下来,我们有Close函数。这个函数接收要关闭的状态的键值。它首先找到调用函数的时间,以最小化将跟踪的额外代码量。然后通过遍历状态列表找到匹配的key。如果找到,将调用状态的Close函数并退出。如果没有找到匹配项,将调用Debug.LogError控制台窗口发送错误消息。

    public static void Close(string key) {
      float closeTime = Time.realtimeSinceStartup;
    
      for(int i=0;i<stats.Length;i++) {
        if(stats[i].key = key) {
          stats[i].Close(closeTime);
          return;
        }
      }
    
      Debug.LogError("Tracking stat not found. Key: " + key);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  18. CreateLabels函数处理屏幕上文本标签的创建,这样我们可以轻松地了解每一段显示信息的含义。就像我们之前的脚本一样,它使用NewLabel函数来处理文本对象的基本创建,传递一个在层次结构窗口中显示的名称。然后设置要在屏幕上显示的文本,将其定位在屏幕左上角,并设置其大小。

    private static void CreateLabels() {
      Text keyLabel = NewLabel("TrackerLabel:Key");
      keyLabel.text = "Key";
      keyLabel.rectTransform.anchoredPosition = new Vector2(75, -15);
      keyLabel.rectTransform.sizeDelta = new Vector2(150, 30);
    
      Text averageLabel = NewLabel("TrackerLabel:Average");
      averageLabel.text = "Average";
      averageLabel.rectTransform.anchoredPosition = new Vector2(200, -15);
      averageLabel.rectTransform.sizeDelta = new Vector2(100, 30);
    
      Text totalLabel = NewLabel("TrackerLabel:Total");
      totalLabel.text = "Total";
      totalLabel.rectTransform.anchoredPosition = new Vector2(275, -15);
      totalLabel.rectTransform.sizeDelta = new Vector2(50, 30);
    
      Text longestLabel = NewLabel("TrackerLabel:Longest");
      longestLabel.text = "Longest";
      longestLabel.rectTransform.anchoredPosition = new Vector2(350, -15);
      longestLabel.rectTransform.sizeDelta = new Vector2(100, 30);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
  19. 此脚本的最后一个静态函数是NewLabel函数。它处理我们在脚本其余部分使用的每个文本对象的基本创建。它首先尝试查找画布对象,如果找不到则创建一个新的。为了使用我们的文本对象,我们需要画布,这样它们实际上才能被绘制。

    public static Text NewLabel(string labelName) {
      Canvas canvas = GameObject.FindObjectOfType<Canvas>();
      if(canvas == null) {
        GameObject go = new GameObject("Canvas");
        go.AddComponent<RectTransform>();
        canvas = go.AddComponent<Canvas>();
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  20. 接下来,NewLabel函数通过使用传递给它的名称创建一个新的GameObject,并将其设置为画布的子对象。然后它添加了RectTransform组件,以便它可以在 2D 空间中定位自己,并将其锚定在左上角。然后给文本对象一个CanvasRenderer组件,这样它实际上可以在屏幕上绘制,并添加一个Text组件,这样它实际上就是一个文本对象。然后我们使用Resources.GetBuiltinResource函数为文本对象获取 Unity 的默认Arial字体,再将其返回给函数的调用者。

    GameObject label = new GameObject(labelName);
    label.transform.parent = canvas.transform;
    
    RectTransform labelTrans = label.AddComponent<RectTransform>();
    labelTrans.anchorMin = Vector2.up;
    labelTrans.anchorMax = Vector2.up;
    
    label.AddComponent<CanvasRenderer>();
    Text textComp = label.AddComponent<Text>();
    textComp.font = Resources.GetBuiltinResource(typeof(Font), "Arial.ttf") as Font;
    return textComp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  21. 要测试这些脚本,请打开你的BananaBounce脚本。在Update函数的开始处,添加以下行以开始跟踪运行所需的时间:

    ScriptTracker.Open("BananaBounce Update");
    
    • 1
  22. Update函数的末尾,我们需要用相同的键调用Close函数:

    ScriptTracker.Close("BananaBounce Update");
    
    • 1
  23. 最后,启动游戏并查看结果(如下截图所示):外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们创建了一个用于测试代码特定部分的工具。通过将任何代码片段包裹在函数调用中,并发送一个唯一的 ID,我们可以确定执行代码需要多长时间。通过平均调用脚本,并包裹代码的不同部分,我们可以确切地确定脚本中哪些部分需要最长的时间来完成。我们还可以找出代码部分是否被调用得太多次。这两种情况都是优化处理和减少延迟的理想点。

在部署游戏之前,请确保删除与此工具的所有引用。如果它被留在最终关卡中,可能会增加不必要的 CPU 负载。这种对游戏的不良影响可能导致游戏无法玩。一定要记得清除那些仅用于编辑器调试的工具使用情况。

最小化延迟

延迟是用于描述比预期慢的应用程序的一个模糊概念。它最常见于应用程序的帧率中。大多数游戏以大约 60 FPS 的速度运行,如果降至 30 FPS 或更低,则被认为是延迟的。然而,延迟及其问题更深层次,包括输入响应性、网络连接以及文件读写等问题。作为开发者,我们不断努力提供尽可能高的体验质量,同时保持用户期望的速度和响应性。这基本上取决于用户设备上的处理器是否能够处理提供游戏体验的成本。游戏中的几个简单对象将导致快速处理,但几个复杂对象将需要最多的处理。

遮挡剔除

遮挡对于拥有大量对象的游戏来说非常有效。在其基本形式中,任何在摄像机侧面或后面的内容都是不可见的,因此不会绘制。在 Unity Pro 中,我们可以设置遮挡剔除。这将计算摄像机实际可以看到的内容,并且不绘制任何被遮挡的视图。在使用这些工具时,必须达到一个平衡。计算不可见内容所需的成本需要小于直接绘制对象的成本。没有确切的数字可以表示一个场景可能需要多长时间来渲染。这完全取决于你所选择的渲染设置以及模型和纹理的细节。作为一个经验法则,如果你有许多经常被较大对象遮挡的小对象,那么选择遮挡剔除是正确的。

我们将为坦克大战游戏添加遮挡剔除,因为它是唯一一个有足够大的对象来经常遮挡视图的游戏。让我们按照以下步骤进行设置:

  1. 现在打开坦克大战游戏。如果你完成了挑战并添加了额外的碎片和障碍物,这一部分将对你特别有效。

  2. 通过转到 Unity 的工具栏并导航到窗口 | 遮挡剔除来打开遮挡窗口。这个窗口是修改与游戏中遮挡相关的各种设置的主要入口。不幸的是,这是一个仅限 Unity Pro 的功能。如果你在 Unity Basic 中尝试打开该窗口,除了在控制台中收到错误消息外,不会有任何结果。

  3. 切换到烘焙页面,我们可以查看与遮挡剔除相关的选项:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 最小遮挡体:这应该设置为可以阻挡其他物体视野的最小物体的尺寸。像大石头和房子这样的事物是很好的遮挡体。像家具或书籍这样的小物体通常太小,无法阻挡任何重要的视野。

    • 最小孔洞:这是场景中可以看到其他对象的最小缝隙。较小的值需要更详细的计算。较大的值成本较低,但更有可能导致对象随着玩家的移动而在视野中闪烁。

    • 背面阈值:这个设置让系统对可能位于其他对象内部的对象进行额外检查。值为100意味着不进行检查,从而节省计算时间。值为5将需要进行大量额外的计算,以确定所有对象相对于彼此的位置。

  4. 在当前阶段,对于我们来说,默认设置将工作得很好。你理想的情况是找到一组在渲染成本降低和计算应渲染内容成本之间平衡的设置。

  5. 为了让遮挡系统与动态对象一起工作,我们需要设置多个遮挡区域。要创建它们,请创建一个空的GameObject,并添加一个在 Unity 工具栏中通过导航到Component | Rendering | Occlusion Area可以找到的Occlusion Area组件。

  6. 你需要创建并操作这些对象。它们需要覆盖任何动态对象和相机可能存在的整个区域。为此,创建并定位足够的区域以覆盖我们游戏中的街道。它们的大小可以像使用Box Collider组件时一样编辑。你还可以使用区域每侧的小圆柱来操纵场域。确保它们足够高,以覆盖所有目标(如下面的截图所示):外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  7. 接下来,在Occlusion窗口底部点击Bake。Unity 编辑器右下角会出现一个进度条,它会告诉你计算还需要多长时间。这个过程通常会花费一些时间,特别是当你的游戏变得越来越复杂时。对于我们简单的坦克大战游戏,这个过程不会特别长。我们场景中内容很少,处理时间只需几秒钟。一个充满细节的大型关卡可能需要一整天来处理。

  8. 当烘焙过程完成后,Occlusion窗口将切换到Visualization标签,如果可以找到的话,应在你的Scene窗口中选择相机。如果没有,现在选择它。在Scene视图中,Unity 会给我们展示遮挡剔除是如何工作的。只有那些可以看到的对象是可见的,其余的将被关闭(如下面的截图所示):外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们已经了解了设置遮挡剔除的基本流程。我们查看了遮挡窗口并了解了那里可用的设置。遮挡剔除对于减少场景中的绘制调用数量非常有效。然而,这种减少需要与存储和检索遮挡计算的成本相平衡。通过选择适当的技术和合适的视单元格大小来实现这种平衡。现在尝试调整不同的值,找到一个可以在不过量提供信息的情况下提供适当细节的单元格大小。

减少延迟的小贴士

以下是一些处理和避免游戏中延迟的小贴士。不是所有这些都会适用于你制作的每一个游戏,但它们对于每个项目都是值得牢记的:

  • 在创建材质时,如果可能的话避免使用透明度。它们比正常的不透明材质更昂贵。此外,如果你避免使用它们,还可以省去处理深度排序的许多麻烦。

  • 每个对象使用一个材质。你的游戏中绘制调用越多,每帧渲染的时间就会越长。即使材质看起来并没有做什么,每个网格也会根据其上的材质进行一次绘制。特别是对于移动平台,保持每个对象一个材质,可以最小化绘制调用次数,最大化渲染速度。

  • 尽可能合并纹理。你制作的不是每个纹理都会利用到整张图像。只要有可能,就合并同一场景中对象的纹理。这最大化了图像的有效使用,同时减少了最终构建的大小和利用纹理所需的内存量。

  • 层级窗口中使用空的GameObject组件来分组对象。这虽然不是特定于减少延迟,但它会使你的项目更容易操作。特别是在大型复杂关卡中,你将能够减少在场景中搜索对象的时间,从而有更多时间制作优秀的游戏。

  • 控制台窗口是你的好朋友。在担心你的游戏不能运行之前,首先查看一下 Unity 中的控制台窗口或底部的栏。两者都会显示 Unity 对于你游戏当前设置可能有的任何抱怨。这里的消息非常适合指引你解决问题。如果你不确定这些消息想要告诉你什么,可以针对这些消息进行一次谷歌搜索,你应该能轻松地从众多 Unity 用户那里找到一个解决方案。如果你的代码似乎不起作用,而 Unity 也没有对此抱怨,使用Debug.Log函数向控制台打印消息。这将帮助你找到代码可能意外退出的地方或找到不是预期值的变量。

  • 设备测试很重要。在编辑器中工作固然好,但没有什么能比在目标设备上进行测试更好。当游戏在设备上运行时,你能更直观地感受到游戏的表现。编辑器总会引入一些额外的处理开销。此外,你用来工作的电脑通常会比你可能打算部署游戏的移动设备要强大。

总结

在本章中,我们了解了在 Unity 中进行优化的各种选择。首先,我们查看了一些用于减小游戏资产文件大小同时保持质量的设置。接下来,我们学习了一些影响整个游戏的设置。之后,我们探索了追踪游戏性能的选项。我们首先了解了一些由 Unity 提供的用于追踪性能的工具。然后,我们创建了自己的工具,详细追踪脚本性能。接着,我们查看了一些减少游戏中延迟的选项,包括利用遮挡剔除。现在我们知道了所有这些工具和选项,请回顾我们创建的游戏并进行优化。让它们尽可能做到最好。

在这本书中,我们学到了很多。我们从学习 Unity、Android 以及如何让它们协同工作开始。我们的旅程继续探索 Unity 的 GUI 系统,并创建了一个井字游戏。然后,在学习任何游戏都需要的基本资产的同时,我们开始创建一个坦克大战游戏。随着一些特殊相机效果和灯光的加入,我们的坦克大战游戏得到了扩展。通过引入一些敌人并让它们追逐玩家,我们完成了坦克大战游戏的制作。我们的猴子球游戏教会了我们如何在游戏中利用触摸和倾斜控制。在短暂离开这个游戏后,我们创建了一个类似愤怒的小鸟的克隆游戏,同时学习了物理知识以及与 Unity 的 2D 管线工作的选项。然后,我们回到猴子球游戏,通过增加声音和粒子效果来完善它。最后,我们的旅程以学习如何优化我们的游戏结束。感谢您阅读这本书。我们希望您在 Unity 和 Android 上创造那些您一直梦寐以求的精彩游戏的过程中,享受这段经历。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/爱喝兽奶帝天荒/article/detail/898113
推荐阅读
相关标签
  

闽ICP备14008679号