2D 视差

前言

视差是一种视觉效果,通过让图像纹理相对于相机以不同速度移动来模拟纵深。Godot 提供了 Parallax2D 节点来实现这种效果。这方面的坑还是不少的,因此本页面会对部分属性进行深入描述,并会介绍纠正一些常见错误的方法。

备注

This page covers how to use Parallax2D, which is recommended to use over the ParallaxLayer and ParallaxBackground nodes.

入门

The parallax node supports adding nodes that render things as children, so you can use one or many nodes to make up each layer. To begin, place each node or nodes you want to have scroll independently as a child of their own parallax node. Make sure that the top left of the textures used are at the (0, 0) crossing, like in the image below. See the section on positioning for why this is important.

../../_images/2d_parallax_size_viewport.webp

The scene above uses one prepared texture for the higher clouds in a Sprite2D, but you could just as easily use multiple nodes spaced out to compose the layer.

滚动缩放

视差效果的核心是 scroll_scale 属性,该属性为滚动速度系数,能够让图层的移动速度和相机的移动速度产生差别,不同的轴可以分别进行调整。设为 1 时视差节点与相机同速。滚动时,如果你想要让图像看起来比较远,请使用小于 1 的值,设为 0 就会完全停止。如果你想要让图像看起来离相机很近,请使用大于 1 的值,这样图像滚动得就会更快。

上面这个场景由五个图层构成,对应的 scroll_scale 可以设置为:

  • (0.7, 1) - 森林

  • (0.5, 1) - 山丘

  • (0.3, 1) - 低处的云

  • (0.2, 1) - 高处的云

  • (0.1, 1) - 天空

下面的这个视频展示的就是这些值在游戏中的对滚动的影响:

无限重复

Parallax2D 还可以顺便让纹理产生无限重复的假象。相机移动时,repeat_size 可以让节点的位置根据这个值向前或向后吸附。这个效果的原理是让所有子级画布项重复一次并使用这个值进行偏移。而相机在原本的图像和重复的图像之间滚动,悄悄跳回原本的位置就会造成图像循环的效果。

../../_images/2d_parallax_scroll.gif

这是一个很巧妙的效果,不熟悉的用户很容易在设置时出错。那我们就来看几个常见问题的“原理”和“解法”。

大小问题

为了方便实现无限重复效果,图像本身最好就需要能够无缝重复,并且需要图像的大小在设置 repeat_size 之前至少要和视口大小一致。如果无法获取合适的资产素材,也有一些方法可以事先处理一下图像的大小。

下例是一个在视口中显得太小的纹理:

../../_images/2d_parallax_size_bad.webp

我们可以看到视口的尺寸是 500x300 而纹理的尺寸是 288x208。如果我们把 repeat_size 设置为图像的尺寸,无限重复效果在滚动时就会出问题,因为原始纹理没有覆盖整个视口。如果我们把 repeat_size 设置为视口的尺寸,那么就会有一个很大的间隙。那该怎么办呢?

把视口调小

The simplest answer is to make the viewport the same size or smaller than your textures. In Project Settings > Display > Window, change the Viewport Width and Viewport Height settings to match your background.

../../_images/2d_parallax_size_viewport.webp

缩放 Parallax2D

如果你想要实现的不是完美像素风,或者不介意存在略微的模糊,那么你可以选择将纹理放大到适合屏幕的尺寸。请设置 Parallax2Dscale,所有子级纹理都会对应发生缩放。

缩放子节点

和放大 Parallax2D 类似的是将 Sprite2D 节点放大到能够覆盖住屏幕。请注意,Parallax2D.repeat_sizeSprite2D.region_rect 等设置并不会考虑缩放,因此这些值也需要根据缩放进行调整。

../../_images/2d_parallax_size_scale.webp

重复纹理

你也可以通过提前准备子节点来确保从一开始就走上正确的道路。如果你有一个希望重复的 Sprite2D,但它的尺寸太小,就可以按照以下步骤来重复它:

下图中可以看到,把图像重复一次就足够覆盖住屏幕了。

../../_images/2d_parallax_size_repeat.webp

位置问题

用户经常错误地把所有纹理都设成在 (0,0) 居中:

../../_images/2d_parallax_single_centered.webp

这会给无限重复效果带来问题,应当尽量避免。“无限重复画布”从 (0,0) 开始,向右下扩展至 repeat_size 的大小值。

../../_images/2d_parallax_single_expand.webp

如果纹理是以 (0,0) 交叉点为中心的,那么无限重复画布只会被部分覆盖,因此也只会部分地重复。

repeat_times 调大有用吗?

从技术上来说,增加 repeat_times 在某些情况下将是可行的,但这是一种暴力的解决方案,而不是它被设计用于解决的问题(我们稍后会讨论这个问题)。更好的解决方法是理解重复效果的工作原理,并在开始时就适当地设置视差纹理。

首先,检查是否有任何纹理溢出到画布的负轴向。确保在视差节点中使用的纹理都位于从 (0,0) 开始的“无限重复画布”内。这样,如果正确设置了 Parallax2D.repeat_size,效果应该看起来像这样,图像的单次循环与视口大小相同或更大:

../../_images/2d_parallax_repeat_good_norect.webp

想象一下图像是如何在屏幕上滚动的,它首先显示红色矩形内的内容(由 repeat_size 确定),当到达黄色矩形内时,它会将图像向前拉动,从而给人一种永远滚动的感觉。

../../_images/2d_parallax_repeat_good.webp

如果你将图像放置在“无限重复画布”之外的位置,当摄像机到达黄色矩形区域时,图像的一半会被裁切掉,然后才会向前跳转,就像下图所示:

../../_images/2d_parallax_repeat_bad.webp

滚动偏移

如果你的视差纹理已经正常工作,但你希望它从不同的点开始,Parallax2D 附带一个 scroll_offset 属性,用于偏移无限重复画布的开始位置。例如,如果你的图像是 288x208,将 scroll_offset 设置为 (-144,0)(144,0) 可使其从图像的中间位置开始。

重复次数

理想情况下,按照本指南操作,你的视差纹理足够大,即使在缩小时也能覆盖屏幕。到目前为止,我们在 288x208 视口内拥有一个完美贴合的 288x208 纹理。但是,当我们通过将 Camera2D.zoom 设置为 (0.5, 0.5) 进行缩小时,会出现问题:

../../_images/2d_parallax_zoom_single.webp

尽管在默认缩放级别下,视口的所有内容都已正确设置,但缩小后会使它小于该视口,从而破坏无限重复效果。这时 repeat_times 就可以提供帮助。将值设置为 3(前后各一个额外的重复),现在它就足够大,可以容纳无限重复的效果了。

../../_images/2d_parallax_zoom_repeat_times.webp

如果这些纹理需要垂直地重复,我们应该为 repeat_size 指定一个 y 值。repeat_times 也会自动在上方和下方添加重复。这只是一个水平视差,因此它会在图像上方和下方留下一个空白块。我们如何解决这个问题?我们需要发挥创造力!在这个例子中,我们将天空拉高,将草精灵拉低。纹理现在支持正常缩放级别以及缩小到一半大小。

../../_images/2d_parallax_zoom_repeat_adjusted.webp

分屏

大多数用 Godot 制作分屏游戏的教程一开始都是要写一个简单的脚本,把第一个 SubViewport 的 Viewport.world_2d 赋值给第二个 SubViewport,从而实现屏幕的共享。此时就会产生如何在这两个屏幕之间共享视差效果的问题。

视差效果模拟透视的方法是让不同的纹理根据其与相机的关系移动不同的距离。存在多个相机时,很显然就会出现问题,因为同一个纹理不可能同时出现在两个不同的地方!

This is still achievable by cloning the parallax nodes into the second (or third or fourth) SubViewport. Here's how a setup looks for a two player game:

../../_images/2d_parallax_splitscreen.webp

Of course, now both backgrounds show in both SubViewports. What we want is for each parallax to only show in their corresponding viewport. We can achieve this by doing the following:

  • Leave all parallax nodes at their default visibility_layer of 1.

  • Set the first SubViewport's canvas_cull_mask to only layers 1 and 2.

  • Do the same for the second SubViewport but use layers 1 and 3.

  • Give your parallax nodes in the first SubViewport a common parent and set its visibility_layer to 2.

  • Do the same for the second SubViewport's parallax nodes, but use a layer of 3.

How does this work? If a canvas item has a visibility_layer that doesn't match the SubViewport's canvas_cull_mask, it will hide all children, even if they do. We use this to our advantage, letting the SubViewports cut off rendering of parallax nodes whose parent doesn't have a supported visibility_layer.

在编辑器中预览

4.3 版本之前推荐的是把每个层都放到各自的 ParallaxBackground 下面,然后启用 follow_viewport_enabled 属性,再对各个层进行缩放。这个方法要用对还挺难的,用 CanvasLayer 代替 ParallaxBackground 也能达到想要的效果。

备注

另外推荐 KoBeWi 的“Parallax2D Preview”插件。这个插件提供了很多预览模式,挺好用的!