我如何在Unity中制作2D阴影

当独立游戏开发人员面对需要添加他对实现一无所知的功能时,首先想到的是什么? 当然,他将寻找那些已经走过这条路并费心写下自己经历的人的踪迹。 所以我前段时间做了,开始在游戏中创建阴影。 以文章,课程和指南的形式找到正确的信息并不困难。 但是,令我惊讶的是,我发现所描述的解决方案都不适合我。 因此,在意识到自己的想法之后,我决定向世界介绍一下。

值得提前警告的是,本文并不伪装成最后通guide指南或大师班。 我使用的方法可能不是通用的,远未达到最有效的方法,并且没有涵盖完整创建二维阴影的任务。 这是一个关于我经验不足的开发人员为了达到满足其要求的结果而必须采取的欺骗手段的故事。

结果本身在您面前:



实现目标的途径之细节正在等待您的切入。

有关游戏本身的一些知识
Dwarfinator是一款二维基础防御/侧滚动射击游戏,着眼于移动和台式机领域。 游戏玩法包括以两种交替模式对敌人的波进行系统性破坏-防御和追逐。 玩家的进步涉及通过改进和替换“坦克”的各种元素(例如武器,引擎和车轮)以及提高水平并学习主动和被动技能来抽出“坦克”。 环境的发展包括波浪中生物的数量不断增加,波浪中通过地点前进的新型敌人的增加以及几个地点的连续变化,每个地点都有自己的一组对手。

问题陈述


因此,在决定为游戏添加阴影时,我有:

  • 以两个精灵的形式定位,一个精灵显示在生物和其他实体的后面,第二个精灵显示在它们的前面;



  • 小怪兽和可破坏的静态物体,不断地被动画化,并由单独的精灵组成,数量从几到几十个不等;



  • 在大多数情况下,由一个精灵或一个粒子系统表示的,自己的和敌人的炮弹,在后一种情况下,不需要阴影;



  • 一个坦克,由几个部分组成,这些部分按照与暴民相同的方式组装;



  • 具有几个固定状态的墙,这些状态又是一组单独的精灵。



为此,需要最简单的阴影,重复对象的轮廓,并从单个固定光源投射。

同时,人们应该对生产力抱有敏锐的态度。 由于类型的特殊性及其实现的特殊性,大多数投射阴影的对象随时都可以直接位于屏幕上。 如果我们谈论游戏实体,那么它们的总数可以超过一百,而如果谈论单个精灵,它们的总数可以达到数千。

实作


实际上,主要发现是Dwarfinator大致上是2.5D游戏。 绝大多数对象存在于带有X和Y轴的二维空间中,而Z轴的使用却很少。 Y轴在视觉上是游戏的一部分,同时用于显示高度和深度,并以相同的方式分为虚拟的Y轴和Z轴,在这种情况下无法使用标准的Unity工具创建阴影。

但实际上,我不需要诚实的照明,足以为每个对象手动创建阴影。 因此,对我而言,最简单的事情是将其副本简单地放置在每个实体的后面,并在三维空间中旋转,以便模拟表面上的位置。 伪阴影的所有精灵都设置为黑色,同时保留了阴影所有者的层次结构,这使它可以由同一动画师与所有者同步进行动画处理。

这样的同步动画看起来像这样:



但是,阴影需要透明。 最简单的解决方案是为每个阴影精灵设置它。 但是,这种实现方式看起来并不令人满意-精灵彼此重叠,从而在覆盖位置形成了不太透明的区域。

下面的屏幕截图显示了几个半透明段的阴影外观。 所使用的阴影变形参数也是可见的:相对于父对象,沿X轴旋转-50度,沿Y轴旋转-140度,沿X轴缩放。



显而易见的是,应该将阴影作为固体对象施加在阴影上。 关于此主题的第一个实验是挂在相机阴影上,在RenderTexture中渲染该阴影,然后将其用作附加到Plane阴影父级的材质。 他已经可以设置透明度而没有任何问题。 阴影本身在画框外,以避免重叠的相机拍摄区域。 这种方法行得通,但事实证明,已经有几十个阴影引起严重的性能问题,这主要是由于舞台上的摄像头数量所致。 另外,许多动画假定单个暴民精灵在其根对象的框架内发生了显着运动,因此,应放置一个摄像头区域,该摄像头区域将在特定时间点显着超过实际图像的大小。

快速找到解决方案-如果您无法使用单独的相机绘制每个阴影-为什么不使用一台相机绘制所有阴影? 所有要做的就是将场景的单独区域放置在阴影下,略高于主摄像机的视场,将另一台摄像机定向到该区域,并在位置和其他实体之间显示其输出。

在下面,您可以看到此摄像机的输出示例:



这种实现带来的生产力损失要少得多,因此该解决方案被认为是可行的,并适用于所有生物,静态对象和外壳。 其次是精灵位置。 如先前所实现的,不可能在所有对象上使用一个精灵。 仅当对象完全平坦时,才使用对象的副本作为阴影。 即使在为小怪创建阴影时,也很明显,与沿第三坐标间隔的表面的接触点违反了阴影相对于这些点的正确性。

以下屏幕截图显示了此类违规的示例。 暴民的脚跟被视为与表面的接触点,但是脚的阴影已经超出了脚本身。



如果在食人魔的腿上,您仍然可以稍微改变阴影的位置并掩盖问题,那么对于几十个树干来说,就没有机会了。 应该将所有应该投射阴影的位置对象都设置为单独的GameObject。 通过将相应的可破坏对象的副本放置在预制位置上并禁用此位置未使用的脚本,我正是这样做的。 同时,由于这个原因,可以将它们包括在场景对象的常规分类中,并且飞出该位置的炮弹不再严格地在所有对象之上绘制,而是在它们之间飞行。 此外,还可以使对象本身具有动画效果。

但是后来我遇到了新的麻烦。 有了阴影和数十个新对象,舞台上同时存在的GameObject的最大数量以及Animator和SpriteRenderer组件的数量增加了一倍以上。 当我将整批怪兽释放到该地点时,总共约有150个,Profiler责备地向我展示了大约40ms,该时间仅用于渲染和动画,而帧速率通常在10左右变化。但这还不够。

在寻找其他优化工具时,我遇到了有关动态批处理的大量文档和指南。

有关批处理的更多信息
简而言之,批处理是一种用于最大程度减少绘图调用次数的机制,并以此来减少在CPU和GPU之间的交互上渲染帧时所花费的时间。 当使用而不是单独发送每个元素进行渲染时,将相似的元素一次组合在一起并绘制在一起。 在Unity的情况下,引擎本身试图最大程度地利用这种机制,并且开发人员几乎不需要采取任何其他措施。

Frame Debugger显示,我充其量只有每个对象或生物的详细信息。 在为地图集的第一个和第二个创建了精灵之后,我仅通过几次绘制调用就获得了阴影阴影,但是这些阴影的所有者顽固地拒绝战斗。

在一个单独的场景上进行的实验表明,当对象具有SortingGroup组件时,动态批处理就会中断,该组件用于对屏幕上实体的显示进行排序。 从理论上讲,没有它是可能的,但是,单独设置对象中每个精灵和粒子系统的排序值可能会比缺少批处理更为昂贵。

但是有些东西困扰着我。 阴影对象是真实场景中宿主对象的后代,在技术上属于同一SortingGroup,但是,动态阴影对象的阴影处理没有问题。 唯一的区别是主机对象是由主摄像机直接在屏幕上绘制的,而阴影对象首先是在RenderTexture中渲染的。

这就是陷阱。 互联网尚不知道此行为的确切原因是什么,但是在RenderTexture中渲染摄像机图像时,SortingGroup不再中断批处理。 这个决定似乎很奇怪,不合逻辑,而且通常是最不利的。 但是,通过使用与阴影的渲染相同的方法来实现实体的渲染,并因此获得了除阴影层之外的实体层,我已经获得了相当令人满意的性能值。

下面的屏幕截图显示了渲染实体层的示例。



因此,通常,在Y坐标中渲染某个实体看起来像这样:

  1. 实体放置在Y-20;
  2. 相机通过在实体的RenderTexture中观察此坐标来渲染实体。
  3. 实体阴影放置在Y + 20处;
  4. 摄像机通过在RenderTexture中观察该坐标的阴影来绘制实体的阴影;
  5. 主摄影机在屏幕上绘制主要位置精灵,这是当前直接渲染到屏幕上的唯一元素。
  6. 主相机在屏幕上绘制一个平面,以RenderTexture阴影作为材质;
  7. 主摄像机在屏幕上绘制一个平面,并使用实体的RenderTexture作为材质。

这样的夹心蛋糕。

在下面的屏幕截图中,编辑器的摄像头设置为三维模式,以演示图层相对于彼此的位置。



细微差别


但事实证明,在将决策复制到其他实体的过程中,一般情况并未涵盖所有可能的情况。 例如,某些实体相对于表面处于某个高度,特别是壳和一些过场动画角色。 此外,壳还具有根据其在屏幕上移动的方向旋转的能力,因此,除了设置对象与其阴影的交点之外,还必须选择旋转部分作为单独的子对象,以校正弹丸及其动画的旋转逻辑。

以下屏幕截图显示了壳及其阴影旋转的示例。



像计划中的飞行小怪一样,飞行角色也可以在其虚拟Y坐标内移动,这需要创建一种机制,用于根据阴影的所有者在虚拟Y轴上的位置来计算阴影的位置。

下面的GIF显示了一个将对象高高移动的示例。



另一个不符合一般概念的案例是一辆坦克。 与所有其他实体不同,该储罐在虚拟Z轴上的尺寸非常大,并且如上所述,阴影的总体实现要求对象几乎是平坦的。 解决此问题的最简单方法是为坦克的各个部分手动绘制阴影形状,因为您可以在阴影层上放置任何东西。

为了正确构造手绘阴影,我必须根据现有阴影的屏幕快照组装线条设计,可以在下面的屏幕快照中看到。



如果以这种方式缩放和放置此结构,使得上部位于父对象的某个点,而下部位于与曲面的接触点,则结构的右角将显示相应阴影点应位于的位置。 以这种方式预测了几个关键点之后,在它们上构建整个阴影并不困难。

另外,坦克的各个部分可以有不同的高度来固定儿童部分,这就像飞行人物和暴民的情况一样,需要调整每个特定部分的阴影位置。

下面的屏幕截图显示了水箱及其阴影组件,并且它也是独立零件的形式。



墙壁的阴影原来是一种单独的痛苦。 在开始进行阴影工作时,墙壁的性质与水箱的细节相同-几十个单独的精灵中的一个物体。 但是,墙壁具有由动画师控制的几种状态。

认真思考如何处理它们,我得出结论,需要更改隔离墙的概念。 结果,这些墙被划分为多个部分,每个部分都有自己的状态集,自己的动画师和自己的阴影。 这样就可以使用相同的方法为平行于X轴的生物创建阴影,就像对于生物一样,对于那些不符合此规则的部分,它们必须自己准备一些东西。 在某些情况下,我必须为剖面阴影创建自己的动画器,并手动设置精灵的位置。

例如,在下面的屏幕快照中显示的部分中,阴影是通过对每个单独的日志而不是整个部分应用失真来形成的。



结论


实际上,仅此而已。 尽管有上述所有细微差别,但最初的任务已完全完成,现在我的项目可以拥有相当不错的阴影,尽管来源有些可疑。 我希望,由于本文的缘故,对于下一个向我问类似问题的独立开发人员,如果不做榜样,那么互联网将变得更加有用,至少可以说是别人为自己的学习所犯的错误。

Source: https://habr.com/ru/post/zh-CN484744/


All Articles