许 志强

许 志强

1660310460

具有 Unity Terrain 功能的简单环境设计

Unity Terrain是用于关卡和环境设计的Unity 原生、强大且通用的工具。它提供了简单的机制来使用高度图的概念来塑造和修改地形。

在本文中,我们将了解如何使用此工具及其主要功能的基础知识。我们从使用高度图如何生成地形的理论开始。然后我们将讨论修改地形的主要机制。在文章的最后,我介绍了使用免费资源和 Unity Terrain 工具制作精美 Unity 场景的典型工作流程。

地形完成示例

从高度图到地形

高度贴图是一种改变玩家查看特定网格的方式的纹理,类似于法线贴图的工作方式。更具体地说,高度图信息会移动网格的顶点以显示高程、悬崖、平原和陨石坑等特征。

由于其 2D 特性,单个高度图一次只能干扰顶点的一个轴。值得注意的是,Unity Terrain 高度图会改变 y 轴上顶点的位置。

当我们通过升高或降低其部件来处理 Unity Terrain 时,我们在技术上改变了它的高度图。改变立即起作用,我们可以看到我们改变地形的结果。或者,也可以直接在 Unity 外部更改高度图并将高度图导入引擎。

例如,网站Cities: Skylines Height Map Generator提供了从现实世界中提取高度图信息的机制。尽管该网站旨在用于Cities: Skylines 游戏,但地图在 Unity 中也同样适用。但请考虑,它们将包含真实世界的距离,并且可能需要在外部工具中进行调整以在 Unity 中实现最佳使用。

下图显示了从网站中提取并导入 Unity Terrain 的高度图。

Unity中的高度图

Unity 地形中的高度图。

高度图可视化

高度图可视化(调整值以提高可见性)。

Unity 地形设置

选中后,可以使用地形工具栏编辑 Unity 地形。工具栏包含地形的主要功能,分为 5 个部分:相邻地形图块雕刻和绘画添加树添加详细信息;和一般设置

工具栏设置

 

相邻地形图块允许您在当前地形附近以类似网格的格式创建其他地形。当您已经拥有完善的地形但旁边需要更多空间时,使用它是有益的。

这是比增加地形大小更合适的解决方案。改变大小将影响高度图的读取方式,并且比例比例因子将应用于地形及其所有元素。

关于高度图,创建一个新的相邻地形将创建一个带有高度图的新地形游戏对象。新创建的高度图将尝试匹配其边界中的值,将其无缝耦合到之前连接的地形。

Sculpt and Paint选项直接与高度图一起使用。此选项包含其地形工具,我们将在下一节中介绍。简而言之,这个选项允许我们变形高度图并隐藏它的一部分以创建孔和入口,例如。

添加树木添加细节的两个选项都用于使用 Unity 自动处理的元素填充地形,例如树木、灌木、灌木、石头、草地等。与在场景中使用用户放置的游戏对象相比,使用 Unity Terrain 的一个优势是 Unity 管理了许多针对地形元素的优化技术。在可见性剔除相关的算法和广告牌中会自动考虑树木和细节。

最后,General Settings 选项包含与地形游戏对象有关的所有相关信息,例如优化技术(细节距离、广告牌距离、阴影等)和地形大小。

常规设置分为基本人族、树和细节对象草风设置网格分辨率孔设置纹理分辨率照明光照贴图。广泛涵盖每个选项超出了本文的范围,但我们将介绍最常用的选项。

地形设置

Basic Terrain部分涵盖了地形的主要渲染方面,例如地形材质、阴影属性和使用的绘制模式。请注意,如果您希望使用自制的后处理着色器或脚本渲染通道,则需要禁用Draw Instanced选项。

网格分辨率值确定地形的大小。更改地形的宽度和长度将立即改变读取高度图信息的方式。地形宽度、长度和高度决定了地形在 x、y 和 z 轴上的大小。

纹理分辨率部分处理更具体的高度图纹理信息。您可以在那里设置高度图的分辨率和导入/导出高度图。使用导入功能,您可以将从其他来源(例如前面介绍的来源)获取的数据用于您的地图。导出当前高度图,在外部工具中对其进行更改,然后将其导入回来也是很常见的。

雕刻和改变地形的选项

如前所述,雕刻和绘画选项允许我们通过改变高度图来改变地形。这些选项有六个工具:升高或降低地形油漆孔油漆纹理设置高度平滑高度标记地形

雕刻和绘画工具

Raise or Lower TerrainSet HeightSmooth Height的选项都用于通过增加或减少其值来直接影响高度图。设置高度用于更改高度图以满足特定的期望结果,而平滑高度则可以柔化地形,就像模糊高度图中的选定区域一样。所有这些选项都是通过使用地形画笔完成的(如上图所示)。

地形画笔类似于画笔在其他图像编辑软件(如 GIMP 和 Photoshop)中的工作方式。刷子以灰色和 alpha 的阴影呈现图案,将其应用于地形以使用其值对其进行塑造。笔刷的透明区域不会影响地形,较暗的区域对地形的影响要大于较亮的区域。Unity 的默认地形笔刷集包含十多个笔刷。

Paint Texture选项用于在地形中应用纹理,主动为其添加颜色和其他纹理属性。使用与其他雕刻和绘画工具相同的地形画笔应用纹理层。但是,Unity 的默认地形没有纹理层。

地形图层

地形图层列表。

一个地形层

一个地形图层的属性。

每个地形层都是它自己的资产,可以被多个地形重用,即使在不同的场景中也是如此。添加的第一个地形图层将自动覆盖整个地形。使用它作为设置基色的快速方法。每个地形层都可以有漫反射法线蒙版贴图

漫反射贴图是地形显示的纹理。设置后,更改其色调的选项变得可见。更改色调是创建地形变化的一种快速有效的方法,无需使用更多纹理或在外部程序中编辑资源。

虽然法线贴图用于传达图层上的法线信息,并且通常与常规法线贴图一样工作,但遮罩贴图明确用于高清和通用渲染管道以传达更多信息,例如金属、环境光遮蔽、高度和平滑度。

最后,平铺设置是地形图层的便捷选项。大小和偏移值会改变纹理在地形中平铺的频率以及每次重复的基本偏移量。

一般来说,对于大地形,最好有几个地形层的变化,它们的大小值不同,因为它可以帮助打破地形地板上的可见重复。

如前所述,Unity 的地形默认带有地形画笔,但在创建新地形对象时没有地形图层。

此外,必须有地图和纹理来创建地形图层。在本文中,我使用了资源商店中的几个新的免费画笔(Flaming Sands 的Generic Terrain Brushes和StampIT! Rowlan Inc 的收藏示例)以及地形图层的免费纹理包(手绘草和地面Chromisu 的纹理)

带有一些纹理的基础地形的结果如下所示:

带纹理的基础地形

在地形上工作时,我通常会尝试平衡高峰区域和平原区域。这往往会给我足够的空间来散布植被,而不会使空间过度拥挤。此外,我尝试在地形中使用一个关键元素来保持其主要组成部分和焦点,例如山、林间空地或河流。

拥有一个主要组件有助于决策使用哪些资产以及如何分配它们。在这种情况下,我们将制作一个带有洞穴状入口的小山丘,以探索更多的地形选择和一些感兴趣的小区域,以使视图多样化。

另外,请注意,我试图探索多个地形图层以打破它们倾向于产生的视觉重复。在这个阶段,我尝试只遮挡主要的斑点,将山丘与普通的草地和平原区分开,并在各处散布一些多样性,一些沙子和较暗的草地。

种树、种草和……石头?

可以通过两种方式添加植被,使用“添加树”选项或使用“添加细节”选项。添加树非常简单,允许您放置具有树状特征的对象。

为了获得更好的协同效果,应该使用 Unity 工具(例如Speed TreeTree Editor)或使用特定的地形树材质来创建树。但这不是强制性的,在接下来的部分中,我将展示另一种产生类似结果并允许更多创作自由的方法。

在任何情况下,地形树的每个网格最多只能使用 2 种材质(一种用于树皮,另一种用于叶子)。如果您的网格有两种以上的材质,它可能无法正确渲染。解决此限制的一个常见方法是使用纹理图集。此外,通常将叶子材质与树皮材质分开以获得更有趣的效果,这允许我们仅为叶子编写着色器(例如假装风运动)。

树木

与地形图层类似,树木也必须单独添加到地形中,然后使用地形的画笔放置。树可以添加为裸网格,也可以添加为预制件。

请注意,将树添加为预制件不一定会添加所有预制件元素,也不一定会完全按照您的预期预制件行事。例如,放置带有动画师的树预制件将忽略动画师并充当静态树。

岩石和草地

以类似的方式,细节是可以放置在地形中以使其视觉效果多样化的元素。细节可以有两种类型:细节网格和草纹理。细节网格的工作方式类似于在场景中保持静态的常规网格,例如石头、鹅卵石甚至灌木。

细节网格仅限于一种材料。如果您尝试使用具有多种材质的网格,它们将无法正确渲染。

细节网格也可以渲染为 Vertex Lit 或 Grass。Vertex Lit 会将网格渲染为常规光照游戏对象,并且不会对风做出反应,这适用于石头和树桩。草渲染的工作方式类似于草纹理,允许地形的风影响网格。

但是,根据经验,只有当风弯曲属性设置为低值(在Terrain General Settings下)时,草渲染才会产生视觉上令人愉悦的结果。否则,细节网格可能会不切实际地四处移动。

Unity 中的平滑草动画

草纹理是在地形中渲染的精灵,根据地形的风来表现。如 Unity 文档中所述,术语“草纹理”具有误导性,因为您可以使用任何通用纹理,例如花朵或树枝,使用相同的工具。与渲染为草的细节网格不同,草纹理将保持其在地面上的轴心并随风移动并保持相同的位置——正如您所期望的草的行为。

在本文中,我使用了资源商店中的树木和其他网格(JustCreate 的Low-Poly Simple Nature Pack和 Broken Vector 的 Low- Poly Rock Pack)。对于草纹理,我使用了来自 Kenney 的优秀Foliage Sprites,它是无版权、高质量资产的重要来源。最后,作为额外的装饰资产,我使用了JustCreate 的Low Poly Dungeons Lite

将这些应用于我们的地形,我们得到以下结果:

地形结果

具有 LOD 组的更通用的树

如前所述,由于地形系统的优化,Unity 的树在某种程度上受到了限制。如果您使用自定义材质或其他变体,可能会忽略某些树放置选项,从而导致非常平淡和重复的场景。

但是,该系统还允许我们绕过它的一些限制,以在材料使用和风格方面实现更高水平的多功能性。为此,我们必须遵循特定的流程。

首先,您必须为树创建一个新的预制件,其中根游戏对象(层次结构中的第一个)是具有LOD 组组件的空游戏对象。然后,您可以将树预制件放置在根游戏对象下方的层次结构中。之后,将树预制添加到 LOD 组,就是这样。

树预制件将与您的自定义着色器一起使用,并从地形接收常规的树放置属性,例如更改树的宽度、高度和旋转。对于颜色变化,需要更改着色器以读取 _TreeInstanceColor 属性,但此步骤超出了本文的范围。

当然,您可以使用已经使用的 LOD 组功能来处理树的其他级别的细节。但是,如果您只是想在树上使用更通用的材质,同时仍使用地形放置属性,您可以将 LOD 的数量更改为 1 并仅使用单个树预制件。

完成这些步骤应该会让您获得类似于以下的结果:

树细节层次

建议的地形工作流程以获得更好的结果

既然我们已经介绍了如何使用地形工具的主要方面,我想提供一个简单的工作流程,我可以使用它来轻松获得相当好的结果,同时还介绍一些可以改善环境的其他渲染方面。

制作洞穴入口

让我们从我之前提到的洞穴开始。Unity Terrain 不允许我们创建入口或水平雕刻高度图(在 xz 平面中应用高度信息)。但是,它将使我们能够在其上画孔。

Unity Terrain 中的孔洞隐藏了地形网格的某些部分,就好像我们正在切割碎片一样。可以使用地形笔刷像雕刻地形工具中的其他元素一样绘制孔洞。

为了制作一个视觉上令人兴奋的洞穴入口,我建议您提高地形,将仰角保持在 40 到 60 度之间。不仅如此,可能会过度拉伸地形网格,使其更难被其他元素和预制件覆盖。通过这样做,我们应该得到如下结果:

撕裂地形

如您所见,地形洞以二进制方式隐藏了部分地形:它们要么完全消失,要么完全可见。为此,有必要用资产和其他元素覆盖接缝以实现更好的视觉质量。

地形视图

使用前面提到的相同免费资源,我在洞穴入口周围缩放和旋转不同的石头,以隐藏洞和可见地形之间的接缝。我还尝试添加比必要的更多的岩石和鹅卵石,以创造更自然的外观。调整高度图以更好地适应与岩石的连接是很常见的。

此外,在感兴趣的区域周围添加额外的道具总是有助于增加玩家的注意力。为此,我添加了一些半埋在地上的罐子和一个在洞穴前的柱子,这有助于创造一些神秘和叙事。我在洞穴入口旁边添加了一个小灯,以引导眼睛更远。

洞穴入口处的灯光

如前所述,这个洞还允许用户看穿地形。为避免这种情况,我使用了具有忽略照明(无光照)的特定材质的平面。这很重要,因为否则,场景光线的变化会将这些暗墙显示为实心,而我们想要的视觉结果是让它看起来像一个暗/阴影区域。

此外,我使用了 Low Poly 包中的一些网格作为洞穴的地面,因为我们不能使用迄今为止一直使用的相同地形。

需要注意的是,如果您希望您的玩家进入并在其中导航,那么到目前为止我们所做的洞穴并不理想。为此,您需要在洞周围正确设置网格,从各个方面模拟洞穴内部。使用新的地形对象可以实现这一点,但我认为所需的工作可能不是最佳的。

另一方面,此解决方案非常适合您用作将玩家移动到专用于洞穴内部的新场景的点。

手动环境遮挡

环境光遮挡示例

环境遮挡示例 2

我用来改善 Unity Terrain 中照明的常用技术是在放置在地面上的对象下手动使用更暗的地形层变化。这有助于提升地形元素的凝聚力,因为某些光照效果(例如环境光遮蔽)不会应用于地形对象,尤其是在您使用自定义着色器和其他技术时。

地形示例遮挡

请注意,这与投射阴影不同。Unity 地形将正确地从对象投射阴影,但这可能仍会造成对象漂浮或未真正连接的印象。在某些地点使用深色地形图层有助于使物体和地面更接近,而无需更多资源或昂贵的照明计算。

此外,我经常将其应用于不是地形对象的其他对象和预制件的底部。如上图所示,我在书本和大锅下方使用了一些较暗的色调,以帮助使元素更加靠近。

真正的环境光遮蔽和光照烘焙

较暗的层在一定程度上有助于假照明,但可以通过烘焙场景中的光来放大。关于光照烘焙的完整讨论超出了本文的范围,但将讨论其中的一些主要元素,以了解如何使用它们来改善地形的视觉效果。

首先,Light Baking是一个过程,我们预先计算场景中的灯光并存储数据,然后将其应用到场景中的对象之上。Unity 专门使用Lightmapping,它在纹理贴图中烘焙对象表面的亮度。

但是,这些纹理不能在运行时更改,并且需要针对场景中的每次更改重新进行预先计算,例如将对象移动到不同的位置或更新灯光的变换。此外,它仅适用于标记为静态的对象。

照明选项卡

光照烘焙可以在 Lighting 选项卡中完成(您可以通过Window菜单访问它,然后是Rendering,然后是Lighting)。Unity 的光照烘焙默认值相当高,处理时间可能太长。为此,我建议您将所有Direct SamplesIndirect SamplesEnvironment Samples减少到较低的数量,例如 16 或 32。

这些属性与 Lightmapper计算场景中任何给定点的光照的步数直接相关(对于静态元素)。增加其中任何一个都会产生更好和更真实的结果,但会大大增加烘烤时间。

我通常从低值开始评估场景中的第一印象,然后在我认为合适的时候逐步增加它们。对于更多的风格或自定义材料,使用较低的值通常足以实现良好的视觉效果。

此外,第一次烘焙可能会显示对象放置的不一致和其他应在应用更彻底和更逼真的光照贴图程序之前修复的错误。

我还建议在烘焙的光照属性中打开光照烘焙环境光遮蔽。环境光遮蔽是当表面彼此靠近时发生的视觉效果,在它们的交叉点遮挡光线并投射阴影,使它们看起来更暗。我们之前使用较深的地形图层颜色来伪造它,但光照贴图实际上可以计算出适当的环境光遮蔽并将其烘焙到纹理中。

下图显示了有和没有光烘烤的结果。

无光烘烤

无需轻烤。

轻烤

轻烤。

如您所见,树干和地面之间没有环境遮挡。然而,大锅的底部和图像背面的板条箱之间的交叉点比没有烘烤的版本更暗,更逼真。

放置天空盒并应用后处理

最后,为了进一步改善视觉效果并将元素联系在一起,这两个步骤必不可少:放置更好的天空盒并应用一些后期处理效果。

在本文中,我使用了来自 Yuki2022的Free Stylized Skybox 。这些天空盒非常适合低多边形风格的卡通外观。

可以在Lighting选项卡(与 Light Baking 相同)下的Environment选项卡下添加天空盒。

后处理效果需要更多步骤。首先,您必须根据安装的渲染管道将后处理包添加到您的项目中。使用内置渲染管道,您需要使用包管理器在项目中安装后处理堆栈。

否则,如果您使用的是通用渲染管线或高清渲染管线,则您已经安装了等效的后处理系统。我在本文中使用的是通用渲染管道,但效果和配置与其他管道相似。

下图显示了我用于地形最终版本的最终后处理体积:

音量设置

使用的效果是:

  • 晕影:使屏幕的角落变暗,将玩家的注意力集中在中心元素上
  • Bloom : 扩展场景中的高光元素,用相邻对象模糊它们的区域,这是一种假的(但很好的)全局照明替代方案
  • 景深:模仿我们眼睛和真实相机的焦点,使离我们更近的元素(在焦点处)清晰,而其他元素根据它们的距离模糊
  • 颜色曲线:更改渲染的最终颜色以匹配更理想的外观。在这种情况下,我增加了红色而不是其他颜色并稍微增加了蓝色
  • Lift Gamma Gain:改变渲染的整体颜色,并增加对暗色调、中色调和高光的控制

我们在本文中所做的两个位置的最终结果如下图所示:

地形环境

最终地形示例

感谢您的阅读,如果您想了解更多关于本文讨论范围之外的许多主题的信息,请告诉我。他们每个人当然都值得拥有自己的文章。你可以在www.dagongraphics.com看到更多我的作品。

 来源:https ://blog.logrocket.com/easy-environment-design-unity-terrain-features/

#unity #design 

具有 Unity Terrain 功能的简单环境设计
Hong  Nhung

Hong Nhung

1660309200

Thiết Kế Môi Trường Dễ Dàng Với Các Tính Năng Unity Terrain

Unity Terrain là một công cụ gốc, mạnh mẽ và linh hoạt của Unity để thiết kế cấp và môi trường. Nó cung cấp các cơ chế dễ dàng để tạo khuôn và sửa đổi địa hình bằng cách sử dụng khái niệm bản đồ độ cao.

Trong bài viết này, chúng ta sẽ thấy những điều cơ bản về cách sử dụng công cụ này và các tính năng chính của nó. Chúng tôi bắt đầu với lý thuyết về cách hoạt động của quá trình tạo địa hình bằng cách sử dụng bản đồ độ cao. Sau đó, chúng ta sẽ thảo luận về các cơ chế chính để sửa đổi địa hình. Ở phần cuối của bài viết, tôi trình bày quy trình làm việc điển hình để tạo các cảnh Unity đẹp bằng cách sử dụng các nội dung miễn phí và công cụ Unity Terrain.

Ví dụ về địa hình đã hoàn thành

Từ bản đồ độ cao đến địa hình

Bản đồ độ cao là một kết cấu thay đổi cách người chơi xem một lưới cụ thể, tương tự như cách hoạt động của bản đồ bình thường. Cụ thể hơn, thông tin bản đồ độ cao thay đổi các đỉnh của lưới để hiển thị các đối tượng địa lý như độ cao, vách đá, đồng bằng và miệng núi lửa.

Do tính chất 2D, một bản đồ độ cao duy nhất chỉ có thể can thiệp vào một trục của các đỉnh tại một thời điểm. Đáng chú ý, bản đồ độ cao Unity Terrain thay đổi vị trí của các đỉnh trên trục y.

Khi chúng tôi làm việc với Unity Terrain bằng cách nâng cao hoặc hạ thấp các phần của nó, chúng tôi đang thay đổi về mặt kỹ thuật bản đồ độ cao của nó. Việc thay đổi có hiệu lực ngay lập tức và chúng tôi có thể thấy kết quả của sự thay đổi địa hình của chúng tôi. Ngoài ra, cũng có thể thay đổi bản đồ độ cao trực tiếp bên ngoài Unity và nhập bản đồ độ cao vào công cụ.

Ví dụ, trang web Cities: Skylines Height Map Generator cung cấp cơ chế trích xuất thông tin bản đồ độ cao từ thế giới thực. Mặc dù trang web được sử dụng trong trò chơi Cities: Skylines , các bản đồ cũng hoạt động tốt trong Unity. Tuy nhiên, hãy cân nhắc rằng chúng sẽ chứa khoảng cách trong thế giới thực và có thể cần được điều chỉnh trong một công cụ bên ngoài để sử dụng tối ưu trong Unity.

Các hình ảnh dưới đây hiển thị bản đồ độ cao được trích xuất từ ​​trang web và được nhập vào Unity Terrain.

Bản đồ độ cao trong Unity

Bản đồ Độ cao trong Địa hình Thống nhất.

Bản đồ độ cao được trực quan hóa

Bản đồ độ cao được hiển thị trực quan (các giá trị được điều chỉnh để cải thiện khả năng hiển thị).

Cài đặt Unity Terrain

Khi được chọn, một Địa hình thống nhất có thể được chỉnh sửa bằng cách sử dụng Thanh công cụ Địa hình. Thanh công cụ chứa các chức năng chính cho địa hình được nhóm thành 5 phần: Ô địa hình liền kề ; Điêu khắc và Sơn ; Thêm Cây s; Thêm chi tiết ; và Cài đặt chung .

Cài đặt Thanh công cụ

 

Các ô địa hình liền kề cho phép bạn tạo các địa hình khác ở định dạng giống như lưới gần địa hình hiện tại. Sẽ có lợi khi sử dụng nó khi bạn đã có địa hình tốt nhưng cần thêm không gian bên cạnh.

Đó là giải pháp phù hợp hơn là tăng kích thước địa hình. Việc thay đổi kích thước sẽ ảnh hưởng đến cách đọc bản đồ độ cao và hệ số tỷ lệ tương ứng sẽ được áp dụng cho địa hình và tất cả các yếu tố của nó.

Về bản đồ độ cao, việc tạo một địa hình liền kề mới sẽ tạo một đối tượng trò chơi địa hình mới với bản đồ độ cao của nó. Bản đồ độ cao mới được tạo sẽ cố gắng khớp các giá trị trong đường viền của nó, nối liền nó với địa hình trước đó mà nó được kết nối.

Tùy chọn Sculpt and Paint hoạt động trực tiếp với bản đồ độ cao. Tùy chọn này chứa các Công cụ địa hình , mà chúng tôi sẽ đề cập trong phần sau. Tóm lại, tùy chọn này cho phép chúng ta làm biến dạng bản đồ chiều cao và ẩn các phần của nó để tạo ra các lỗ và lối vào chẳng hạn.

Cả hai tùy chọn Thêm câyThêm chi tiết đều được sử dụng để điền vào địa hình với các yếu tố được Unity xử lý tự động, chẳng hạn như cây cối, bụi rậm, bụi rậm, đá, các mảng cỏ và các yếu tố khác. Một lợi thế của việc sử dụng Unity Terrain so với việc sử dụng các đối tượng trò chơi do người dùng đặt trong cảnh là Unity quản lý nhiều kỹ thuật tối ưu hóa cho các yếu tố của địa hình. Cây cối và các chi tiết được tính toán tự động trong các thuật toán và bảng quảng cáo liên quan đến kiểm tra khả năng hiển thị .

Cuối cùng, tùy chọn Cài đặt chung chứa tất cả thông tin liên quan đến đối tượng trò chơi địa hình, chẳng hạn như kỹ thuật tối ưu hóa (khoảng cách chi tiết, khoảng cách bảng quảng cáo, bóng, v.v.) và kích thước của địa hình.

Cài đặt chung được chia thành Cơ bản Terran , Cây & Đối tượng chi tiết , Cài đặt gió cho Cỏ , Độ phân giải lưới , Cài đặt lỗ , Độ phân giải kết cấu , Ánh sángÁnh xạ . Nó nằm ngoài phạm vi của phần này để bao quát từng tùy chọn, nhưng chúng ta sẽ đi qua những tùy chọn được sử dụng nhiều nhất.

Cài đặt địa hình

Phần Địa hình cơ bản bao gồm các khía cạnh hiển thị chính của địa hình, chẳng hạn như chất liệu địa hình, đặc tính bóng của nó và chế độ vẽ được sử dụng. Lưu ý rằng nếu bạn muốn sử dụng các trình đổ bóng xử lý hậu kỳ tự tạo hoặc các đường kết xuất theo kịch bản, tùy chọn Draw Instanced cần phải được tắt.

Các giá trị Độ phân giải lưới xác định kích thước của địa hình. Thay đổi chiều rộng và chiều dài của địa hình sẽ ngay lập tức thay đổi cách đọc thông tin bản đồ độ cao. Chiều rộng, chiều dài và chiều cao của địa hình xác định kích thước của địa hình theo các trục x, y và z.

Phần Độ phân giải kết cấu xử lý thông tin kết cấu bản đồ độ cao cụ thể hơn. Bạn có thể đặt độ phân giải cho bản đồ độ cao và nhập / xuất bản đồ độ cao ở đó. Sử dụng chức năng nhập, bạn có thể sử dụng dữ liệu thu được từ các nguồn khác (chẳng hạn như dữ liệu được trình bày trước đó) vào bản đồ của bạn. Nó cũng phổ biến để xuất bản đồ độ cao hiện tại, thay đổi nó trong một công cụ bên ngoài, và sau đó nhập nó trở lại.

Các tùy chọn để điêu khắc và thay đổi địa hình

Như đã nêu trước đây, tùy chọn Sculpt and Paint cho phép chúng tôi thay đổi địa hình bằng cách thay đổi bản đồ độ cao. Các tùy chọn này có sáu công cụ: Nâng hoặc Hạ địa hình , Lỗ sơn , Kết cấu sơn , Đặt chiều cao, Chiều cao mịnĐịa hình đóng dấu .

Công cụ điêu khắc và sơn

Các tùy chọn Nâng hoặc Hạ địa hình , Đặt Chiều caoChiều cao mịn đều được sử dụng để ảnh hưởng trực tiếp đến bản đồ độ cao bằng cách tăng hoặc giảm các giá trị của nó. Đặt Chiều cao được sử dụng để thay đổi bản đồ độ cao để đạt được kết quả mong muốn cụ thể, trong khi Độ cao mịn làm mềm địa hình như thể làm mờ các khu vực đã chọn trong bản đồ độ cao. Tất cả các tùy chọn này được thực hiện bằng cách sử dụng Terrain Brushes (như trong hình trên).

Bản vẽ địa hình tương tự như cách bàn chải hoạt động trong các phần mềm chỉnh sửa hình ảnh khác, chẳng hạn như GIMP và Photoshop. Một bàn chải trình bày một mẫu có màu xám và alpha, được áp dụng cho địa hình để tạo khuôn bằng cách sử dụng các giá trị của nó. Các khu vực trong suốt của bàn chải sẽ không ảnh hưởng đến địa hình, và các khu vực tối hơn sẽ ảnh hưởng đến địa hình nhiều hơn các khu vực sáng hơn. Bộ bàn chải địa hình mặc định của Unity đi kèm với hơn mười bàn chải.

Tùy chọn Paint Texture được sử dụng để áp dụng các lớp kết cấu trong địa hình, chủ động thêm màu cho nó và các thuộc tính kết cấu khác. Các lớp kết cấu được áp dụng bằng cách sử dụng cọ vẽ địa hình giống như được sử dụng cho các công cụ Điêu khắc và Sơn khác. Tuy nhiên, địa hình mặc định của Unity không có lớp kết cấu.

Lớp địa hình

Danh sách các lớp địa hình.

Một lớp địa hình

Các thuộc tính cho một lớp địa hình.

Mỗi lớp địa hình là một tài sản riêng của nó và có thể được tái sử dụng theo nhiều địa hình, ngay cả trong các cảnh khác nhau. Lớp địa hình đầu tiên được thêm vào sẽ tự động bao phủ toàn bộ địa hình. Sử dụng nó như một cách nhanh chóng để thiết lập màu cơ bản của bạn. Mỗi lớp địa hình có thể có Bản đồ Khuếch tán , Bình thườngMặt nạ .

Bản đồ khuếch tán là kết cấu được hiển thị bởi địa hình. Sau khi đặt, tùy chọn thay đổi màu của nó sẽ hiển thị. Thay đổi tông màu là một cách nhanh chóng và hiệu quả để tạo ra sự thay đổi địa hình mà không cần sử dụng thêm họa tiết hoặc chỉnh sửa nội dung của bạn trong các chương trình bên ngoài.

Mặc dù bản đồ bình thường được sử dụng để truyền tải thông tin bình thường trên lớp và thường hoạt động giống như bản đồ bình thường thông thường, bản đồ mặt nạ được sử dụng rõ ràng cho Đường ống hiển thị độ nét cao và phổ quát để truyền tải thêm thông tin, chẳng hạn như kim loại, tắc nghẽn xung quanh, chiều cao và độ mịn.

Cuối cùng, cài đặt lát là các tùy chọn tiện dụng cho lớp địa hình. Các giá trị kích thước và độ lệch thay đổi tần suất kết cấu sẽ xếp theo địa hình và với độ lệch cơ sở nào cho mỗi lần lặp lại.

Nói chung, đối với địa hình rộng lớn, rất tốt nếu có một vài biến thể của các lớp địa hình với các giá trị khác nhau về kích thước, vì nó có thể giúp phá vỡ sự lặp lại có thể nhìn thấy trên sàn của địa hình.

Như đã nêu trước đây, các cọ vẽ địa hình đi kèm với địa hình của Unity theo mặc định, nhưng không có các lớp địa hình khi tạo một đối tượng địa hình mới.

Hơn nữa, cần phải có bản đồ và kết cấu để tạo các lớp địa hình của bạn. Đối với bài viết này, tôi đã sử dụng một số bàn chải miễn phí mới từ cửa hàng tài sản ( Bàn chải địa hình chung của Flaming Sands và StampIT! Ví dụ bộ sưu tập của Rowlan Inc) cũng như một gói các kết cấu miễn phí cho các lớp địa hình ( Handpainted Grass & Ground Kết cấu của Chromisu ).

Các kết quả cho địa hình cơ sở với một số kết cấu có thể được nhìn thấy dưới đây:

Địa hình cơ sở có kết cấu

Khi làm việc trên địa hình, tôi thường cố gắng cân bằng giữa khu vực cao điểm và khu vực đồng bằng. Điều đó có xu hướng cung cấp cho tôi đủ không gian để trải thảm thực vật và không quá chật chội. Hơn nữa, tôi cố gắng có một yếu tố quan trọng trong địa hình để giữ các thành phần chính và trọng tâm của nó, chẳng hạn như núi, sông băng hoặc sông chẳng hạn.

Có một thành phần chính giúp đưa ra quyết định sử dụng tài sản nào và phân phối chúng như thế nào. Trong trường hợp này, chúng tôi sẽ tạo một ngọn đồi nhỏ với lối vào giống như hang động để khám phá thêm các tùy chọn địa hình và một số khu vực nhỏ quan tâm để đa dạng hóa tầm nhìn.

Ngoài ra, hãy lưu ý rằng tôi đã cố gắng khám phá nhiều lớp địa hình để phá vỡ sự lặp lại trực quan mà chúng có xu hướng tạo ra. Ở giai đoạn này, tôi cố gắng chỉ chặn các điểm chính, ngăn cách các ngọn đồi với cỏ thông thường và các khu vực đồng bằng, cũng như rắc một số đa dạng ở đây và ở đó với một số cát và các mảng cỏ sẫm màu hơn.

Trồng cây, cỏ, và… đá?

Thảm thực vật có thể được thêm theo hai cách, hoặc với tùy chọn Thêm cây hoặc với tùy chọn Thêm chi tiết . Add Tree khá đơn giản và cho phép bạn đặt các đối tượng có đặc điểm giống cây.

Để có sức mạnh tổng hợp tốt hơn, nên tạo cây bằng Công cụ hợp nhất, chẳng hạn như Cây tốc độ hoặc Trình chỉnh sửa cây , hoặc sử dụng vật liệu cây địa hình cụ thể. Tuy nhiên, điều đó không phải là bắt buộc và trong các phần sau, tôi sẽ chỉ ra một cách tiếp cận khác mang lại kết quả tương tự và cho phép tự do sáng tạo hơn.

Trong mọi trường hợp, cây địa hình có giới hạn tối đa là 2 vật liệu cho mỗi mắt lưới (một vật liệu cho vỏ và một vật liệu khác cho lá). Nếu lưới của bạn có nhiều hơn hai vật liệu, nó có thể không hiển thị chính xác. Một giải pháp phổ biến cho hạn chế này là sử dụng tập bản đồ kết cấu. Ngoài ra, người ta thường tách các vật liệu lá khỏi vật liệu vỏ cây để tạo ra một hiệu ứng thú vị hơn, điều này cho phép chúng ta lập trình các bộ đổ bóng chỉ cho lá cây (chẳng hạn như giả mạo chuyển động của gió).

Cây

Tương tự như các lớp địa hình, cây cối cũng phải được thêm vào địa hình riêng lẻ, sau đó được đặt trên đó bằng cách sử dụng bút vẽ của địa hình. Cây có thể được thêm vào dưới dạng lưới trần và cũng có thể là tấm lót sẵn.

Lưu ý rằng việc thêm một cái cây làm nhà lắp ghép sẽ không nhất thiết phải thêm tất cả các phần tử của nhà lắp ghép cũng như hoạt động chính xác như nhà lắp ghép dự kiến ​​của bạn. Ví dụ, đặt một nhà lắp ghép cây với một trình tạo hoạt hình sẽ bỏ qua trình tạo hoạt hình và hoạt động như một cây tĩnh.

Đá và cỏ

Tương tự, các chi tiết là các yếu tố có thể được đặt trong địa hình để đa dạng hóa hình ảnh của nó. Chi tiết có thể có hai loại : Lưới chi tiết và Kết cấu cỏ. Detail Mesh hoạt động giống như các mắt lưới thông thường giữ tĩnh trong cảnh, chẳng hạn như đá, sỏi và thậm chí cả cây bụi.

Các mắt lưới chi tiết được giới hạn cho một vật liệu. Nếu bạn cố gắng sử dụng các mắt lưới với nhiều hơn một vật liệu, chúng sẽ không được hiển thị chính xác.

Các mắt lưới chi tiết cũng có thể được hiển thị dưới dạng Vertex Lit hoặc Grass. Vertex Lit sẽ hiển thị lưới như các Đối tượng trò chơi được chiếu sáng thông thường và không phản ứng với gió, điều này thích hợp với đá và gốc cây. Kết xuất cỏ hoạt động tương tự như Kết cấu cỏ, cho phép gió của địa hình ảnh hưởng đến lưới.

Tuy nhiên, theo kinh nghiệm, kết xuất cỏ chỉ mang lại kết quả đẹp mắt khi đặc tính uốn cong của gió được đặt thành giá trị thấp (trong Cài đặt chung về địa hình ). Nếu không, lưới chi tiết có thể di chuyển không thực tế.

Hoạt ảnh Cỏ mịn trong Unity

Các họa tiết cỏ là các hình vẽ được hiển thị trong địa hình hoạt động theo gió của địa hình. Như đã nêu trong tài liệu của Unity, thuật ngữ "Grass Texture" gây hiểu lầm vì bạn có thể sử dụng bất kỳ kết cấu chung nào, chẳng hạn như hoa hoặc que, với cùng một công cụ. Khác với Lưới chi tiết được hiển thị dưới dạng Cỏ, Kết cấu Cỏ sẽ giữ trục của chúng trên mặt đất và di chuyển theo gió, giữ nguyên vị trí - như bạn mong đợi cỏ sẽ hoạt động.

Đối với bài viết này, tôi đã sử dụng cây và các mắt lưới khác từ kho tài sản ( Low-Poly Simple Nature Pack của JustCreate và Low Poly Rock Pack của Broken Vector). Đối với họa tiết cỏ, tôi đã sử dụng Foliage Sprites tuyệt vời từ Kenney, một nguồn nội dung chất lượng cao, không có bản quyền tuyệt vời. Cuối cùng, như các tài sản trang trí bổ sung, tôi đã sử dụng Low Poly Dungeons Lite của JustCreate.

Áp dụng những điều này vào địa hình của chúng tôi, chúng tôi có kết quả sau:

Kết quả địa hình

Nhiều cây đa năng hơn với Nhóm LOD

Như đã nói trước đây, cây cối của Unity bị hạn chế bằng cách nào đó do việc tối ưu hóa hệ thống địa hình. Nếu bạn sử dụng vật liệu tùy chỉnh hoặc các biến thể khác, một số tùy chọn vị trí cây có thể bị bỏ qua, dẫn đến cảnh rất nhạt nhẽo và lặp đi lặp lại.

Tuy nhiên, hệ thống cũng cho phép chúng tôi bỏ qua một số hạn chế của nó để đạt được mức độ linh hoạt cao hơn về cách sử dụng vật liệu và kiểu dáng. Muốn vậy, chúng ta phải tuân theo một quy trình cụ thể.

Đầu tiên, bạn phải tạo một nhà lắp ghép mới cho cây trong đó đối tượng trò chơi gốc (đối tượng đầu tiên trong hệ thống phân cấp) là một đối tượng trò chơi trống với thành phần LOD Group . Sau đó, bạn có thể đặt nhà lắp ghép cây của mình trong hệ thống phân cấp bên dưới đối tượng trò chơi gốc. Sau đó, thêm nhà lắp ghép cây vào Nhóm LOD, và thế là xong.

Nhà lắp ghép cây sẽ hoạt động với các bộ đổ bóng tùy chỉnh của bạn và nhận các thuộc tính đặt cây thông thường từ địa hình, chẳng hạn như thay đổi chiều rộng, chiều cao và vòng quay của cây. Để thay đổi màu sắc, cần phải thay đổi bộ đổ bóng để đọc thuộc tính _TreeInstanceColor, nhưng bước này nằm ngoài phạm vi của bài viết này.

Tất nhiên, bạn có thể sử dụng các tính năng của Nhóm LOD để xử lý các mức chi tiết khác cho cây của bạn. Tuy nhiên, nếu bạn chỉ muốn một vật liệu linh hoạt hơn trên cây của mình trong khi vẫn sử dụng các thuộc tính vị trí địa hình, bạn có thể thay đổi số LOD thành 1 và chỉ sử dụng một nhà lắp ghép cây duy nhất.

Đạt được các bước này sẽ đưa bạn đến kết quả tương tự như kết quả bên dưới:

Cây LOD

Quy trình làm việc theo địa hình được đề xuất để có kết quả tốt hơn

Bây giờ chúng ta đã đề cập đến các khía cạnh chính của cách sử dụng công cụ địa hình, tôi muốn cung cấp một quy trình làm việc đơn giản mà tôi sử dụng để có được kết quả tốt mà không cần nỗ lực nhiều đồng thời bao gồm một số khía cạnh kết xuất khác có thể cải thiện môi trường của bạn.

Làm một lối vào hang động

Chúng ta hãy bắt đầu với hang động mà tôi đã đề cập trước đây. Unity Terrain không cho phép chúng tôi tạo lối vào hoặc khắc bản đồ độ cao theo chiều ngang (áp dụng thông tin độ cao trong mặt phẳng xz). Tuy nhiên, nó sẽ cho phép chúng tôi vẽ các lỗ trên đó.

Các lỗ trong Unity Terrain ẩn các phần của lưới địa hình như thể chúng ta đang cắt từng mảnh. Các lỗ có thể được vẽ giống như các phần tử khác trong công cụ Sculpt Terrain bằng bút vẽ địa hình.

Để tạo một lối vào hang thú vị về mặt hình ảnh, tôi khuyên bạn nên nâng cao địa hình, giữ độ cao ở một góc từ 40 đến 60 độ. Hơn thế nữa, lưới địa hình có thể kéo căng quá nhiều và khiến việc che phủ các phần tử và bản dựng sẵn khác trở nên khó khăn hơn. Bằng cách làm như vậy, chúng ta sẽ nhận được một kết quả như dưới đây:

Tear in Terrain

Như bạn có thể thấy, các lỗ địa hình ẩn các phần của địa hình theo kiểu nhị phân: chúng bị biến mất hoàn toàn hoặc hoàn toàn có thể nhìn thấy được. Vì vậy, cần phải che các đường nối bằng tài sản và các yếu tố khác để đạt được chất lượng hình ảnh tốt hơn.

Xem địa hình

Sử dụng các tài sản miễn phí tương tự đã đề cập trước đó, tôi thu nhỏ và xoay các viên đá khác nhau xung quanh cửa hang để ẩn các đường nối giữa các lỗ và địa hình có thể nhìn thấy được. Tôi cũng cố gắng thêm nhiều đá và sỏi hơn mức cần thiết để tạo ra một cái nhìn tự nhiên hơn. Người ta thường điều chỉnh bản đồ độ cao để phù hợp với kết nối với các tảng đá tốt hơn.

Hơn nữa, thêm các đạo cụ phụ xung quanh các khu vực quan tâm luôn tốt để tăng sự tập trung của người chơi. Vì vậy, tôi đã thêm một số cái lọ được chôn nửa dưới đất và một cây cột ngay trước cửa hang, điều này giúp tạo ra một số bí ẩn và tường thuật. Tôi đặt thêm một ngọn đèn nhỏ cạnh cửa hang để dẫn mắt xa hơn.

Ánh sáng trên lối vào hang động

Như đã thấy trước đây, lỗ cũng cho phép người dùng nhìn xuyên qua địa hình. Để tránh điều đó, tôi đã sử dụng mặt phẳng với một vật liệu cụ thể bỏ qua ánh sáng (Unlit). Điều đó rất quan trọng bởi vì nếu không, những thay đổi trong ánh sáng của cảnh sẽ hiển thị những bức tường tối này là chắc chắn, trong khi kết quả hình ảnh mà chúng tôi muốn là nó trông giống như một vùng tối / bóng tối.

Hơn nữa, tôi đã sử dụng một số mắt lưới từ gói Low Poly để làm nền của hang động vì chúng tôi không thể sử dụng cùng địa hình mà chúng tôi đã sử dụng cho đến nay.

Điều quan trọng cần lưu ý là hang động mà chúng tôi đã làm cho đến nay không phải là lý tưởng nếu bạn muốn người chơi của mình vào và điều hướng bên trong nó. Để làm được điều đó, bạn sẽ cần đặt các mắt lưới xung quanh lỗ một cách hợp lý, phần nào mô hình hóa bên trong hang động từ mọi phía. Có thể đạt được điều đó bằng cách sử dụng một đối tượng địa hình mới, nhưng tôi nghĩ rằng công việc cần thiết cho điều đó có thể không tối ưu.

Mặt khác, giải pháp này hoạt động khá tốt để bạn sử dụng như một điểm để di chuyển người chơi đến một khung cảnh mới dành riêng cho bên trong hang động.

Khớp môi trường xung quanh bằng tay

Ví dụ về tắc nghẽn môi trường xung quanh

Ví dụ về tắc nghẽn môi trường xung quanh 2

Một kỹ thuật phổ biến mà tôi sử dụng để cải thiện ánh sáng trong Unity Terrain là sử dụng thủ công biến thể tối hơn của lớp địa hình bên dưới các đối tượng được đặt trên mặt đất. Điều đó giúp nâng cao tính gắn kết của các yếu tố địa hình vì một số hiệu ứng ánh sáng, chẳng hạn như tắc nghẽn xung quanh, không được áp dụng cho các đối tượng địa hình, đặc biệt nếu bạn đang sử dụng bộ đổ bóng tùy chỉnh và các kỹ thuật khác.

Ví dụ về địa hình

Lưu ý rằng điều này khác với đổ bóng. Địa hình hợp nhất sẽ đổ bóng từ các đối tượng một cách chính xác, nhưng điều đó vẫn có thể gây ra ấn tượng rằng các đối tượng đang trôi hoặc không thực sự được kết nối. Sử dụng lớp địa hình tối tại một số điểm giúp đưa các vật thể và mặt đất lại gần nhau hơn mà không yêu cầu nhiều tài nguyên hơn hoặc tính toán ánh sáng tốn kém.

Hơn nữa, tôi thường áp dụng điều này cho phần dưới cùng của các đối tượng khác và các prefabs không phải là các đối tượng địa hình. Như đã thấy trong hình trên, tôi đã sử dụng một chút tông màu tối hơn dưới các cuốn sách và chiếc vạc để giúp các yếu tố gần nhau hơn.

Môi trường xung quanh thực sự tắc và nướng nhẹ

Các lớp tối hơn sẽ giúp giảm ánh sáng giả ở một mức độ nhất định, nhưng nó có thể được khuếch đại bằng cách nướng ánh sáng trong cảnh. Phần thảo luận đầy đủ về nướng nhẹ nằm ngoài phạm vi của bài viết này, nhưng một số yếu tố chính của nó sẽ được thảo luận về cách sử dụng chúng để cải thiện hình ảnh cho địa hình.

Đầu tiên, Light Baking là một quá trình trong đó chúng tôi tính toán trước ánh sáng trong một cảnh và lưu trữ dữ liệu để sau đó áp dụng nó lên trên các đối tượng trong cảnh. Unity đặc biệt sử dụng Ánh xạ ánh sáng, cung cấp độ sáng của bề mặt đối tượng trong bản đồ kết cấu.

Tuy nhiên, những kết cấu này không thể thay đổi trong thời gian chạy và cần được tính toán lại trước cho mọi thay đổi trong cảnh, chẳng hạn như di chuyển các đối tượng đến một nơi khác hoặc cập nhật sự biến đổi của đèn. Ngoài ra, nó chỉ được áp dụng cho các đối tượng được gắn cờ là tĩnh.

Tab chiếu sáng

Nướng ánh sáng có thể được thực hiện trong tab Chiếu sáng (bạn có thể truy cập nó thông qua menu Cửa sổ , tiếp theo là Kết xuất và sau đó là Chiếu sáng ). Các giá trị mặc định của Unity cho đèn nướng khá cao và có thể mất quá nhiều thời gian để xử lý. Vì vậy, tôi khuyên bạn nên giảm tất cả các Mẫu trực tiếp , Mẫu gián tiếpMẫu môi trường xuống một con số thấp hơn, chẳng hạn như 16 hoặc 32.

Các thuộc tính đó có liên quan trực tiếp đến số bước Lightmapper sẽ thực hiện để tính toán ánh sáng tại bất kỳ điểm nhất định nào trong cảnh (đối với các phần tử tĩnh). Tăng bất kỳ loại nào trong số chúng sẽ mang lại kết quả tốt hơn và thực tế hơn nhưng sẽ làm tăng đáng kể thời gian nướng.

Tôi thường bắt đầu với các giá trị thấp để đánh giá ấn tượng đầu tiên trong cảnh và sau đó tăng dần chúng khi tôi thấy phù hợp. Đối với nhiều phong cách hoặc vật liệu tùy chỉnh hơn, việc sử dụng các giá trị thấp hơn thường là đủ để đạt được hình ảnh tốt.

Hơn nữa, những lỗi đầu tiên có thể cho bạn thấy sự mâu thuẫn trong vị trí của các đối tượng và các lỗi khác cần được sửa trước khi áp dụng quy trình lập bản đồ ánh sáng thực tế và kỹ lưỡng hơn.

Tôi cũng khuyên bạn nên bật chế độ nướng ánh sáng Môi trường xung quanh trong thuộc tính Chiếu sáng để nướng. Hiện tượng xung quanh là hiệu ứng hình ảnh xảy ra khi các bề mặt ở gần nhau, che khuất ánh sáng và chiếu bóng vào các điểm giao nhau của chúng, làm cho chúng có vẻ tối hơn. Trước đây, chúng tôi đã làm giả nó bằng cách sử dụng màu sắc của lớp địa hình tối hơn, nhưng ánh sáng lập bản đồ thực sự có thể tính toán sự tắc nghẽn của môi trường xung quanh thích hợp và biến nó thành các kết cấu.

Các hình ảnh dưới đây cho thấy kết quả có và không có nướng nhẹ.

Không cần nướng nhẹ

Không cần nướng nhẹ.

Với Nướng nhẹ

Với nướng nhẹ.

Như bạn có thể thấy, không có môi trường xung quanh tắc nghẽn giữa thân cây và mặt đất. Tuy nhiên, phần đáy của vạc và các điểm giao nhau giữa các thùng ở mặt sau hình ảnh đậm hơn và chân thực hơn so với phiên bản không nướng của chúng.

Đặt skybox và áp dụng xử lý hậu kỳ

Cuối cùng, để cải thiện hình ảnh hơn nữa và gắn kết các yếu tố với nhau, hai bước sau là điều cần thiết: đặt một skybox tốt hơn và áp dụng một vài hiệu ứng xử lý hậu kỳ.

Đối với bài viết này, tôi đã sử dụng Skybox cách điệu miễn phí từ Yuki2022. Những hộp bầu trời này khá phù hợp với phong cách low poly khá phù hợp với giao diện hoạt hình của chúng.

Bạn có thể thêm skybox trong tab Lighting (giống như Light Baking), trong tab Môi trường .

Các hiệu ứng xử lý hậu kỳ đòi hỏi nhiều bước hơn. Đầu tiên, bạn phải thêm gói xử lý hậu kỳ vào dự án của mình tùy thuộc vào đường dẫn kết xuất của bạn đã được cài đặt. Sử dụng Đường ống kết xuất tích hợp, bạn cần cài đặt Ngăn xếp sau xử lý trong dự án của mình bằng Trình quản lý gói.

Mặt khác, nếu bạn đang sử dụng Universal Render Pipeline hoặc High Definition Render Pipeline, bạn đã cài đặt hệ thống xử lý hậu kỳ tương đương. Tôi đang sử dụng Universal Render Pipeline cho bài viết này, nhưng các hiệu ứng và cấu hình tương tự như các đường ống khác.

Hình ảnh bên dưới cho thấy khối lượng xử lý hậu kỳ cuối cùng mà tôi đã sử dụng cho phiên bản cuối cùng của địa hình:

Cài đặt âm lượng

Các hiệu ứng được sử dụng là:

  • Làm mờ nét ảnh: làm tối các góc của màn hình, tập trung sự chú ý của người chơi vào các yếu tố trung tâm
  • Bloom : mở rộng các yếu tố nổi bật trong cảnh, làm mờ các khu vực của chúng với các đối tượng lân cận, hoạt động như một giải pháp thay thế chiếu sáng toàn cầu giả (nhưng tốt)
  • Độ sâu trường ảnh : bắt chước tiêu điểm của mắt chúng ta và máy ảnh thực bằng cách làm cho các yếu tố gần chúng ta hơn (trong tiêu điểm) trở nên sắc nét, trong khi các yếu tố khác bị mờ theo khoảng cách của chúng
  • Đường cong màu : thay đổi màu sắc cuối cùng được hiển thị để phù hợp với giao diện mong muốn hơn. Trong trường hợp này, tôi tăng màu đỏ lên các màu khác và tăng một chút màu xanh lam
  • Lift Gamma Gain : thay đổi màu tổng thể của kết xuất, cũng như tăng khả năng kiểm soát tông màu tối, tông màu trung bình và vùng sáng

Kết quả cuối cùng cho hai địa điểm mà chúng tôi đã thực hiện trong bài viết này có thể được nhìn thấy trong các hình ảnh dưới đây:

Môi trường địa hình

Ví dụ về địa hình cuối cùng

Cảm ơn bạn đã đọc và hãy cho tôi biết nếu bạn muốn đọc thêm về nhiều chủ đề được thảo luận nằm ngoài phạm vi của bài viết này. Mỗi người trong số họ chắc chắn xứng đáng với bài viết của riêng họ. Bạn có thể xem thêm công việc của tôi tại www.dagongraphics.com .

 Nguồn: https://blog.logrocket.com/easy-enosystem-design-unity-terrain-features/

#unity #design 

Thiết Kế Môi Trường Dễ Dàng Với Các Tính Năng Unity Terrain

Fácil Diseño De Entornos Con Características De Unity Terrain

Unity Terrain es una herramienta nativa, potente y versátil de Unity para el diseño de niveles y entornos . Proporciona mecanismos sencillos para moldear y modificar terrenos utilizando el concepto de mapas de altura.

En este artículo, veremos los conceptos básicos de cómo usar esta herramienta y sus principales características. Comenzamos con la teoría de cómo funciona la generación de terreno usando mapas de altura. Luego discutiremos los mecanismos primarios para modificar un terreno. Al final del artículo, presento un flujo de trabajo típico para crear bonitas escenas de Unity utilizando recursos gratuitos y la herramienta Unity Terrain.

Ejemplo de terreno terminado

De mapas de altura a terrenos

Un mapa de altura es una textura que cambia la forma en que el jugador ve una malla específica, similar a cómo funcionan los mapas normales. Más específicamente, la información del mapa de altura cambia los vértices de la malla para mostrar características como elevaciones, acantilados, llanuras y cráteres.

Debido a su naturaleza 2D, un mapa de una sola altura solo puede interferir con un eje de los vértices a la vez. En particular, el mapa de altura de Unity Terrain altera la posición de los vértices en el eje y.

Cuando trabajamos con Unity Terrain elevando o bajando sus partes, técnicamente estamos alterando su mapa de altura. La alteración actúa inmediatamente, y podemos ver los resultados de nuestro cambio en el terreno. Alternativamente, también es posible cambiar el mapa de altura directamente fuera de Unity e importar mapas de altura al motor.

Por ejemplo, el sitio web Cities: Skylines Height Map Generator proporciona mecanismos para extraer información de mapas de altura del mundo real. Aunque el sitio web está diseñado para usarse en el juego Cities: Skylines , los mapas funcionan igual de bien en Unity. Sin embargo, tenga en cuenta que contendrán distancias del mundo real y es posible que deban ajustarse en una herramienta externa para un uso óptimo en Unity.

Las imágenes a continuación muestran un mapa de altura extraído del sitio web e importado a Unity Terrain.

Mapa de altura en Unity

Mapa de altura en un terreno de unidad.

Mapa de altura visualizado

Mapa de altura visualizado (valores ajustados para mejorar la visibilidad).

Configuración del terreno de unidad

Cuando se selecciona, un terreno de unidad se puede editar mediante la barra de herramientas de terreno. La barra de herramientas contiene las principales funcionalidades para el terreno agrupadas en 5 secciones: Azulejos de Terreno Adyacentes ; esculpir y pintar ; Agregar árboles ; Agregar detalles ; y Ajustes generales .

Configuración de la barra de herramientas

 

Los mosaicos de terreno adyacentes le permiten crear otros terrenos en un formato similar a una cuadrícula cerca del terreno actual. Es beneficioso usarlo cuando ya tiene un terreno bien establecido pero necesita más espacio al lado.

Esa es una solución más adecuada que aumentar el tamaño del terreno. Alterar el tamaño afectará cómo se lee el mapa de altura y se aplicará un factor de escala proporcional al terreno y todos sus elementos.

Con respecto a los mapas de altura, la creación de un nuevo terreno adyacente creará un nuevo objeto de juego de terreno con su mapa de altura. El mapa de altura recién creado intentará hacer coincidir los valores en su borde, acoplándolo perfectamente al terreno anterior al que estaba conectado.

La opción Esculpir y pintar funciona directamente con el mapa de altura. Esta opción contiene sus herramientas de terreno , que cubriremos en la siguiente sección. En resumen, esta opción nos permite deformar el mapa de alturas y ocultar partes del mismo para crear huecos y entradas, por ejemplo.

Ambas opciones para Agregar árboles y Agregar detalles se utilizan para poblar el terreno con elementos que Unity maneja automáticamente, como árboles, arbustos, arbustos, piedras, parches de césped y otros. Una ventaja de usar Unity Terrain sobre el uso de objetos de juego colocados por el usuario en la escena es que Unity administra muchas técnicas de optimización para los elementos de un terreno. Los árboles y los detalles se tienen en cuenta automáticamente en los algoritmos y vallas publicitarias relacionados con la selección de visibilidad .

Finalmente, la opción Configuración general contiene toda la información relevante sobre el objeto del juego de terreno, como las técnicas de optimización (distancia detallada, distancia de vallas publicitarias, sombras, etc.) y el tamaño del terreno.

Los Ajustes generales se dividen en Terreno básico , Objetos de árbol y detalles , Ajustes de viento para hierba , Resolución de malla , Ajustes de agujeros , Resoluciones de textura , Iluminación y Mapeo de luz . Está fuera del alcance de esta pieza cubrir cada opción extensamente, pero repasaremos las más utilizadas.

Configuración del terreno

La sección Terreno básico cubre los principales aspectos de representación del terreno, como el material del terreno, sus propiedades de sombra y el modo de dibujo utilizado. Tenga en cuenta que si espera usar sombreadores de posprocesamiento hechos a sí mismos o pases de renderizado con secuencias de comandos, la opción Dibujar instanciado debe estar deshabilitada.

Los valores de Resolución de malla determinan el tamaño del terreno. Cambiar el ancho y el largo del terreno cambiará inmediatamente cómo se leerá la información del mapa de altura. El ancho, la longitud y la altura del terreno determinan el tamaño del terreno en los ejes x, y y z.

La sección Resoluciones de textura maneja la información de textura de mapa de altura más específica. Puede establecer la resolución para el mapa de altura e importar/exportar mapas de altura allí. Usando la función de importación, puede usar los datos adquiridos de otras fuentes (como la presentada anteriormente) en su mapa. También es común exportar el mapa de altura actual, modificarlo en una herramienta externa y luego volver a importarlo.

Opciones para esculpir y alterar el terreno.

Como se dijo anteriormente, la opción Esculpir y pintar nos permite alterar el terreno cambiando el mapa de altura. Estas opciones tienen seis herramientas: Elevar o bajar terreno , Pintar agujeros , Pintar textura , Establecer altura , Suavizar altura y Estampar terreno .

Herramienta para esculpir y pintar

Las opciones para Subir o Bajar Terreno , Establecer Altura y Suavizar Altura se usan para afectar directamente el mapa de altura aumentando o disminuyendo sus valores. Establecer altura se usa para cambiar el mapa de altura para lograr un resultado deseado específico, mientras que Suavizar altura suaviza el terreno como si desenfocara las áreas seleccionadas en el mapa de altura. Todas estas opciones se realizan mediante el uso de pinceles de terreno (como se ve en la imagen de arriba).

Los pinceles de terreno son similares a cómo funcionan los pinceles en otro software de edición de imágenes, como GIMP y Photoshop. Un pincel presenta un patrón en tonos de gris y alfa, que se aplica al terreno para moldearlo usando sus valores. Las áreas transparentes del pincel no afectarán el terreno, y las áreas más oscuras afectarán el terreno más que las áreas más claras. El conjunto de pinceles de terreno predeterminado de Unity incluye más de diez pinceles.

La opción Pintar textura se usa para aplicar capas de textura en el terreno, agregando activamente color y otras propiedades de textura. Las capas de textura se aplican utilizando los mismos pinceles de terreno que se utilizan para las otras herramientas de esculpir y pintar. Sin embargo, el terreno predeterminado de Unity viene sin capa de textura.

Capas de terreno

Una lista de capas de terreno.

Una capa de terreno

Las propiedades de una capa de terreno.

Cada capa de terreno es un recurso propio y puede ser reutilizada por múltiples terrenos, incluso en diferentes escenas. La primera capa de terreno agregada cubrirá automáticamente todo el terreno. Úselo como una forma rápida de configurar sus colores base. Cada capa de terreno puede tener mapas difusos , normales y de máscara .

El mapa difuso es la textura que muestra el terreno. Una vez configurado, la opción para cambiar su tinte se vuelve visible. Cambiar el tinte es una forma rápida y efectiva de crear variaciones de terreno sin tener que recurrir al uso de más texturas o editar sus recursos en programas externos.

Mientras que el mapa normal se usa para transmitir información normal en la capa y, en general, funciona igual que un mapa normal normal, el mapa de máscara se usa explícitamente para las canalizaciones de renderizado universal y de alta definición para transmitir más información, como oclusión ambiental metálica, altura y suavidad.

Finalmente, la configuración de mosaicos es una opción útil para la capa de terreno. Los valores de tamaño y desplazamiento alteran la frecuencia con la que la textura se colocará en mosaico en el terreno y con qué desplazamiento base para cada repetición.

Generalmente, para terrenos grandes, es bueno tener un par de variaciones de las capas del terreno con valores variados para el tamaño, ya que puede ayudar a romper la repetición visible en el suelo del terreno.

Como se indicó anteriormente, los pinceles de terreno vienen de forma predeterminada con el terreno de Unity, pero no hay capas de terreno al crear un nuevo objeto de terreno.

Además, es necesario tener mapas y texturas para crear tus capas de terreno. Para este artículo, he usado un par de nuevos pinceles gratuitos de la tienda de activos ( Generic Terrain Brushes de Flaming Sands y StampIT! Collection Examples de Rowlan Inc), así como un paquete de texturas gratuitas para las capas del terreno ( Handpainted Grass & Ground Texturas de Chromisu).

Los resultados para el terreno base con algunas texturas se pueden ver a continuación:

Terreno base con texturas

Mientras trabajo en el terreno, generalmente trato de equilibrar las áreas pico y las áreas planas. Eso tiende a darme suficiente espacio para esparcir vegetación y no abarrotar el espacio. Además, trato de tener un elemento clave en el terreno para mantener sus principales componentes y enfoque, como una montaña, un claro o un río, por ejemplo.

Tener un componente principal ayuda a la toma de decisiones sobre qué activos usar y cómo distribuirlos. En este caso, vamos a hacer una pequeña colina con una entrada en forma de cueva para explorar más opciones de terreno y algunas áreas pequeñas de interés para diversificar la vista.

Además, observe que traté de explorar las múltiples capas del terreno para romper la repetición visual que tienden a hacer. En esta etapa, solo trato de bloquear los lugares principales, separando las colinas de la hierba normal y las áreas planas, así como rociar algo de diversidad aquí y allá con un poco de arena y parches más oscuros de suelo de hierba.

¿Plantar árboles, césped y… piedras?

La vegetación se puede agregar de dos maneras, ya sea con la opción Agregar árbol o con la opción Agregar detalle . Agregar árbol es bastante sencillo y le permite colocar objetos con características de árbol.

Para lograr una mejor sinergia, los árboles deben crearse con las herramientas de Unity, como el árbol de velocidad o el editor de árboles , o con los materiales específicos del árbol del terreno. Sin embargo, eso no es obligatorio, y en las siguientes secciones, mostraré otro enfoque que produce resultados similares y permite una mayor libertad creativa.

En cualquier caso, los árboles de terreno tienen una limitación de un máximo de 2 materiales por malla (uno para la corteza y otro para las hojas). Si su malla tiene más de dos materiales, es posible que no se represente correctamente. Una solución habitual a esta limitación es recurrir al atlas de texturas. Además, es común separar los materiales de las hojas del material de la corteza para lograr un efecto más interesante, lo que nos permite programar sombreadores solo para las hojas (como simular el movimiento del viento).

Árboles

De manera similar a las capas de terreno, los árboles también deben agregarse al terreno individualmente y luego colocarse usando los pinceles del terreno. Los árboles se pueden agregar como mallas desnudas y también como prefabricados.

Tenga en cuenta que agregar un árbol como prefabricado no necesariamente agregará todos los elementos prefabricados ni se comportará exactamente como su prefabricado previsto. Por ejemplo, colocar un árbol prefabricado con un animador ignorará al animador y actuará como un árbol estático.

rocas y hierba

De manera similar, los detalles son elementos que se pueden colocar en el terreno para diversificar sus visuales. Los detalles pueden ser de dos tipos : malla de detalle y textura de hierba. Detail Mesh funciona como mallas regulares que permanecen estáticas en la escena, como piedras, guijarros e incluso arbustos.

Las mallas de detalle están limitadas a un material. Si intenta utilizar mallas con más de un material, no se renderizarán correctamente.

Las mallas de detalle también se pueden renderizar como Vertex Lit o Grass. Vertex Lit representará la malla como Game Objects iluminados regulares y no reaccionará al viento, lo cual es apropiado para piedras y tocones de árboles. El renderizado de césped funciona de forma similar a Texturas de césped, lo que permite que el viento del terreno afecte a la malla.

Sin embargo, por experiencia, el renderizado de césped solo produce resultados agradables a la vista cuando la propiedad de curvatura del viento se establece en un valor bajo (en la Configuración general del terreno ). De lo contrario, la malla de detalle podría moverse de forma poco realista.

Animación de césped suave en Unity

Las texturas de hierba son sprites representados en el terreno que se comportan de acuerdo con el viento del terreno. Como se indica en la documentación de Unity, el término "Textura de hierba" es engañoso ya que puede usar cualquier textura genérica, como flores o palos, con la misma herramienta. A diferencia de las mallas de detalle renderizadas como hierba, las texturas de hierba mantendrán su pivote en el suelo y se moverán con el viento manteniendo la misma posición, como se esperaría que se comportara la hierba.

Para este artículo, he usado árboles y otras mallas de la tienda de activos ( Low-Poly Simple Nature Pack de JustCreate y Low Poly Rock Pack de Broken Vector). Para las texturas de la hierba, utilicé los excelentes Foliage Sprites de Kenney, una gran fuente de activos de alta calidad libres de derechos de autor. Finalmente, como activos de decoración adicionales, he utilizado Low Poly Dungeons Lite de JustCreate.

Aplicando estos a nuestro terreno, tenemos el siguiente resultado:

Resultado del terreno

Árboles más versátiles con LOD Groups

Como se dijo anteriormente, los árboles de Unity están de alguna manera limitados debido a las optimizaciones realizadas por el sistema de terreno. Si usa materiales personalizados u otras variaciones, es posible que se ignoren algunas opciones de colocación de árboles, lo que generará una escena muy suave y repetitiva.

Sin embargo, el sistema también nos permite eludir algunas de sus restricciones para lograr un mayor nivel de versatilidad en cuanto al uso de materiales y estilo. Para eso, tenemos que seguir un proceso específico.

Primero, debe crear un nuevo prefabricado para el árbol en el que el objeto de juego raíz (el primero en la jerarquía) es un objeto de juego vacío con el componente LOD Group . Luego, puede colocar su árbol prefabricado en la jerarquía debajo del objeto del juego raíz. Después de eso, agregue el árbol prefabricado al grupo LOD, y eso es todo.

El árbol prefabricado funcionará con sus sombreadores personalizados y recibirá las propiedades regulares de colocación de árboles del terreno, como cambiar el ancho, la altura y la rotación del árbol. Para la variación de color, es necesario modificar el sombreador para que lea la propiedad _TreeInstanceColor, pero este paso queda fuera del alcance de este artículo.

Por supuesto, puede usar las funciones de LOD Group para manejar el otro nivel de detalles de su árbol. Sin embargo, si simplemente desea un material más versátil en sus árboles mientras sigue usando las propiedades de ubicación del terreno, puede cambiar la cantidad de LOD a 1 y usar solo un árbol prefabricado.

Lograr estos pasos debería llevarlo a un resultado similar al siguiente:

Árbol LOD

Flujo de trabajo de terreno propuesto para mejores resultados

Ahora que hemos cubierto los aspectos principales de cómo usar la herramienta de terreno, quiero ofrecer un flujo de trabajo simple que uso para obtener resultados razonablemente buenos sin mucho esfuerzo, al mismo tiempo que cubro algunos otros aspectos de representación que pueden mejorar su entorno.

Hacer una entrada a la cueva

Comencemos con la cueva que mencioné anteriormente. El Unity Terrain no nos permite crear entradas o tallar el mapa de altura horizontalmente (aplicando información de altura en el plano xz). Sin embargo, nos permitirá pintar agujeros en él.

Los agujeros en Unity Terrain ocultan partes de la malla del terreno como si estuviéramos cortando piezas. Los agujeros se pueden pintar como los otros elementos en las herramientas Esculpir terreno con pinceles de terreno.

Para hacer una entrada a la cueva visualmente emocionante, le recomiendo que eleve el terreno, manteniendo la elevación en un ángulo de entre 40 y 60 grados. Más que eso podría estirar demasiado la cuadrícula del terreno y hacer que sea más difícil cubrirla con otros elementos y casas prefabricadas. Al hacerlo, deberíamos obtener un resultado como el siguiente:

Desgarro en el terreno

Como puede ver, los agujeros de terreno ocultan partes del terreno de forma binaria: desaparecen por completo o son completamente visibles. Para eso, es necesario cubrir las costuras con activos y otros elementos para lograr una mejor calidad visual.

Vista del Terreno

Usando los mismos activos gratuitos mencionados anteriormente, escalé y roté diferentes piedras alrededor de la entrada de la cueva para ocultar las costuras entre los agujeros y el terreno visible. También traté de agregar más rocas y guijarros de lo necesario para crear una apariencia más natural. Es común ajustar el mapa de altura para que se ajuste mejor a la conexión con las rocas.

Además, agregar accesorios adicionales alrededor de las áreas de interés siempre es bueno para aumentar la concentración del jugador. Para eso, añadí unas tinajas semienterradas en el suelo y un pilar justo en frente de la cueva, lo que ayuda a crear algo de misterio y narrativa. Agregué una pequeña luz al lado de la entrada de la cueva para guiar los ojos aún más.

Luz en la entrada de la cueva

Como se vio antes, el agujero también permite al usuario ver a través del terreno. Para evitar eso, utilicé planos con un material específico que ignora la iluminación (Unlit). Eso es importante porque, de lo contrario, los cambios en la luz de la escena mostrarían estas paredes oscuras como sólidas, mientras que el resultado visual que queremos es que se vea como un área oscura/sombra.

Además, he usado algunas de las mallas del paquete Low Poly para que sirvan como suelo de la cueva ya que no podemos usar el mismo terreno que hemos estado usando hasta ahora.

Es importante tener en cuenta que la cueva que hicimos hasta ahora no es ideal si desea que su jugador ingrese y navegue dentro de ella. Para eso, necesitaría colocar correctamente las mallas alrededor del agujero, modelando un poco el interior de la cueva desde todos los lados. Sería posible lograr eso usando un nuevo objeto de terreno, pero creo que el trabajo requerido para eso podría no ser óptimo.

Por otro lado, esta solución funciona bastante bien para que la uses como un punto para mover al jugador a una nueva escena dedicada al interior de la cueva.

Oclusión ambiental manual

Ejemplo de oclusión ambiental

Ejemplo de oclusión ambiental 2

Una técnica común que uso para mejorar la iluminación en Unity Terrain es usar manualmente una variación más oscura de la capa de terreno debajo de los objetos colocados en el suelo. Eso ayuda a elevar la cohesión de los elementos del terreno, ya que algunos efectos de iluminación, como la oclusión ambiental, no se aplican a los objetos del terreno, especialmente si usa sombreadores personalizados y otras técnicas.

Oclusión de ejemplo de terreno

Tenga en cuenta que esto es diferente a proyectar sombras. Los terrenos de Unity proyectarán las sombras de los objetos correctamente, pero eso aún podría causar la impresión de que los objetos están flotando o no están realmente conectados. El uso de una capa de terreno oscuro en algunos puntos ayuda a acercar los objetos y el suelo sin requerir más recursos o cálculos de iluminación costosos.

Además, a menudo aplico esto a la parte inferior de otros objetos y prefabricados que no son objetos de terreno. Como se ve en la imagen de arriba, usé un poco del tono más oscuro debajo de los libros y el caldero para ayudar a acercar los elementos.

Oclusión ambiental real y horneado ligero.

Las capas más oscuras ayudarán con la iluminación falsa hasta cierto punto, pero se pueden amplificar horneando la luz en la escena. Una discusión completa sobre el horneado ligero está fuera del alcance de este artículo, pero se discutirán algunos de sus elementos principales sobre cómo usarlos para mejorar las imágenes de un terreno.

Primero, Light Baking es un proceso en el que precalculamos la luz en una escena y almacenamos los datos para luego aplicarlos sobre los objetos en la escena. Unity usa específicamente Lightmapping , que hornea el brillo de la superficie del objeto en mapas de textura.

Sin embargo, estas texturas no se pueden cambiar en tiempo de ejecución y deben calcularse previamente nuevamente para cada cambio en la escena, como mover objetos a un lugar diferente o actualizar la transformación de las luces. Además, solo se aplica a los objetos marcados como estáticos.

Pestaña de iluminación

La cocción ligera se puede realizar en la pestaña Iluminación (puede acceder a ella a través del menú Ventana , seguido de Renderizado y luego Iluminación ). Los valores predeterminados de Unity para el horneado ligero son considerablemente altos y pueden tardar demasiado en procesarse. Para eso, le recomiendo que reduzca todas las muestras directas , las muestras indirectas y las muestras ambientales a un número más bajo, como 16 o 32.

Esos atributos están directamente relacionados con cuántos pasos tomará Lightmapper para calcular la luz en cualquier punto dado de la escena (para elementos estáticos). Aumentar cualquiera de ellos producirá resultados mejores y más realistas, pero aumentará considerablemente el tiempo de horneado.

Suelo empezar con valores bajos para valorar las primeras impresiones de la escena y luego ir aumentándolos progresivamente según me parezca. Para materiales más estilísticos o personalizados, el uso de valores más bajos suele ser suficiente para lograr buenas imágenes.

Además, los primeros horneados pueden mostrarle inconsistencias en la colocación de objetos y otros errores que deben corregirse antes de aplicar un procedimiento de mapeo de luz más completo y realista.

También recomiendo encender la oclusión ambiental de horneado ligero en las propiedades de iluminación para el horneado. La oclusión ambiental es el efecto visual que ocurre cuando las superficies están cerca unas de otras, ocluyendo la luz y proyectando sombras en sus puntos de intersección, haciéndolas parecer más oscuras. Anteriormente lo falsificamos usando colores de capa de terreno más oscuros, pero el mapeo de luz en realidad puede calcular la oclusión ambiental adecuada y convertirla en texturas.

Las imágenes a continuación muestran los resultados con y sin horneado ligero.

Sin horneado ligero

Sin horneado ligero.

con horneado ligero

Con horneado ligero.

Como puede ver, no hay oclusión ambiental entre los troncos de los árboles y el suelo. Sin embargo, la parte inferior del caldero y las intersecciones entre las cajas en la parte posterior de la imagen son más oscuras y realistas que su versión sin hornear.

Colocación de un palco y aplicación de posprocesamiento

Finalmente, para mejorar aún más las imágenes y unir los elementos, estos dos pasos son esenciales: colocar un palco mejor y aplicar algunos efectos de procesamiento posterior.

Para este artículo, utilicé el Skybox estilizado gratuito de Yuki2022. Estos palcos encajan bastante bien en el estilo low poly por su aspecto de dibujos animados.

El skybox se puede agregar en la pestaña Iluminación (igual que Light Baking), en la pestaña Entorno .

Los efectos de posprocesamiento requieren más pasos. Primero, debe agregar el paquete de posprocesamiento a su proyecto según la canalización de representación instalada. Con el canal de procesamiento incorporado, debe instalar la pila de posprocesamiento en su proyecto con el administrador de paquetes.

De lo contrario, si está utilizando Universal Render Pipeline o High Definition Render Pipeline, ya tiene instalado el sistema de posprocesamiento equivalente. Estoy usando Universal Render Pipeline para este artículo, pero los efectos y las configuraciones son similares a los de otras canalizaciones.

La siguiente imagen muestra el volumen final de procesamiento posterior que utilicé para la versión final del terreno:

Configuración de volumen

Los efectos utilizados fueron:

  • Viñeta : oscurece las esquinas de la pantalla, enfocando la atención del jugador en los elementos centrales
  • Bloom : expande los elementos destacados en la escena, difuminando sus áreas con los objetos vecinos, lo que actúa como una alternativa de iluminación global falsa (pero buena)
  • Profundidad de campo : imita el enfoque de nuestros ojos y cámaras reales al hacer que los elementos más cercanos a nosotros (en el punto focal) sean nítidos, mientras que otros elementos se desenfocan según su distancia
  • Curvas de color : altera los colores finales representados para que coincidan con un aspecto más deseado. En este caso, aumenté el color rojo sobre otros y aumenté ligeramente el color azul.
  • Levantar ganancia gamma : cambia el color general del renderizado, así como también aumenta el control de los tonos oscuros, los tonos de rango medio y las luces.

Los resultados finales para las dos ubicaciones que hicimos durante este artículo se pueden ver en las imágenes a continuación:

Entorno del terreno

Ejemplo de terreno final

Gracias por leer, y avíseme si desea leer más sobre los muchos temas discutidos que estaban fuera del alcance de este artículo. Cada uno de ellos sin duda merece su propio artículo. Puedes ver más de mi trabajo en www.dagongraphics.com .

 Fuente: https://blog.logrocket.com/easy-environment-design-unity-terrain-features/

 #unity #design 

Fácil Diseño De Entornos Con Características De Unity Terrain
Mélanie  Faria

Mélanie Faria

1660303800

Design De Ambiente Fácil Com Recursos Do Unity Terrain

O Unity Terrain é uma ferramenta nativa da Unity, poderosa e versátil para design de nível e ambiente. Ele fornece mecanismos fáceis para moldar e modificar terrenos usando o conceito de mapas de altura.

Neste artigo, veremos o básico de como usar essa ferramenta e suas principais funcionalidades. Começamos com a teoria de como funciona a geração de terreno usando mapas de altura. Em seguida, discutiremos os principais mecanismos para modificar um terreno. No final do artigo, apresento um fluxo de trabalho típico para criar belas cenas do Unity usando recursos gratuitos e a ferramenta Unity Terrain.

Exemplo de terreno concluído

De mapas de altura a terrenos

Um mapa de altura é uma textura que muda a forma como uma malha específica é vista pelo jogador, semelhante à forma como os mapas normais funcionam. Mais especificamente, as informações do mapa de altura deslocam os vértices da malha para exibir recursos como elevações, penhascos, planícies e crateras.

Devido à sua natureza 2D, um mapa de altura única pode interferir apenas em um eixo dos vértices por vez. Notavelmente, o mapa de altura do Unity Terrain altera a posição dos vértices no eixo y.

Quando trabalhamos com um Unity Terrain elevando ou abaixando suas partes, estamos alterando tecnicamente seu mapa de altura. A alteração age imediatamente, e podemos ver os resultados de nossa mudança no terreno. Como alternativa, também é possível alterar o mapa de altura diretamente fora do Unity e importar mapas de altura para o mecanismo.

Por exemplo, o site Cities: Skylines Height Map Generator fornece mecanismos para extrair informações do mapa de altura do mundo real. Embora o site deva ser usado no jogo Cities: Skylines , os mapas funcionam tão bem no Unity. Considere, no entanto, que eles conterão distâncias do mundo real e podem precisar ser ajustados em uma ferramenta externa para uso ideal no Unity.

As imagens abaixo mostram um mapa de altura extraído do site e importado para um Unity Terrain.

Mapa de altura no Unity

Mapa de Altura em um Terreno Unity.

Mapa de Altura Visualizado

Mapa de Alturas visualizado (valores ajustados para melhorar a visibilidade).

Configurações do terreno do Unity

Quando selecionado, um terreno do Unity pode ser editado usando a barra de ferramentas do terreno. A barra de ferramentas contém as principais funcionalidades do terreno agrupadas em 5 seções: Adjacent Terrain Tiles ; Esculpir e Pintar ; Adicionar Árvores ; Adicionar detalhes ; e Configurações Gerais .

Configurações da barra de ferramentas

 

As telhas de terreno adjacentes permitem que você crie outros terrenos em um formato semelhante a uma grade perto do terreno atual. É benéfico usá-lo quando você já tem um terreno bem estabelecido, mas precisa de mais espaço próximo a ele.

Essa é uma solução mais adequada do que aumentar o tamanho do terreno. Alterar o tamanho afetará a leitura do mapa de altura e um fator de escala proporcional será aplicado ao terreno e a todos os seus elementos.

Em relação aos mapas de altura, criar um novo terreno adjacente criará um novo objeto de jogo de terreno com seu mapa de altura. O mapa de altura recém-criado tentará corresponder aos valores em sua borda, acoplando-o perfeitamente ao terreno anterior ao qual estava conectado.

A opção Esculpir e Pintar funciona diretamente com o mapa de altura. Esta opção contém suas Ferramentas de Terreno , que abordaremos na seção a seguir. Resumindo, esta opção nos permite deformar o mapa de altura e ocultar partes dele para criar buracos e entradas, por exemplo.

Ambas as opções para Adicionar Árvores e Adicionar Detalhes são usadas para preencher o terreno com elementos que são manipulados automaticamente pelo Unity, como árvores, arbustos, arbustos, pedras, manchas de grama e outros. Uma vantagem de usar o Unity Terrain sobre o uso de objetos de jogo colocados pelo usuário na cena é que o Unity gerencia muitas técnicas de otimização para os elementos de um terreno. Árvores e detalhes são automaticamente contabilizados em algoritmos relacionados à seleção de visibilidade e outdoors .

Por fim, a opção Configurações Gerais contém todas as informações relevantes sobre o objeto do jogo terreno, como as técnicas de otimização (distância do detalhe, distância do outdoor, sombras, etc.) e o tamanho do terreno.

As Configurações Gerais são divididas em Terrano Básico , Objetos de Árvore e Detalhes , Configurações de Vento para Grama , Resolução de Malha , Configurações de Furo , Resoluções de Textura , Iluminação e Mapeamento de Luz . Está fora do escopo desta peça cobrir cada opção extensivamente, mas passaremos pelas mais usadas.

Configurações de terreno

A seção Terreno Básico cobre os principais aspectos de renderização do terreno, como o material do terreno, suas propriedades de sombra e o modo de desenho usado. Observe que, se você espera usar shaders de pós-processamento feitos por conta própria ou passes de renderização com script, a opção Desenhar instância precisa ser desativada.

Os valores de resolução de malha determinam o tamanho do terreno. Alterar a largura e o comprimento do terreno alterará imediatamente a forma como as informações do mapa de altura serão lidas. A largura, comprimento e altura do terreno determinam o tamanho do terreno nos eixos x, y e z.

A seção Resoluções de textura lida com as informações de textura do mapa de altura mais específicas. Você pode definir a resolução para o mapa de altura e importar/exportar mapas de altura lá. Usando a função de importação, você pode usar os dados adquiridos de outras fontes (como a apresentada anteriormente) em seu mapa. Também é comum exportar o mapa de altura atual, alterá-lo em uma ferramenta externa e depois importá-lo de volta.

Opções para esculpir e alterar o terreno

Como dito anteriormente, a opção Sculpt and Paint nos permite alterar o terreno alterando o mapa de altura. Essas opções têm seis ferramentas: Elevar ou Abaixar Terreno , Pintar Furos , Pintar Textura , Definir Altura , Suavizar Altura e Marcar Terreno .

Ferramenta Esculpir e Pintar

As opções para Elevar ou Abaixar Terreno , Definir Altura e Suavizar Altura são todas usadas para afetar diretamente o mapa de altura aumentando ou diminuindo seus valores. Definir Altura é usado para alterar o mapa de altura para atender a um resultado desejado específico, enquanto Suavizar Altura suaviza o terreno como se estivesse desfocando as áreas selecionadas no mapa de altura. Todas essas opções são feitas usando os Pincéis de Terreno (como visto na imagem acima).

Os pincéis de terreno são semelhantes ao modo como os pincéis funcionam em outros softwares de edição de imagens, como o GIMP e o Photoshop. Um pincel apresenta um padrão em tons de cinza e alfa, que é aplicado ao terreno para moldá-lo usando seus valores. As áreas transparentes do pincel não afetarão o terreno, e as áreas mais escuras afetarão mais o terreno do que as áreas mais claras. O conjunto de pincéis de terreno padrão do Unity vem com mais de dez pincéis.

A opção Pintar textura é usada para aplicar camadas de textura no terreno, adicionando cor a ele e outras propriedades de textura. As camadas de textura são aplicadas usando os mesmos pincéis de terreno usados ​​para as outras ferramentas Esculpir e Pintar. No entanto, o terreno padrão do Unity vem sem camada de textura.

Camadas de Terreno

Uma lista de camadas de terreno.

Uma camada de terreno

As propriedades para uma camada de terreno.

Cada camada de terreno é um ativo próprio e pode ser reutilizada por vários terrenos, mesmo em cenas diferentes. A primeira camada de terreno adicionada cobrirá automaticamente todo o terreno. Use isso como uma maneira rápida de configurar suas cores de base. Cada camada de terreno pode ter Mapas Difusos , Normais e de Máscara .

O mapa difuso é a textura exibida pelo terreno. Uma vez definido, a opção de alterar sua tonalidade se torna visível. Alterar a tonalidade é uma maneira rápida e eficaz de criar variação de terreno sem recorrer ao uso de mais texturas ou editar seus ativos em programas externos.

Enquanto o mapa normal é usado para transmitir informações normais na camada e geralmente funciona da mesma forma que um mapa normal regular, o mapa de máscara é usado explicitamente para os Pipelines de Renderização Universal e de Alta Definição para transmitir mais informações, como oclusão metálica, ambiente, altura e suavidade.

Finalmente, as configurações de ladrilhos são opções úteis para a camada de terreno. Os valores de tamanho e deslocamento alteram a frequência com que a textura será ladrilhada no terreno e com qual deslocamento de base para cada repetição.

Geralmente, para terrenos grandes, é bom ter algumas variações das camadas do terreno com valores variados para o tamanho, pois isso pode ajudar a quebrar a repetição visível no piso do terreno.

Como dito anteriormente, os pincéis de terreno vêm por padrão com o terreno do Unity, mas não há camadas de terreno ao criar um novo objeto de terreno.

Além disso, é necessário ter mapas e texturas para criar suas camadas de terreno. Para este artigo, usei alguns novos pincéis gratuitos da loja de ativos ( Genic Terrain Brushes by Flaming Sands e StampIT! Collection Examples by Rowlan Inc), bem como um pacote de texturas gratuitas para as camadas de terreno ( Handpainted Grass & Ground Texturas por Chromisu).

Os resultados para o terreno base com algumas texturas podem ser vistos abaixo:

Terreno base com texturas

Ao trabalhar no terreno, costumo tentar equilibrar áreas de pico e áreas planas. Isso tende a me dar espaço suficiente para espalhar a vegetação e não superlotar o espaço. Além disso, tento ter um elemento chave no terreno para manter seus principais componentes e foco, como uma montanha, uma clareira ou um rio, por exemplo.

Ter um componente principal ajuda na tomada de decisão sobre quais ativos usar e como distribuí-los. Neste caso, vamos fazer uma pequena colina com uma entrada em forma de caverna para explorar mais as opções de terreno e algumas pequenas áreas de interesse para diversificar a vista.

Além disso, observe que tentei explorar as múltiplas camadas do terreno para quebrar a repetição visual que elas costumam fazer. Nesta fase, tento apenas bloquear os principais pontos, separando as colinas de grama regular e áreas planas, bem como polvilhar alguma diversidade aqui e ali com um pouco de areia e manchas mais escuras de grama.

Plantar árvores, grama e... pedras?

A vegetação pode ser adicionada de duas maneiras, com a opção Adicionar árvore ou com a opção Adicionar detalhe . Add Tree é bastante simples e permite colocar objetos com características semelhantes a árvores.

Para uma melhor sinergia, as árvores devem ser criadas usando as Unity Tools, como a Speed ​​Tree ou o Tree Editor , ou usando os materiais específicos da árvore do terreno. No entanto, isso não é obrigatório e, nas seções a seguir, mostrarei outra abordagem que produz resultados semelhantes e permite mais liberdade criativa.

Em qualquer caso, as árvores do terreno têm um limite máximo de 2 materiais por malha (um para a casca e outro para as folhas). Se sua malha tiver mais de dois materiais, talvez ela não seja renderizada corretamente. Uma solução comum para essa limitação é recorrer ao atlas de textura. Além disso, é comum separar os materiais da folha do material da casca para um efeito mais interessante, o que nos permite programar shaders apenas para as folhas (como simular o movimento do vento).

Árvores

Semelhante às camadas de terreno, as árvores também precisam ser adicionadas ao terreno individualmente e depois colocadas usando os pincéis do terreno. As árvores podem ser adicionadas como malhas nuas e também como prefabs.

Observe que adicionar uma árvore como um prefab não necessariamente adicionará todos os elementos prefab nem se comportará exatamente como o prefab pretendido. Por exemplo, colocar uma árvore pré-fabricada com um animador ignorará o animador e agirá como uma árvore estática.

Rochas e Grama

De forma semelhante, os detalhes são elementos que podem ser colocados no terreno para diversificar seu visual. Os detalhes podem ser de dois tipos : Detalhe da Malha e Textura da Grama. A malha de detalhes funciona como malhas regulares que permanecem estáticas na cena, como pedras, seixos e até arbustos.

As malhas de detalhes são limitadas a um material. Se você tentar usar malhas com mais de um material, elas não serão renderizadas corretamente.

Malhas de detalhes também podem ser renderizadas como Vertex Lit ou Grass. O Vertex Lit renderizará a malha como objetos de jogo iluminados regularmente e não reage ao vento, o que é apropriado para pedras e tocos de árvores. A renderização de grama funciona de maneira semelhante às Texturas de grama, permitindo que o vento do terreno afete a malha.

No entanto, por experiência própria, a renderização da grama só produz resultados visualmente agradáveis ​​quando a propriedade de flexão do vento está definida para um valor baixo (nas Configurações gerais do terreno ). Caso contrário, a malha de detalhes pode se mover de forma irreal.

Animação de grama suave no Unity

Texturas de grama são sprites renderizados no terreno que se comportam de acordo com o vento do terreno. Conforme declarado na documentação do Unity, o termo “Grass Texture” é enganoso, pois você pode usar qualquer textura genérica, como flores ou gravetos, com a mesma ferramenta. Diferente das malhas de detalhes renderizadas como grama, texturas de grama manterão seu pivô no chão e se moverão com o vento mantendo a mesma posição - como você esperaria que a grama se comportasse.

Para este artigo, usei árvores e outras malhas da loja de ativos ( Low-Poly Simple Nature Pack da JustCreate e Low Poly Rock Pack da Broken Vector). Para as texturas de grama, usei os excelentes Foliage Sprites da Kenney, uma ótima fonte de ativos de alta qualidade e livres de direitos autorais. Por fim, como recursos adicionais de decoração, usei o Low Poly Dungeons Lite da JustCreate.

Aplicando-os ao nosso terreno, temos o seguinte resultado:

Resultado do terreno

Árvores mais versáteis com grupos LOD

Como dito anteriormente, as árvores do Unity são de alguma forma limitadas devido às otimizações feitas pelo sistema de terreno. Se você usar materiais personalizados ou outras variações, algumas opções de posicionamento da árvore podem ser ignoradas, levando a uma cena muito sem graça e repetitiva.

No entanto, o sistema também nos permite contornar algumas de suas restrições para alcançar um maior nível de versatilidade em relação ao uso e estilo dos materiais. Para isso, temos que seguir um processo específico.

Primeiro, você deve criar um novo prefab para a árvore na qual o objeto de jogo raiz (o primeiro na hierarquia) é um objeto de jogo vazio com o componente LOD Group . Em seguida, você pode colocar sua árvore pré-fabricada na hierarquia abaixo do objeto do jogo raiz. Depois disso, adicione o prefab da árvore ao LOD Group e pronto.

A árvore pré-fabricada funcionará com seus shaders personalizados e receberá as propriedades regulares de posicionamento da árvore do terreno, como alterar a largura, a altura e a rotação da árvore. Para variação de cores, é necessário alterar o sombreador para ler a propriedade _TreeInstanceColor, mas esta etapa foge ao escopo deste artigo.

É claro que você já pode usar os recursos do LOD Group para lidar com o outro nível de detalhes da sua árvore. No entanto, se você simplesmente deseja um material mais versátil em suas árvores enquanto ainda usa as propriedades de posicionamento do terreno, pode alterar o número de LODs para 1 e usar apenas uma única árvore pré-fabricada.

A realização dessas etapas deve levar você a um resultado semelhante ao abaixo:

LOD da árvore

Fluxo de trabalho de terreno proposto para melhores resultados

Agora que abordamos os principais aspectos de como usar a ferramenta de terreno, quero oferecer um fluxo de trabalho simples que uso para obter resultados razoavelmente bons sem muito esforço, além de abordar alguns outros aspectos de renderização que podem melhorar seu ambiente.

Fazendo uma entrada de caverna

Comecemos pela caverna que mencionei anteriormente. O Unity Terrain não nos permite criar entradas ou esculpir o mapa de altura horizontalmente (aplicando informações de altura no plano xz). No entanto, isso nos permitirá pintar buracos nele.

Buracos no Unity Terrain escondem partes da malha do terreno como se estivéssemos cortando pedaços. Os furos podem ser pintados como os outros elementos nas ferramentas Sculpt Terrain com pincéis de terreno.

Para fazer uma entrada de caverna visualmente emocionante, recomendo que você levante o terreno, mantendo a elevação em um ângulo entre 40 e 60 graus. Mais do que isso pode esticar demais a grade do terreno e dificultar a cobertura com outros elementos e pré-fabricados. Ao fazer isso, devemos obter um resultado como o abaixo:

Rasgo no terreno

Como você pode ver, os buracos do terreno escondem partes do terreno de forma binária: eles estão completamente desaparecidos ou completamente visíveis. Para isso, é necessário revestir as costuras com ativos e outros elementos para obter melhor qualidade visual.

Vista do terreno

Usando os mesmos recursos gratuitos mencionados anteriormente, escalei e girei diferentes pedras ao redor da entrada da caverna para esconder as costuras entre os buracos e o terreno visível. Eu também tentei adicionar mais pedras e seixos do que o necessário para criar uma aparência mais natural. É comum ajustar o mapa de altura para se adequar melhor à conexão com as rochas.

Além disso, adicionar adereços extras em torno de áreas de interesse é sempre bom para aumentar o foco do jogador. Para isso, acrescentei alguns jarros meio enterrados no chão e um pilar bem em frente à caverna, o que ajuda a criar algum mistério e narrativa. Acrescentei uma pequena luz próxima à entrada da caverna para guiar ainda mais os olhos.

Luz na entrada da caverna

Como visto anteriormente, o buraco também permite que o usuário veja através do terreno. Para evitar isso, usei aviões com um material específico que ignora a iluminação (Unlit). Isso é importante porque, caso contrário, as mudanças na luz da cena exibiriam essas paredes escuras como sólidas, enquanto o resultado visual que queremos é que pareça uma área escura/sombra.

Além disso, usei algumas das malhas do pacote Low Poly para servir de solo da caverna, pois não podemos usar o mesmo terreno que usamos até agora.

É importante notar que a caverna que fizemos até agora não é ideal se você quiser que seu jogador entre e navegue dentro dela. Para isso, você precisaria definir corretamente as malhas ao redor do buraco, modelando um pouco o interior da caverna de todos os lados. Seria possível conseguir isso usando um novo objeto de terreno, mas acho que o trabalho necessário para isso pode não ser o ideal.

Por outro lado, esta solução funciona muito bem para você usar como um ponto para mover o jogador para uma nova cena dedicada ao interior da caverna.

Oclusão ambiente manual

Exemplo de oclusão de ambiente

Exemplo de oclusão de ambiente 2

Uma técnica comum que uso para melhorar a iluminação em um Unity Terrain é usar manualmente uma variação mais escura da camada de terreno sob objetos colocados no chão. Isso ajuda a elevar a coesão dos elementos do terreno, já que alguns efeitos de iluminação, como oclusão de ambiente, não são aplicados aos objetos do terreno, especialmente se você estiver usando shaders personalizados e outras técnicas.

Exemplo de Oclusão de Terreno

Observe que isso é diferente de projetar sombras. Os terrenos da unidade projetarão sombras dos objetos corretamente, mas isso ainda pode causar a impressão de que os objetos estão flutuando ou não estão realmente conectados. Usar uma camada de terreno escuro em alguns pontos ajuda a aproximar os objetos e o solo sem exigir mais recursos ou cálculos de iluminação caros.

Além disso, muitas vezes aplico isso na parte inferior de outros objetos e prefabs que não são objetos de terreno. Como visto na imagem acima, usei um pouco do tom mais escuro embaixo dos livros e do caldeirão para ajudar a aproximar os elementos.

Oclusão ambiente real e cozimento leve

Camadas mais escuras ajudarão com a iluminação falsa até certo ponto, mas podem ser amplificadas ao assar a luz na cena. Uma discussão completa sobre light bake está fora do escopo deste artigo, mas alguns de seus principais elementos serão discutidos sobre como usá-los para melhorar o visual de um terreno.

Primeiro, Light Baking é um processo no qual pré-calculamos a luz em uma cena e armazenamos os dados para depois aplicá-los sobre os objetos da cena. O Unity usa especificamente o Lightmapping , que reduz o brilho da superfície do objeto em mapas de textura.

Essas texturas, no entanto, não podem ser alteradas em tempo de execução e precisam ser pré-calculadas novamente para cada alteração na cena, como mover objetos para um local diferente ou atualizar a transformação das luzes. Além disso, só é aplicado a objetos sinalizados como estáticos.

Guia Iluminação

Light bake pode ser feito na guia Lighting (você pode acessá-lo através do menu Window , seguido de Rendering e Lighting ). Os valores padrão do Unity para o light bake são consideravelmente altos e podem demorar muito para serem processados. Para isso, recomendo que você reduza todas as Amostras Diretas , Amostras Indiretas e Amostras de Ambiente para um número menor, como 16 ou 32.

Esses atributos estão diretamente relacionados a quantas etapas o Lightmapper levará para calcular a luz em qualquer ponto da cena (para elementos estáticos). Aumentar qualquer um deles produzirá resultados melhores e mais realistas, mas aumentará consideravelmente o tempo de cozimento.

Costumo começar com valores baixos para avaliar as primeiras impressões na cena e depois aumentá-las progressivamente conforme achar melhor. Para materiais mais estilísticos ou personalizados, usar valores mais baixos geralmente é suficiente para obter bons visuais.

Além disso, os primeiros bakes podem mostrar inconsistências na colocação de objetos e outros erros que devem ser corrigidos antes de aplicar um procedimento de mapeamento de luz mais completo e realista.

Também recomendo ativar a Oclusão de ambiente de cozimento leve nas propriedades de iluminação para o cozimento. A Oclusão de Ambiente é o efeito visual que acontece quando as superfícies estão próximas umas das outras, ocluindo a luz e projetando sombras em seus pontos de interseção, fazendo com que pareçam mais escuras. Anteriormente, fingimos usar cores de camada de terreno mais escuras, mas o mapeamento de luz pode realmente calcular a oclusão de ambiente adequada e assá-la em texturas.

As imagens abaixo mostram os resultados com e sem leve cozimento.

Sem cozimento leve

Sem cozimento leve.

Com Cozimento Leve

Com leve cozimento.

Como você pode ver, não há oclusão ambiental entre os troncos das árvores e o solo. No entanto, a parte inferior do caldeirão e as interseções entre as caixas na parte de trás da imagem são mais escuras e mais realistas do que a versão sem assar.

Colocando uma skybox e aplicando o pós-processamento

Finalmente, para melhorar ainda mais o visual e unir os elementos, essas duas etapas são essenciais: colocar uma skybox melhor e aplicar alguns efeitos de pós-processamento.

Para este artigo, usei o Free Stylized Skybox do Yuki2022. Esses skyboxes se encaixam muito bem no estilo low poly para sua aparência de desenho animado.

O skybox pode ser adicionado na aba Lighting (o mesmo que Light Baking), na aba Environment .

Os efeitos de pós-processamento requerem mais etapas. Primeiro, você deve adicionar o pacote de pós-processamento ao seu projeto, dependendo do pipeline de renderização instalado. Usando o pipeline de renderização integrado, você precisa instalar a pilha de pós-processamento em seu projeto usando o gerenciador de pacotes.

Caso contrário, se você estiver usando o Pipeline de renderização universal ou o Pipeline de renderização de alta definição, já terá o sistema de pós-processamento equivalente instalado. Estou usando o pipeline de renderização universal para este artigo, mas os efeitos e as configurações são semelhantes aos dos outros pipelines.

A imagem abaixo mostra o volume final de pós-processamento que usei para a versão final do terreno:

Configurações de volume

Os efeitos usados ​​foram:

  • Vinheta : escurece os cantos da tela, focando a atenção do jogador nos elementos centrais
  • Bloom : expande os elementos de destaque na cena, desfocando suas áreas com objetos vizinhos, o que funciona como uma alternativa de iluminação global falsa (mas boa)
  • Profundidade de campo : imita o foco de nossos olhos e câmeras reais, tornando os elementos mais próximos de nós (no ponto focal) nítidos, enquanto outros elementos são borrados de acordo com sua distância
  • Curvas de cores : altera as cores finais renderizadas para combinar com uma aparência mais desejada. Neste caso, aumentei a cor vermelha sobre as outras e aumentei ligeiramente a cor azul
  • Lift Gamma Gain : muda a cor geral da renderização, além de aumentar o controle de tons escuros, tons médios e realces

Os resultados finais para os dois locais que fizemos durante este artigo podem ser vistos nas imagens abaixo:

Ambiente do terreno

Exemplo de terreno final

Obrigado por ler, e deixe-me saber se você gostaria de ler mais sobre os muitos tópicos discutidos que estavam fora do escopo deste artigo. Cada um deles certamente merece seu próprio artigo. Você pode ver mais do meu trabalho em www.dagongraphics.com .

 Fonte: https://blog.logrocket.com/easy-environment-design-unity-terrain-features/

 #unity #design 

Design De Ambiente Fácil Com Recursos Do Unity Terrain

Conception D'environnement Facile Avec Les Fonctionnalités Unity Terra

Unity Terrain est un outil natif Unity, puissant et polyvalent pour la conception de niveau et d'environnement . Il fournit des mécanismes simples pour modeler et modifier les terrains en utilisant le concept de cartes de hauteur.

Dans cet article, nous allons voir les bases de l'utilisation de cet outil et ses principales fonctionnalités. Nous commençons par la théorie du fonctionnement de la génération de terrain en utilisant des cartes de hauteur. Ensuite, nous aborderons les principaux mécanismes de modification d'un terrain. À la fin de l'article, je présente un flux de travail typique pour créer de belles scènes Unity à l'aide de ressources gratuites et de l'outil Unity Terrain.

Exemple de terrain fini

Des cartes de hauteur aux terrains

Une carte de hauteur est une texture qui modifie la façon dont un maillage spécifique est vu par le joueur, similaire au fonctionnement des cartes normales. Plus précisément, les informations de la carte de hauteur déplacent les sommets du maillage pour afficher des caractéristiques telles que les élévations, les falaises, les plaines et les cratères.

En raison de sa nature 2D, une seule carte de hauteur ne peut interférer qu'avec un axe des sommets à la fois. Notamment, la carte de hauteur Unity Terrain modifie la position des sommets sur l'axe y.

Lorsque nous travaillons avec un Unity Terrain en élevant ou en abaissant ses parties, nous modifions techniquement sa carte de hauteur. La modification agit immédiatement et nous pouvons voir les résultats de notre changement de terrain. Alternativement, il est également possible de modifier la carte de hauteur directement en dehors de Unity et d'importer des cartes de hauteur dans le moteur.

Par exemple, le site Web Cities: Skylines Height Map Generator fournit des mécanismes pour extraire des informations de carte de hauteur du monde réel. Bien que le site Web soit destiné à être utilisé dans le jeu Cities: Skylines , les cartes fonctionnent tout aussi bien dans Unity. Considérez, cependant, qu'ils contiendront des distances réelles et devront peut-être être ajustés dans un outil externe pour une utilisation optimale dans Unity.

Les images ci-dessous montrent une carte de hauteur extraite du site Web et importée dans Unity Terrain.

Carte de hauteur dans Unity

Carte de hauteur dans un terrain d'unité.

Carte de hauteur visualisée

Carte de hauteur visualisée (valeurs ajustées pour améliorer la visibilité).

Paramètres de terrain d'unité

Lorsqu'il est sélectionné, un terrain Unity peut être modifié à l'aide de la barre d'outils Terrain. La barre d'outils contient les principales fonctionnalités du terrain regroupées en 5 sections : Tuiles terrain adjacentes ; Sculpter et peindre ; Ajouter des arbres ; Ajouter des détails ; et Paramètres généraux .

Paramètres de la barre d'outils

 

Les tuiles de terrain adjacentes vous permettent de créer d'autres terrains dans un format semblable à une grille près du terrain actuel. Il est avantageux de l'utiliser lorsque vous avez déjà un terrain bien établi mais que vous avez besoin de plus d'espace à côté.

C'est une solution plus appropriée que d'augmenter la taille du terrain. La modification de la taille affectera la lecture de la carte de hauteur et un facteur d'échelle proportionnel sera appliqué au terrain et à tous ses éléments.

En ce qui concerne les cartes de hauteur, la création d'un nouveau terrain adjacent créera un nouvel objet de jeu de terrain avec sa carte de hauteur. La carte de hauteur nouvellement créée essaiera de faire correspondre les valeurs de sa bordure, en la couplant de manière transparente au terrain précédent auquel elle était connectée.

L' option Sculpter et peindre fonctionne directement avec la carte de hauteur. Cette option contient ses outils de terrain , que nous aborderons dans la section suivante. En bref, cette option nous permet de déformer la carte de hauteur et d'en masquer des parties pour créer des trous et des entrées, par exemple.

Les deux options pour ajouter des arbres et ajouter des détails sont utilisées pour remplir le terrain avec des éléments qui sont gérés automatiquement par Unity, tels que des arbres, des buissons, des arbustes, des pierres, des plaques d'herbe et autres. L'un des avantages de l'utilisation de Unity Terrain par rapport à l'utilisation d'objets de jeu placés par l'utilisateur dans la scène est que Unity gère de nombreuses techniques d'optimisation pour les éléments d'un terrain. Les arbres et les détails sont automatiquement pris en compte dans les algorithmes liés à l'élimination de la visibilité et dans le panneau d' affichage .

Enfin, l' option Paramètres généraux contient toutes les informations pertinentes concernant l'objet de jeu de terrain, telles que les techniques d'optimisation (distance de détail, distance d'affichage, ombres, etc.) et la taille du terrain.

Les paramètres généraux sont divisés en Terran de base , Objets d'arbre et de détail , Paramètres de vent pour l'herbe , Résolution de maillage , Paramètres de trou , Résolutions de texture , Éclairage et Lightmapping . Il n'entre pas dans le cadre de cet article de couvrir chaque option de manière approfondie, mais nous passerons en revue les plus utilisées.

Paramètres du relief

La section Terrain de base couvre les principaux aspects de rendu du terrain, tels que le matériau du terrain, ses propriétés d'ombre et le mode de dessin utilisé. Notez que si vous prévoyez d'utiliser des shaders de post-traitement faits maison ou des passes de rendu scriptées, l'option Dessiner une instance doit être désactivée.

Les valeurs de résolution de maillage déterminent la taille du terrain. Changer la largeur et la longueur du terrain changera immédiatement la façon dont les informations de la carte de hauteur seront lues. La largeur, la longueur et la hauteur du terrain déterminent la taille du terrain sur les axes x, y et z.

La section Résolutions de texture gère les informations de texture de carte de hauteur plus spécifiques. Vous pouvez définir la résolution de la carte de hauteur et y importer/exporter des cartes de hauteur . À l'aide de la fonction d'importation, vous pouvez utiliser les données acquises à partir d'autres sources (telles que celle présentée précédemment) dans votre carte. Il est également courant d'exporter la carte de hauteur actuelle, de la modifier dans un outil externe, puis de la réimporter.

Options pour sculpter et modifier le terrain

Comme indiqué précédemment, l' option Sculpt and Paint nous permet de modifier le terrain en modifiant la carte de hauteur. Ces options comportent six outils : Augmenter ou abaisser le terrain , Peindre des trous , Peindre la texture , Définir la hauteur , Lisser la hauteur et Estamper le terrain .

Outil de sculpture et de peinture

Les options Augmenter ou Abaisser le terrain , Définir la hauteur et Lisser la hauteur sont toutes utilisées pour affecter directement la carte de hauteur en augmentant ou en diminuant ses valeurs. Définir la hauteur est utilisé pour modifier la carte de hauteur afin d'obtenir un résultat souhaité spécifique, tandis que Lisser la hauteur adoucit le terrain comme s'il brouillait les zones sélectionnées dans la carte de hauteur. Toutes ces options sont effectuées à l'aide de pinceaux de terrain (comme indiqué dans l'image ci-dessus).

Les pinceaux de terrain sont similaires au fonctionnement des pinceaux dans d'autres logiciels d'édition d'images, tels que GIMP et Photoshop. Un pinceau présente un motif en nuances de gris et alpha, qui est appliqué au terrain pour le modeler à l'aide de ses valeurs. Les zones transparentes du pinceau n'affecteront pas le terrain et les zones plus sombres auront plus d'impact sur le terrain que les zones plus claires. L'ensemble de pinceaux de terrain par défaut de Unity comprend plus de dix pinceaux.

L' option Peindre la texture est utilisée pour appliquer des couches de texture au terrain, en y ajoutant activement de la couleur et d'autres propriétés de texture. Les couches de texture sont appliquées à l'aide des mêmes pinceaux de terrain que ceux utilisés pour les autres outils de sculpture et de peinture. Cependant, le terrain par défaut d'Unity n'a pas de couche de texture.

Couches de terrain

Une liste de couches de terrain.

Une couche de terrain

Les propriétés d'une couche de terrain.

Chaque couche de terrain est un atout en soi et peut être réutilisée par plusieurs terrains, même dans différentes scènes. La première couche de terrain ajoutée couvrira automatiquement tout le terrain. Utilisez-le comme un moyen rapide de configurer vos couleurs de base. Chaque couche de terrain peut avoir des cartes diffuses , normales et masquées .

La carte diffuse est la texture affichée par le terrain. Une fois définie, l'option permettant de modifier sa teinte devient visible. Changer la teinte est un moyen rapide et efficace de créer une variation de terrain sans recourir à l'utilisation de plus de textures ou à la modification de vos actifs dans des programmes externes.

Alors que la carte normale est utilisée pour transmettre des informations normales sur le calque et fonctionne généralement de la même manière qu'une carte normale normale, la carte de masque est explicitement utilisée pour les pipelines de rendu haute définition et universel afin de transmettre plus d'informations, telles que l'occlusion métallique, ambiante, hauteur et douceur.

Enfin, les paramètres de mosaïque sont des options pratiques pour la couche de terrain. Les valeurs de taille et de décalage modifient la fréquence de mosaïque de la texture dans le terrain et avec quel décalage de base pour chaque répétition.

Généralement, pour les grands terrains, il est bon d'avoir quelques variations des couches de terrain avec des valeurs variées pour la taille, car cela peut aider à briser la répétition visible sur le sol du terrain.

Comme indiqué précédemment, les pinceaux de terrain sont fournis par défaut avec le terrain d'Unity, mais il n'y a pas de couches de terrain lors de la création d'un nouvel objet de terrain.

De plus, il est nécessaire d'avoir des cartes et des textures pour créer vos couches de terrain. Pour cet article, j'ai utilisé quelques nouveaux pinceaux gratuits du magasin d'actifs ( Generic Terrain Brushes de Flaming Sands et StampIT! Collection Exemples de Rowlan Inc) ainsi qu'un ensemble de textures gratuites pour les couches de terrain ( Handpainted Grass & Ground Textures par Chromisu).

Les résultats pour le terrain de base avec certaines textures peuvent être vus ci-dessous :

Terrain de base avec textures

Lorsque je travaille sur le terrain, j'essaie généralement d'équilibrer les zones de pointe et les zones de plaine. Cela a tendance à me donner suffisamment d'espace pour étendre la végétation et ne pas surcharger l'espace. De plus, j'essaie d'avoir un élément clé dans le terrain pour contenir ses principales composantes et se concentrer, comme une montagne, une clairière ou une rivière, par exemple.

Avoir un composant principal aide à la prise de décision pour les actifs à utiliser et comment les distribuer. Dans ce cas, nous allons créer une petite colline avec une entrée en forme de grotte pour explorer davantage les options de terrain et quelques petites zones d'intérêt pour diversifier la vue.

Notez également que j'ai essayé d'explorer les multiples couches de terrain pour briser la répétition visuelle qu'elles ont tendance à faire. À ce stade, j'essaie de bloquer uniquement les principaux endroits, en séparant les collines des zones d'herbe et de plaine régulières, ainsi qu'en saupoudrant une certaine diversité ici et là avec du sable et des plaques plus sombres de sol en herbe.

Planter des arbres, de l'herbe et… des pierres ?

La végétation peut être ajoutée de deux manières, soit avec l' option Ajouter un arbre , soit avec l'option Ajouter un détail . Ajouter un arbre est assez simple et vous permet de placer des objets avec des caractéristiques arborescentes.

Pour une meilleure synergie, les arbres doivent être créés à l'aide des outils Unity, tels que l' arbre de vitesse ou l' éditeur d'arbres , ou à l'aide des matériaux d'arbre de terrain spécifiques. Ce n'est cependant pas obligatoire, et dans les sections suivantes, je montrerai une autre approche qui donne des résultats similaires et permet une plus grande liberté de création.

Dans tous les cas, les arbres de terrain ont une limitation d'un maximum de 2 matériaux par maille (un pour l'écorce et un autre pour les feuilles). Si votre maillage comporte plus de deux matériaux, il se peut qu'il ne s'affiche pas correctement. Une solution courante à cette limitation consiste à recourir à l'atlas de texture. De plus, il est courant de séparer les matériaux de la feuille du matériau de l'écorce pour un effet plus intéressant, ce qui nous permet de programmer des shaders uniquement pour les feuilles (comme la simulation du mouvement du vent).

Des arbres

Semblables aux couches de terrain, les arbres doivent également être ajoutés au terrain individuellement, puis placés à l'aide des pinceaux du terrain. Les arbres peuvent être ajoutés sous forme de mailles nues et également sous forme de préfabriqués.

Notez que l'ajout d'un arbre en tant que préfabriqué n'ajoutera pas nécessairement tous les éléments préfabriqués ni ne se comportera exactement comme votre préfabriqué prévu. Par exemple, placer un arbre préfabriqué avec un animateur ignorera l'animateur et agira comme un arbre statique.

Roches et herbe

De la même manière, les détails sont des éléments qui peuvent être placés dans le terrain pour diversifier ses visuels. Les détails peuvent être de deux types : Detail Mesh et Grass Texture. Detail Mesh fonctionne comme des maillages réguliers qui restent statiques dans la scène, tels que des pierres, des cailloux et même des arbustes.

Les maillages de détail sont limités à un seul matériau. Si vous essayez d'utiliser des maillages avec plusieurs matériaux, ils ne seront pas rendus correctement.

Les maillages de détail peuvent également être rendus en tant que Vertex Lit ou Grass. Vertex Lit rendra le maillage sous forme d'objets de jeu éclairés régulièrement et ne réagit pas au vent, ce qui est approprié pour les pierres et les souches d'arbres. Le rendu de l'herbe fonctionne de la même manière que les textures d'herbe, permettant au vent du terrain d'affecter le maillage.

Cependant, par expérience, le rendu de l'herbe ne donne des résultats visuellement agréables que lorsque la propriété de flexion du vent est définie sur une valeur faible (sous les Paramètres généraux du terrain ). Sinon, le maillage de détail pourrait se déplacer de manière irréaliste.

Animation d'herbe lisse dans Unity

Les textures d'herbe sont des sprites rendus dans le terrain qui se comportent en fonction du vent du terrain. Comme indiqué dans la documentation de Unity, le terme "Grass Texture" est trompeur car vous pouvez utiliser n'importe quelle texture générique, comme des fleurs ou des bâtons, avec le même outil. Différent des maillages détaillés rendus sous forme d'herbe, les textures d'herbe garderont leur pivot sur le sol et se déplaceront avec le vent en gardant la même position - comme vous vous attendez à ce que l'herbe se comporte.

Pour cet article, j'ai utilisé des arbres et d'autres maillages de l'asset store ( Low-Poly Simple Nature Pack de JustCreate, et Low Poly Rock Pack de Broken Vector). Pour les textures d'herbe, j'ai utilisé les excellents Foliage Sprites de Kenney, une excellente source d'éléments de haute qualité sans droits d'auteur. Enfin, comme atouts de décoration supplémentaires, j'ai utilisé le Low Poly Dungeons Lite de JustCreate.

En les appliquant à notre terrain, nous avons le résultat suivant :

Résultat terrain

Arbres plus polyvalents avec les groupes LOD

Comme dit précédemment, les arbres d'Unity sont en quelque sorte limités en raison des optimisations effectuées par le système de terrain. Si vous utilisez des matériaux personnalisés ou d'autres variantes, certaines options de placement des arbres peuvent être ignorées, ce qui conduit à une scène très fade et répétitive.

Cependant, le système nous permet également de contourner certaines de ses restrictions pour atteindre un niveau de polyvalence plus élevé en ce qui concerne l'utilisation des matériaux et le style. Pour cela, nous devons suivre un processus spécifique.

Tout d'abord, vous devez créer un nouveau préfabriqué pour l'arborescence dans lequel l'objet de jeu racine (le premier dans la hiérarchie) est un objet de jeu vide avec le composant LOD Group . Ensuite, vous pouvez placer votre arbre préfabriqué dans la hiérarchie sous l'objet de jeu racine. Après cela, ajoutez l'arborescence préfabriquée au groupe LOD, et c'est tout.

Le préfabriqué d'arbre fonctionnera avec vos shaders personnalisés et recevra les propriétés de placement d'arbre régulières du terrain, telles que la modification de la largeur, de la hauteur et de la rotation de l'arbre. Pour la variation de couleur, il est nécessaire de modifier le shader pour lire la propriété _TreeInstanceColor, mais cette étape sort du cadre de cet article.

Vous pouvez, bien sûr, déjà utiliser les fonctionnalités du groupe LOD pour gérer l'autre niveau de détails de votre arbre. Cependant, si vous voulez simplement un matériau plus polyvalent sur vos arbres tout en utilisant les propriétés de placement du terrain, vous pouvez changer le nombre de LOD à 1 et n'utiliser qu'un seul arbre préfabriqué.

La réalisation de ces étapes devrait vous amener à un résultat similaire à celui ci-dessous :

Niveau de détail de l'arbre

Workflow de terrain proposé pour de meilleurs résultats

Maintenant que nous avons couvert les principaux aspects de l'utilisation de l'outil de terrain, je souhaite proposer un flux de travail simple que j'utilise pour obtenir des résultats raisonnablement bons sans trop d'efforts, tout en couvrant également d'autres aspects du rendu qui peuvent améliorer votre environnement.

Faire une entrée de grotte

Commençons par la grotte dont j'ai parlé précédemment. Unity Terrain ne nous permet pas de créer des entrées ou de découper la carte de hauteur horizontalement (en appliquant les informations de hauteur dans le plan xz). Cependant, cela nous permettra de peindre des trous dessus.

Les trous dans Unity Terrain cachent des parties du maillage du terrain comme si nous découpions des morceaux. Les trous peuvent être peints comme les autres éléments des outils Sculpter le terrain avec des pinceaux de terrain.

Pour créer une entrée de grotte visuellement excitante, je vous recommande de surélever le terrain en maintenant l'élévation à un angle compris entre 40 et 60 degrés. Plus que cela pourrait trop étirer la grille de terrain et la rendre plus difficile à couvrir avec d'autres éléments et préfabriqués. Ce faisant, nous devrions obtenir un résultat tel que celui ci-dessous :

Déchirure du terrain

Comme vous pouvez le voir, les trous de terrain cachent des parties du terrain de manière binaire : ils sont soit complètement disparus, soit complètement visibles. Pour cela, il est nécessaire de couvrir les coutures avec des actifs et d'autres éléments pour obtenir une meilleure qualité visuelle.

Vue du relief

En utilisant les mêmes ressources gratuites mentionnées précédemment, j'ai mis à l'échelle et fait pivoter différentes pierres autour de l'entrée de la grotte pour masquer les coutures entre les trous et le terrain visible. J'ai également essayé d'ajouter plus de roches et de cailloux que nécessaire pour créer un aspect plus naturel. Il est courant d'ajuster la carte de hauteur pour mieux s'adapter à la connexion avec les rochers.

De plus, ajouter des accessoires supplémentaires autour des zones d'intérêt est toujours bon pour augmenter la concentration du joueur. Pour cela, j'ai ajouté des jarres à moitié enterrées dans le sol et un pilier juste devant la grotte, ce qui contribue à créer du mystère et de la narration. J'ai ajouté une petite lumière à côté de l'entrée de la grotte pour guider les yeux encore plus loin.

Lumière sur l'entrée de la grotte

Comme vu précédemment, le trou permet également à l'utilisateur de voir à travers le terrain. Pour éviter cela, j'ai utilisé des avions avec un matériau spécifique qui ignore l'éclairage (Unlit). C'est important car sinon, les changements de lumière de la scène afficheraient ces murs sombres comme solides, alors que le résultat visuel que nous voulons est qu'ils ressemblent à une zone sombre/ombre.

De plus, j'ai utilisé certains des maillages du package Low Poly pour servir de sol à la grotte car nous ne pouvons pas utiliser le même terrain que nous avons utilisé jusqu'à présent.

Il est important de noter que la grotte que nous avons créée jusqu'à présent n'est pas idéale si vous souhaitez que votre joueur y pénètre et navigue à l'intérieur. Pour cela, vous devez définir correctement les maillages autour du trou, en modélisant quelque peu l'intérieur de la grotte de tous les côtés. Il serait possible d'y parvenir en utilisant un nouvel objet de terrain, mais je pense que le travail requis pour cela pourrait ne pas être optimal.

D'un autre côté, cette solution fonctionne assez bien pour que vous puissiez l'utiliser comme un point pour déplacer le joueur vers une nouvelle scène dédiée à l'intérieur de la grotte.

Occlusion ambiante manuelle

Exemple d'occlusion ambiante

Exemple d'occlusion ambiante 2

Une technique courante que j'utilise pour améliorer l'éclairage dans Unity Terrain consiste à utiliser manuellement une variation plus sombre de la couche de terrain sous les objets placés au sol. Cela contribue à améliorer la cohésion des éléments de terrain puisque certains effets d'éclairage, tels que l'occlusion ambiante, ne sont pas appliqués aux objets de terrain, en particulier si vous utilisez des shaders personnalisés et d'autres techniques.

Terrain Exemple Occlusion

Notez que cela est différent de la projection d'ombres. Les terrains Unity projetteront correctement les ombres des objets, mais cela pourrait toujours donner l'impression que les objets flottent ou ne sont pas vraiment connectés. L'utilisation d'une couche de terrain sombre à certains endroits permet de rapprocher les objets et le sol sans nécessiter plus de ressources ou des calculs d'éclairage coûteux.

De plus, j'applique souvent cela au bas d'autres objets et préfabriqués qui ne sont pas des objets de terrain. Comme on le voit dans l'image ci-dessus, j'ai utilisé un peu de ton plus sombre sous les livres et le chaudron pour aider à rapprocher les éléments.

Véritable occlusion ambiante et cuisson légère

Des couches plus sombres aideront le faux éclairage dans une certaine mesure, mais il peut être amplifié en chauffant la lumière dans la scène. Une discussion complète sur la cuisson légère sort du cadre de cet article, mais quelques-uns de ses principaux éléments seront discutés sur la façon de les utiliser pour améliorer les visuels d'un terrain.

Tout d'abord, Light Baking est un processus dans lequel nous pré-calculons la lumière dans une scène et stockons les données pour ensuite les appliquer sur les objets de la scène. Unity utilise spécifiquement Lightmapping , qui cuit la luminosité de la surface de l'objet dans les cartes de texture.

Ces textures, cependant, ne peuvent pas être modifiées lors de l'exécution et doivent être précalculées à nouveau pour chaque changement dans la scène, comme le déplacement d'objets vers un endroit différent ou la mise à jour de la transformation des lumières. En outre, il ne s'applique qu'aux objets marqués comme statiques.

Onglet Eclairage

La cuisson de la lumière peut être effectuée dans l'onglet Éclairage (vous pouvez y accéder via le menu Fenêtre , suivi de Rendu , puis Éclairage ). Les valeurs par défaut de Unity pour la cuisson légère sont considérablement élevées et peuvent prendre trop de temps à traiter. Pour cela, je vous recommande de réduire tous les échantillons directs , les échantillons indirects et les échantillons d'environnement à un nombre inférieur, tel que 16 ou 32.

Ces attributs sont directement liés au nombre d'étapes que le Lightmapper prendra pour calculer la lumière à un point donné de la scène (pour les éléments statiques). L'augmentation de l'un d'entre eux donnera des résultats meilleurs et plus réalistes, mais augmentera considérablement le temps de cuisson.

Je commence généralement par des valeurs faibles pour évaluer les premières impressions dans la scène, puis je les augmente progressivement comme bon me semble. Pour plus de style ou de matériaux personnalisés, l'utilisation de valeurs inférieures est généralement suffisante pour obtenir de bons visuels.

De plus, les premières cuissons peuvent vous montrer des incohérences dans le placement des objets et d'autres erreurs qui doivent être corrigées avant d'appliquer une procédure de cartographie de la lumière plus approfondie et réaliste.

Je recommande également d'activer l' occlusion ambiante de cuisson légère dans les propriétés d'éclairage pour la cuisson. L'occlusion ambiante est l'effet visuel qui se produit lorsque les surfaces sont proches les unes des autres, occultant la lumière et projetant des ombres à leurs points d'intersection, les faisant apparaître plus sombres. Nous l'avons précédemment simulé en utilisant des couleurs de couche de terrain plus sombres, mais la cartographie de la lumière peut en fait calculer l'occlusion ambiante appropriée et l'intégrer dans des textures.

Les images ci-dessous montrent les résultats avec et sans cuisson légère.

Sans cuisson légère

Sans cuisson légère.

Avec cuisson légère

Avec cuisson légère.

Comme vous pouvez le voir, il n'y a pas d'occlusion ambiante entre les troncs d'arbres et le sol. Cependant, la partie inférieure du chaudron et les intersections entre les caisses au dos de l'image sont plus sombres et plus réalistes que leur version sans cuisson.

Placer une skybox et appliquer le post-traitement

Enfin, pour améliorer encore plus les visuels et lier les éléments entre eux, ces deux étapes sont essentielles : placer une meilleure skybox et appliquer quelques effets de post-traitement.

Pour cet article, j'ai utilisé la Free Stylized Skybox de Yuki2022. Ces skyboxes correspondent assez bien au style low poly pour leur aspect cartoon.

La skybox peut être ajoutée sous l' onglet Lighting (le même que le Light Baking), sous l'onglet Environment .

Les effets de post-traitement nécessitent plus d'étapes. Tout d'abord, vous devez ajouter le package de post-traitement à votre projet en fonction de votre pipeline de rendu installé. À l'aide du pipeline de rendu intégré, vous devez installer la pile de post-traitement dans votre projet à l'aide du gestionnaire de packages.

Sinon, si vous utilisez le pipeline de rendu universel ou le pipeline de rendu haute définition, le système de post-traitement équivalent est déjà installé. J'utilise Universal Render Pipeline pour cet article, mais les effets et les configurations sont similaires aux autres pipelines.

L'image ci-dessous montre le volume de post-traitement final que j'ai utilisé pour la version finale du terrain :

Paramètres de volume

Les effets utilisés étaient :

  • Vignette : assombrit les coins de l'écran, concentrant l'attention du joueur sur les éléments centraux
  • Bloom : élargit les éléments de surbrillance de la scène, brouillant leurs zones avec les objets voisins, ce qui agit comme une fausse (mais bonne) alternative d'éclairage global
  • Profondeur de champ : imite la mise au point de nos yeux et des caméras réelles en rendant les éléments plus proches de nous (au point focal) nets, tandis que les autres éléments sont flous en fonction de leur distance
  • Courbes de couleur : modifie les couleurs finales rendues pour correspondre à un look plus recherché. Dans ce cas, j'ai augmenté la couleur rouge par rapport aux autres et légèrement augmenté la couleur bleue
  • Lift Gamma Gain : modifie la couleur globale du rendu et augmente le contrôle des tons sombres, des tons moyens et des hautes lumières

Les résultats finaux pour les deux emplacements que nous avons fait au cours de cet article peuvent être vus dans les images ci-dessous :

Environnement terrain

Exemple de terrain final

Merci d'avoir lu et faites-moi savoir si vous souhaitez en savoir plus sur les nombreux sujets abordés qui n'entraient pas dans le cadre de cet article. Chacun d'entre eux mérite certainement son propre article. Vous pouvez voir plus de mon travail à www.dagongraphics.com .

 Source : https://blog.logrocket.com/easy-environment-design-unity-terrain-features/

 #unity #design 

Conception D'environnement Facile Avec Les Fonctionnalités Unity Terra
高橋  花子

高橋 花子

1660263420

Unity Terrain 機能による簡単な環境設計

Unity Terrainは、レベルおよび環境設計のための Unity ネイティブの強力で多目的なツールです。高さマップの概念を使用して、地形を成形および変更するための簡単なメカニズムを提供します。

この記事では、このツールの基本的な使用方法とその主な機能について説明します。高さマップを使用して地形生成がどのように機能するかについての理論から始めます。次に、地形を変更する主なメカニズムについて説明します。記事の終わりまでに、無料のアセットと Unity Terrain ツールを使用して素敵な Unity シーンを作成するための典型的なワークフローを紹介します。

地形完成例

高さマップから地形まで

ハイト マップは、法線マップの動作と同様に、特定のメッシュがプレイヤーにどのように表示されるかを変更するテクスチャです。より具体的には、高さマップ情報はメッシュの頂点をシフトして、標高、崖、平野、クレーターなどのフィーチャを表示します。

その 2D の性質により、単一の高さマップは一度に頂点の 1 つの軸にしか干渉できません。特に、Unity Terrain の高さマップは、y 軸上の頂点の位置を変更します。

Unity Terrain のパーツを上げたり下げたりして作業する場合、技術的には高さマップを変更しています。変更はすぐに実行され、地形の変更の結果を確認できます。または、高さマップを Unity の外部で直接変更し、高さマップをエンジンにインポートすることもできます。

たとえば、Web サイトCities: Skylines Height Map Generatorは、現実世界から高さマップ情報を抽出するメカニズムを提供します。Web サイトはCities: Skylines ゲームで使用することを意図していますが、マップは Unity でも同様に機能します。ただし、それらには現実世界の距離が含まれており、Unity で最適に使用するために外部ツールで調整する必要がある場合があることを考慮してください。

以下の画像は、ウェブサイトから抽出され、Unity Terrain にインポートされた高さマップを示しています。

Unity の高さマップ

Unity Terrain の高さマップ。

高さマップの視覚化

視覚化された高さマップ (視認性を向上させるために調整された値)。

ユニティ地形設定

選択すると、Terrain ツールバーを使用して Unity Terrain を編集できます。ツールバーには、5 つのセクションにグループ化された地形の主な機能が含まれていますスカルプトとペイント; Tree を追加します。詳細を追加します。および一般設定

ツールバー設定

 

隣接する地形タイルを使用すると、現在の地形の近くにグリッドのような形式で他の地形を作成できます。すでに確立された地形があり、その隣にさらにスペースが必要な場合に使用すると便利です。

これは、地形のサイズを大きくするよりも適切なソリューションです。サイズを変更すると、高さマップの読み取り方法に影響し、地形とそのすべての要素に比例した倍率が適用されます。

高さマップに関しては、新しい隣接地形を作成すると、その高さマップを持つ新しい地形ゲーム オブジェクトが作成されます。新しく作成された高さマップは、境界の値を一致させようとし、接続されていた以前の地形にシームレスに結合します。

Sculpt and Paintオプションは、高さマップを直接操作します。このオプションには、次のセクションで説明するTerrain Toolsが含まれています。つまり、このオプションを使用すると、高さマップを変形し、その一部を非表示にして、たとえば穴や入り口を作成できます。

Add TreesAdd Detailsの両方のオプションを使用して、樹木、茂み、低木、石、草地など、Unity によって自動的に処理される要素を地形に追加します。シーン内でユーザーが配置したゲーム オブジェクトを使用するよりも Unity Terrain を使用する利点の 1 つは、Unity が地形の要素に対して多くの最適化手法を管理することです。ツリーと詳細は、ビジビリティ カリング関連のアルゴリズムとビルボードで自動的に考慮されます。

最後に、General Settings オプションは、最適化手法 (詳細距離、ビ​​ルボード距離、影など) や地形のサイズなど、地形ゲーム オブジェクトに関するすべての関連情報を保持します。

一般設定は、基本的な地形樹木と詳細オブジェクト草の風の設定メッシュの解像度穴の設定テクスチャの解像度照明、およびライトマッピングに分かれています。各オプションを広範にカバーすることは、この記事の範囲外ですが、最もよく使用されるオプションについて説明します。

地形設定

基本的な地形セクションでは、地形のマテリアル、影のプロパティ、使用される描画モードなど、地形の主なレンダリングの側面について説明します。自作のポストプロセッシング シェーダーまたはスクリプト化されたレンダリング パスを使用する予定がある場合は、[ Draw Instanced ] オプションを無効にする必要があることに注意してください。

Mesh Resolutionの値は、地形のサイズを決定します。地形の幅と長さを変更すると、高さマップ情報の読み取り方法がすぐに変わります。地形の幅、長さ、および高さによって、地形の x、y、および z 軸のサイズが決まります。

Texture Resolutionsセクションは、より具体的な高さマップ テクスチャ情報を処理します。高さマップの解像度を設定し、そこで高さマップをインポート/エクスポートできます。インポート機能を使用すると、他のソース (前に示したものなど) から取得したデータをマップに使用できます。また、現在の高さマップをエクスポートし、外部ツールで変更してからインポートし直すことも一般的です。

地形の彫刻と変更のオプション

前述のように、スカルプトとペイントオプションを使用すると、高さマップを変更して地形を変更できます。これらのオプションには 6 つのツールがあります。[地形を上昇または下降] 、 [ペイント] 、[テクスチャをペイント] 、[高さを設定] 、[高さスムーズ] 、および[地形をスタンプ]です。

スカルプトおよびペイント ツール

Raise or Lower TerrainSet Height、およびSmooth Heightのオプションはすべて、値を増減して高さマップに直接影響を与えるために使用されます。高さの設定は、特定の目的の結果に合わせて高さマップを変更するために使用されます。一方、高さのスムーズ化は、高さマップで選択された領域をぼかすかのように地形を柔らかくします。これらのオプションはすべて、地形ブラシを使用して行います (上の画像を参照)。

地形ブラシは、GIMP や Photoshop などの他の画像編集ソフトウェアでのブラシの動作に似ています。ブラシは、グレーとアルファの陰影でパターンを表示します。これは、地形に適用され、その値を使用して形作られます。ブラシの透明な領域は地形に影響を与えず、暗い領域は明るい領域よりも地形に影響を与えます。Unity のデフォルトの地形ブラシ セットには、10 以上のブラシが付属しています。

Paint Textureオプションは、地形にテクスチャ レイヤーを適用するために使用され、積極的に色やその他のテクスチャ プロパティを追加しますテクスチャ レイヤーは、他のスカルプト ツールとペイント ツールで使用されるのと同じ地形ブラシを使用して適用されます。ただし、Unity のデフォルトの地形にはテクスチャ レイヤーがありません。

地形レイヤー

地形レイヤーのリスト。

1 つの地形レイヤー

1 つの地形レイヤーのプロパティ。

各テレイン レイヤーは独自のアセットであり、異なるシーンであっても、複数のテレインで再利用できます。追加された最初の地形レイヤーは、自動的に地形全体をカバーします。これを使用して、基本色を簡単に設定できます。各テレイン レイヤーには、拡散マップ、法線マップ、およびマスク マップを含めることができます。

拡散反射光マップは、地形によって表示されるテクスチャです。設定すると、色合いを変更するオプションが表示されます。色合いを変更することは、追加のテクスチャを使用したり、外部プログラムでアセットを編集したりすることなく、地形のバリエーションを作成するための迅速かつ効果的な方法です。

法線マップはレイヤー上の法線情報を伝達するために使用され、通常は通常の法線マップと同じように機能しますが、マスク マップは高解像度およびユニバーサル レンダー パイプラインで明示的に使用され、メタリック、アンビエント オクルージョンなどのより多くの情報を伝達します。高さ、滑らかさ。

最後に、タイル設定は地形レイヤーの便利なオプションです。サイズとオフセットの値は、テクスチャが地形にタイルを張る頻度と、各繰り返しのベース オフセットを変更します。

一般に、大規模な地形の場合、地形の床で目に見える繰り返しを壊すのに役立つため、さまざまなサイズの値を持つ地形レイヤーのバリエーションをいくつか用意することをお勧めします。

前に述べたように、地形ブラシはデフォルトで Unity の地形に付属していますが、新しい地形オブジェクトを作成する場合、地形レイヤーはありません。

さらに、地形レイヤーを作成するにはマップとテクスチャが必要です。この記事では、アセット ストア ( Flaming Sands によるGeneric Terrain BrushesとStampIT! Collection Examples by Rowlan Inc) からのいくつかの新しい無料のブラシと、地形レイヤー用の無料のテクスチャのパッケージ (手描きの草と地面) を使用しました。クロミスによるテクスチャ)。

いくつかのテクスチャを含むベース テレインの結果を以下に示します。

テクスチャ付きの基本地形

地形で作業している間、私は通常、ピークエリアと平野エリアのバランスをとろうとします. これにより、植生を広げてスペースを過密にするのに十分なスペースが得られる傾向があります。さらに、山、空き地、川など、主要な構成要素と焦点を維持するために、地形に 1 つの重要な要素を持たせるようにしています。

主要なコンポーネントが 1 つあると、どのアセットを使用し、どのように配布するかを決定するのに役立ちます。この場合、洞窟のような入り口のある小さな丘を作り、より多くの地形オプションと、ビューを多様化するためのいくつかの小さな関心領域を探索します。

また、複数の地形レイヤーを探索して、それらが作りがちな視覚的な繰り返しを打破しようとしたことにも注意してください。この段階では、丘を通常の芝生や平地から分離し、あちこちに砂や暗い芝生の床のパッチを散らして、主要なスポットのみをブロックしようとします.

木、草、そして…石を植える?

植生は、 [ツリーの追加] オプションまたは [詳細の追加] オプションの 2 つの方法で追加できます。Add Treeは非常に簡単で、木のような特性を持つオブジェクトを配置できます。

相乗効果を高めるには、 Speed TreeTree Editorなどの Unity ツールを使用するか、特定の地形ツリー マテリアルを使用してツリーを作成する必要があります。ただし、これは必須ではありません。次のセクションでは、同様の結果をもたらし、より創造的な自由を可能にする別のアプローチを示します。

いずれにせよ、地形の木には、メッシュごとに最大 2 つのマテリアル (樹皮用と葉用) の制限があります。メッシュに 3 つ以上のマテリアルがある場合、正しくレンダリングされない可能性があります。この制限に対する一般的な解決策は、テクスチャ アトラスに頼ることです。また、より興味深い効果を得るために、葉のマテリアルを樹皮のマテリアルから分離するのが一般的です。これにより、葉だけのシェーダーをプログラムできます (風の動きを偽装するなど)。

木

地形レイヤーと同様に、樹木も個別に地形に追加し、地形のブラシを使用して配置する必要があります。ツリーは、ベア メッシュとしてもプレハブとしても追加できます。

ツリーをプレハブとして追加しても、すべてのプレハブ要素が追加されるわけではなく、意図したプレハブとまったく同じように動作するとは限らないことに注意してください。たとえば、アニメーターを使用してツリー プレハブを配置すると、アニメーターは無視され、静的ツリーとして機能します。

岩と草

同様に、ディテールは、地形に配置してそのビジュアルを多様化できる要素です。詳細には、詳細メッシュと草のテクスチャの 2 つのタイプがあります。詳細メッシュは、石、小石、さらには低木など、シーン内で静止している通常のメッシュのように機能します。

詳細メッシュは 1 つのマテリアルに限定されます。複数のマテリアルでメッシュを使用しようとすると、正しくレンダリングされません。

詳細メッシュは、Vertex Lit または Grass としてレンダリングすることもできます。Vertex Lit は、メッシュを通常のライティング ゲーム オブジェクトとしてレンダリングし、風に反応しないため、石や木の切り株に適しています。草のレンダリングは草のテクスチャと同様に機能し、地形の風がメッシュに影響を与えることができます。

ただし、経験上、草のレンダリングは、風の曲げプロパティが低い値に設定されている場合にのみ ( [地形の一般設定]で)、視覚的に満足のいく結果をもたらします。そうしないと、詳細メッシュが非現実的に動き回る可能性があります。

Unity での滑らかな草のアニメーション

草のテクスチャは、地形の風に応じて動作する地形でレンダリングされるスプライトです。Unity のドキュメントに記載されているように、同じツールで花や棒などの一般的なテクスチャを使用できるため、「Grass Texture」という用語は誤解を招くものです。草としてレンダリングされたディテール メッシュとは異なり、草のテクスチャは地面にピボットを保持し、風が同じ位置を維持して移動します — 草の動作が予想されるように。

この記事では、アセット ストア ( JustCreateの Low-Poly Simple Nature PackとBroken VectorのLow Poly Rock Pack ) から木とその他のメッシュを使用しました。草のテクスチャには、著作権フリーで高品質なアセットの優れたソースである Kenneyの優れたFoliage Spritesを使用しました。最後に、追加の装飾アセットとして、JustCreateのLow Poly Dungeons Liteを使用しました。

これらを地形に適用すると、次の結果が得られます。

地形結果

LOD グループを使用してより用途の広いツリー

前に述べたように、Unity のツリーは、地形システムによって行われる最適化のために何らかの制限があります。カスタム マテリアルまたはその他のバリエーションを使用すると、一部の木の配置オプションが無視され、非常に当たり障りのない反復的なシーンになる可能性があります。

ただし、このシステムにより、その制限の一部をバイパスして、材料の使用法とスタイルに関するより高いレベルの汎用性を実現することもできます。そのためには、特定のプロセスに従う必要があります。

最初に、ルート ゲーム オブジェクト (階層の最初) がLOD グループコンポーネントを持つ空のゲーム オブジェクトであるツリーの新しいプレハブを作成する必要があります。次に、ツリー プレハブをルート ゲーム オブジェクトの下の階層に配置できます。その後、木のプレハブを LOD グループに追加します。

木のプレハブは、カスタム シェーダーと連動し、木の幅、高さ、回転の変更など、通常の木配置プロパティを地形から受け取ります。色のバリエーションについては、_TreeInstanceColor プロパティを読み取るようにシェーダーを変更する必要がありますが、この手順はこの記事の範囲外です。

もちろん、すでに LOD グループ機能を使用して、ツリーの他のレベルの詳細を処理することもできます。ただし、地形配置プロパティを使用しながら、より用途の広いマテリアルが必要な場合は、LOD の数を 1 に変更して、1 つの木のプレハブのみを使用できます。

これらの手順を実行すると、次のような結果が得られます。

ツリー LOD

より良い結果を得るための提案された地形ワークフロー

地形ツールの使用方法の主な側面について説明したので、環境を改善できる他のレンダリングの側面についても説明しながら、あまり労力をかけずに適度に良い結果を得るために使用する簡単なワークフローを提供したいと思います。

洞窟の入り口を作る

先ほど紹介した洞窟から始めましょう。Unity Terrain では、入り口を作成したり、高さマップを水平に切り分けたりすることはできません (xz 平面で高さ情報を適用します)。ただし、穴をペイントすることができます。

Unity Terrain の穴は、あたかも断片を切断しているかのように、地形メッシュの一部を隠します。穴は、地形ブラシを使用して地形のスカルプト ツールの他の要素と同様にペイントできます。

視覚的にエキサイティングな洞窟の入り口を作るには、標高を 40 ~ 60 度に保ちながら地形を高くすることをお勧めします。それを超えると、地形グリッドが伸びすぎて、他の要素やプレハブでカバーするのが難しくなる可能性があります。そうすることで、以下のような結果が得られるはずです。

地形の引き裂き

ご覧のとおり、テレイン ホールは、バイナリ形式でテレインの一部を隠しています。それらは完全になくなっているか、完全に表示されています。そのためには、継ぎ目をアセットやその他の要素で覆い、視覚的な品質を向上させる必要があります。

地形図

前述と同じ無料のアセットを使用して、洞窟の入り口の周りのさまざまな石をスケーリングおよび回転させて、穴と目に見える地形の間の継ぎ目を隠しました。また、より自然な外観を作成するために、必要以上に岩や小石を追加しようとしました. 高さマップを調整して岩との接続をより良くするのが一般的です。

さらに、関心のある領域の周りに追加の小道具を追加することは、プレーヤーの集中力を高めるために常に良いことです. そのために、地面に半分埋まっている壷をいくつか追加し、洞窟のすぐ前に柱を追加しました。これは、謎と物語を作成するのに役立ちます. 洞窟の入り口の横に小さなライトを追加して、目をさらに誘導しました.

洞窟の入り口の光

前に見たように、この穴により、ユーザーは地形を透かして見ることができます。それを避けるために、ライティングを無視する特定のマテリアル (Unlit) を持つプレーンを使用しました。そうしないと、シーンのライトの変化によってこれらの暗い壁がソリッドとして表示され、必要な視覚的結果は暗い/影の領域のように見えるため、これは重要です。

さらに、これまで使用してきた同じ地形を使用できないため、Low Poly パッケージのメッシュの一部を洞窟の地面として使用しました。

これまでに行った洞窟は、プレイヤーがその中に入って移動したい場合には理想的ではないことに注意することが重要です。そのためには、穴の周囲にメッシュを適切に設定し、洞窟の内部をあらゆる側面からモデリングする必要があります。新しい地形オブジェクトを使用してそれを達成することは可能ですが、そのために必要な作業は最適ではない可能性があると思います.

一方、このソリューションは、プレーヤーを洞窟の内部専用の新しいシーンに移動するためのポイントとして使用するのに非常に適しています。

手動アンビエント オクルージョン

アンビエント オクルージョンの例

アンビエント オクルージョンの例 2

Unity Terrain の照明を改善するために私が使用する一般的な手法は、地面に配置されたオブジェクトの下にある地形レイヤーの暗いバリエーションを手動で使用することです。これは、特にカスタム シェーダーやその他の技術を使用している場合、アンビエント オクルージョンなどの一部の照明効果が地形オブジェクトに適用されないため、地形要素のまとまりを高めるのに役立ちます。

地形の例のオクルージョン

これは影を落とすこととは異なることに注意してください。Unity テレインはオブジェクトから正しく影を落としますが、それでもオブジェクトが浮いているか、実際には接続されていないという印象を与える可能性があります。一部のスポットで暗い地形レイヤーを使用すると、より多くのリソースや高価な照明計算を必要とせずに、オブジェクトと地面を近づけることができます。

また、地形オブジェクトではない他のオブジェクトやプレハブの下部にこれを適用することがよくあります。上の画像に見られるように、本と大釜の下に少し暗いトーンを使用して、要素を近づけました.

リアル アンビエント オクルージョンとライト ベイク

より暗いレイヤーはある程度フェイク ライティングに役立ちますが、シーン内でライトをベイクすることで増幅することができます。ライト ベイクの完全な説明はこの記事の範囲外ですが、主な要素のいくつかを使用して地形のビジュアルを改善する方法について説明します。

まず、ライト ベイクは、シーン内のライトを事前に計算し、データを保存して、シーン内のオブジェクトの上に適用するプロセスです。Unity は特に、オブジェクトの表面の明るさをテクスチャ マップに焼き付ける Lightmappingを使用します。

ただし、これらのテクスチャは実行時に変更することはできず、オブジェクトを別の場所に移動したり、ライトのトランスフォームを更新したりするなど、シーン内の変更ごとに再計算する必要があります。また、静的フラグが設定されたオブジェクトにのみ適用されます。

照明タブ

ライトのベイクは、[ライティング] タブで実行できます ([ウィンドウ] メニュー、 [レンダリング] 、 [ライティング] の順に選択してアクセスできます)。Unity のライト ベイクのデフォルト値はかなり高く、処理に時間がかかりすぎる可能性があります。そのためには、すべての直接サンプル間接サンプル、および環境サンプルを 16 または 32 などの低い数値に減らすことをお勧めします。

これらの属性は、ライトマッパーがシーン内の特定のポイント (静的要素の場合) でライトを計算するために実行するステップ数に直接関連しています。いずれかを大きくすると、よりリアルな結果が得られますが、ベイク時間が大幅に長くなります。

通常、シーンの第一印象を評価するために低い値から始めて、必要に応じて値を徐々に上げていきます。より多くのスタイルまたはカスタム マテリアルの場合、通常は低い値を使用するだけで、良好なビジュアルを得ることができます。

さらに、最初のベイクでは、オブジェクトの配置の不一致やその他のエラーが表示される場合があります。これらのエラーは、より完全で現実的なライト マッピング手順を適用する前に修正する必要があります。

また、ベイクのライティング プロパティでライト ベイクのアンビエント オクルージョンをオンにすることをお勧めします。アンビエント オクルージョンは、サーフェスが互いに接近しているときに発生する視覚効果であり、光を遮り、交点に影を投影して、それらをより暗く見せます。以前は、より暗い地形レイヤーの色を使用して偽装していましたが、ライト マッピングは実際には適切なアンビエント オクルージョンを計算し、テクスチャに焼き付けることができます。

以下の画像は、ライト ベイクを使用した場合と使用しない場合の結果を示しています。

ライトベーキングなし

ライトベーキングなし。

ライトベーキングあり

ライトベーキング付き。

ご覧のとおり、木の幹と地面の間にアンビエント オクルージョンはありません。ただし、大釜の下部と、画像の背面にある箱の間の交点は、焼かないバージョンよりも暗く、よりリアルです。

スカイボックスの配置と後処理の適用

最後に、ビジュアルをさらに改善して要素を結び付けるには、次の 2 つの手順が不可欠です。より良いスカイボックスを配置し、いくつかの後処理効果を適用します。

この記事では、Yuki2022 のFree Stylized Skybox を使用しました。これらのスカイボックスは、ローポリ スタイルに非常によく合い、漫画のような外観になっています。

スカイボックスはLightingタブ (Light Baking と同じ) のEnvironmentタブの下に追加できます。

後処理効果には、さらに多くの手順が必要です。最初に、インストールされているレンダリング パイプラインに応じて、後処理パッケージをプロジェクトに追加する必要があります。ビルトイン レンダー パイプラインを使用するには、パッケージ マネージャーを使用してプロジェクトに後処理スタックをインストールする必要があります。

それ以外の場合、Universal Render Pipeline または High Definition Render Pipeline を使用している場合は、同等の後処理システムが既にインストールされています。この記事ではUniversal Render Pipelineを使用していますが、効果と構成は他のパイプラインと同様です。

下の画像は、地形の最終バージョンに使用した最終的な後処理ボリュームを示しています。

音量設定

使用された効果は次のとおりです。

  • ビネット: 画面の隅を暗くし、プレーヤーの注意を中央の要素に集中させます
  • ブルーム: シーン内のハイライト要素を拡張し、それらの領域を近隣のオブジェクトでぼかします。これは、偽の (しかし良い) グローバル イルミネーションの代替として機能します。
  • 被写界深度: 私たちに近い (焦点にある) 要素をシャープにすることで、目と実際のカメラの焦点を模倣し、他の要素は距離に応じてぼやけます。
  • カラー カーブ: より望ましい外観に一致するようにレンダリングされる最終的な色を変更します。この場合、他の色よりも赤を増やし、青を少し増やしました
  • リフト ガンマ ゲイン: レンダリングの全体的な色をシフトし、ダーク トーン、ミッドレンジ トーン、およびハイライトのコントロールを増加させます

この記事で行った 2 つの場所の最終結果は、以下の画像で確認できます。

地形環境

最終的な地形の例

読んでくれてありがとう。この記事の範囲外で議論された多くのトピックについてもっと読みたい場合はお知らせください。それらのそれぞれは、確かに独自の記事に値します。私の作品はwww.dagongraphics.comでもっと見ることができます。

 ソース: https://blog.logrocket.com/easy-environment-design-unity-terrain-features/

 #unity #design 

Unity Terrain 機能による簡単な環境設計

Easy Environment Design with Unity Terrain Features

The Unity Terrain is a Unity native, powerful, and versatile tool for level and environmental designing. It provides easy mechanisms to mold and modify terrains using the concept of height maps.

In this article, we will see the basics of how to use this tool and its main features. We start with the theory of how terrain generation works by using height maps. Then we will discuss the primary mechanisms to modify a terrain. By the end of the article, I present a typical workflow for making nice Unity scenes using free assets and the Unity Terrain tool.

 See more at: https://blog.logrocket.com/easy-environment-design-unity-terrain-features/

#unity #design 

Easy Environment Design with Unity Terrain Features

How to Design a Beautiful Mobile App Icon

App icons are tiny in size but make a significant impact on the success of your app.

But do you know how to design a great app icon that will outperform your competitors?

We've put up this handy guide that covers all you need to know about creating app icons:

https://multiqos.com/how-to-design-your-mobile-app-icon-stand-out

#appicon #mobileappicon #icondesign #design #mobileapp #appicons #mobileappicons

How to Design a Beautiful Mobile App Icon
伊藤  直子

伊藤 直子

1657654200

Solidityデザインパターンの使用方法

ブロックチェーンとDApp(分散型アプリケーション)の人気が継続的に高まっているため、オープンソースのDAppは、さまざまな開発者からの貢献が増えています。ほとんどのDAppとブロックチェーンアプリケーションの中心は、Solidityを使用して開発されたスマートコントラクトです。

オープンソースプロジェクトへの貢献は、Solidityコミュニティ内で懸念を引き起こします。これらのプロジェクトは人々のお金に現実世界の影響を及ぼし、さまざまなバックグラウンドの開発者がプロ​​ジェクトで共同作業を行う場合、アプリケーションでエラーやコードの競合が発生することはほぼ確実です。これが、DAppの適切な標準を実践することが非常に重要である理由です。

優れた標準を維持し、リスクを排除し、競合を軽減し、スケーラブルで安全なスマートコントラクトを構築するには、Solidityでのデザインパターンとスタイルの正しい実装を研究して使用する必要があります。

この記事では、Solidityのデザインパターンについて説明します。フォローするには、Solidityに精通している必要があります。

Solidityデザインパターンとは何ですか?

開発者は、オンラインのさまざまなリソースからSolidityの使用法を学ぶことができますが、Solidityに実装する方法やスタイルはさまざまであるため、これらの資料は同じではありません。

デザインパターンは再利用可能な従来のソリューションであり、繰り返し発生するデザインの欠陥を解決するために使用されます。あるアドレスから別のアドレスに転送することは、デザインパターンで調整できるSolidityで頻繁に懸念される実際的な例です。

SolidityでEtherを転送するときは、、、、またはメソッドを使用SendTransferますCall。これらの3つの方法には、同じ単一の目標があります。それは、スマートコントラクトからEtherを送信することです。この目的のためにTransferとメソッドを使用する方法を見てみましょう。Call次のコードサンプルは、さまざまな実装を示しています。

最初はTransfer方法です。このアプローチを使用する場合、受信するすべてのスマートコントラクトはフォールバック機能を定義する必要があります。そうしないと、転送トランザクションが失敗します。利用可能なガスの制限は2300であり、これは転送トランザクションを完了するのに十分であり、再突入攻撃の防止に役立ちます。

function Transfer(address payable _to) public payable {     
  _to.transfer(msg.value); 
} 

上記のコードスニペットTransferは、受信アドレスをとして受け入れ_to、メソッドを使用して_to.transferとして指定されたEtherの転送を開始する関数を定義しますmsg.value

次はそのCall方法です。契約の他の機能は、この方法を使用してトリガーでき、オプションで、機能の実行時に使用するガス料金を設定できます。

 

function Call(address payable _to) public payable {
    (bool sent) = _to.call.gas(1000){value: msg.value}("");
    require("Sent, Ether not sent");
}

上記のコードスニペットCallは、受信アドレスをとして受け入れ_to、トランザクションステータスをブール値として設定する関数を定義し、返される結果はデー​​タ変数で提供されます。msg.data空の場合、関数はメソッドreceiveの直後に実行されます。Callフォールバックは、receive関数の実装がない場合に実行されます。

スマートコントラクト間でEtherを転送する最も好ましい方法は、このCall方法を使用することです。

上記の例では、2つの異なる手法を使用してEtherを転送しました。を使用して消費するガスの量を指定できますがCallTransferデフォルトではガスの量は固定されています。

これらの手法は、Solidityで繰り返し発生するを実装するために実践されているパターンですTransfer

状況を把握するために、次のセクションはSolidityが規制しているデザインパターンの一部です。

行動パターン

ガードチェック

スマートコントラクトの主な機能は、トランザクションの要件が確実に通過するようにすることです。いずれかの条件が失敗すると、コントラクトは以前の状態に戻ります。Solidityは、EVMのエラー処理メカニズムを使用して例外をスローし、例外の前にコントラクトを動作状態に復元することでこれを実現します。

以下のスマートコントラクトは、3つの手法すべてを使用してガードチェックパターンを実装する方法を示しています。

contract Contribution {
  function contribute (address _from) payable public {
    require(msg.value != 0);
    require(_from != address(0));
    unit prevBalance = this.balance;
    unit amount;

    if(_from.balance == 0) {
      amount = msg.value;
    } else if (_from.balance < msg.sender.balance) {
      amount = msg.value / 2;
    } else {
      revert("Insufficent Balance!!!");
    }

    _from.transfer(amount);
    assert(this.balance == prevBalance - amount);
  }
}

上記のコードスニペットでは、Solidityは以下を使用してエラー例外を処理します。

require()関数が実行される条件を宣言します。引数として単一の条件を受け入れ、条件がfalseと評価された場合は例外をスローし、ガスを燃焼せずに関数の実行を終了します。

assert()関数の条件を評価してから例外をスローし、コントラクトを前の状態に戻し、実行後に要件が失敗した場合はガス供給を消費します。

revert()例外をスローし、供給されたガスを返し、関数の要件が満たされない場合は、関数呼び出しをコントラクトの元の状態に戻します。このrevert()メソッドは、条件を評価または必要としません。

ステートマシン

ステートマシンパターンは、以前の入力と現在の入力に基づいてシステムの動作をシミュレートします。開発者はこのアプローチを使用して、大きな問題を単純なステージと遷移に分解し、それらを使用してアプリケーションの実行フローを表現および制御します。

以下のコードスニペットに示すように、ステートマシンパターンはスマートコントラクトで実装することもできます。

contract Safe {
    Stages public stage = Stages.AcceptingDeposits;
    uint public creationTime = now;
    mapping (address => uint) balances;

    modifier atStage(Stages _stage) {
      require(stage == _stage);
      _;
    }

    modifier timedTransitions() {
      if (stage == Stages.AcceptingDeposits && now >=
      creationTime + 1 days)
      nextStage();
      if (stage == Stages.FreezingDeposits && now >=
      creationTime + 4 days)
      nextStage();
      _;
    }
    function nextStage() internal {
      stage = Stages(uint(stage) + 1);
    }
    function deposit() public payable timedTransitions atStage(Stages.AcceptingDeposits) {
      balances[msg.sender] += msg.value;
    }
    function withdraw() public timedTransitions atStage(Stages.ReleasingDeposits) {
      uint amount = balances[msg.sender];
      balances[msg.sender] = 0;
      msg.sender.transfer(amount);
    }
}

上記のコードスニペットでは、Safeコントラクトは修飾子を使用して、さまざまなステージ間のコントラクトの状態を更新します。ステージは、いつ入出金ができるかを決定します。契約の現在の状態がそうでない場合AcceptingDeposit、ユーザーは契約に預金することができず、現在の状態がそうでない場合ReleasingDeposit、ユーザーは契約から撤退することはできません。

オラクル

イーサリアム契約には、通信する独自​​のエコシステムがあります。システムはトランザクションを介して(データをメソッドに渡すことによって)外部データのみをインポートできます。これは、多くの契約のユースケースがブロックチェーン以外のソース(株式市場など)からの知識を伴うため、欠点です。

この問題の1つの解決策は、外界への接続でoracleパターンを使用することです。オラクルサービスとスマートコントラクトが非同期で通信する場合、オラクルサービスはAPIとして機能します。トランザクションは、オラクルにリクエストを送信するための命令を含むスマートコントラクト関数を呼び出すことから始まります。

このようなリクエストのパラメータに基づいて、Oracleは結果をフェッチし、プライマリコントラクトでコールバック関数を実行して結果を返します。Oracleベースの契約は、単一の組織またはグループの誠実さに依存しているため、分散型ネットワークのブロックチェーンの概念と互換性がありません。

オラクルのサービス21および22は、提供されたデータを使用して妥当性チェックを提供することにより、この欠陥に対処します。オラクルはコールバックの呼び出しに対して料金を支払う必要があることに注意してください。したがって、オラクル料金は、コールバックの呼び出しに必要なEtherと一緒に支払われます。

以下のコードスニペットは、オラクルコントラクトとそのコンシューマーコントラクト間のトランザクションを示しています。

contract API {
    address trustedAccount = 0x000...; //Account address
    struct Request {
        bytes data;
        function(bytes memory) external callback;
    }
    Request[] requests;
    event NewRequest(uint);

    modifier onlyowner(address account) {
        require(msg.sender == account);
        _;
    }
    function query(bytes data, function(bytes memory) external callback) public {
        requests.push(Request(data, callback));
        NewRequest(requests.length - 1);
    }
    // invoked by outside world
    function reply(uint requestID, bytes response) public
    onlyowner(trustedAccount) {
    requests[requestID].callback(response);
    }
}

上記のコードスニペットでは、スマートコントラクトは関数を使用してAPIクエリリクエストを送信します。関数は関数を実行し、関数を使用して外部ソースから応答データを収集します。knownSourcequeryexternal callbackreply

ランダム性

Solidityでランダムで一意の値を生成するのは非常に難しいですが、需要が高くなっています。ブロックタイムスタンプはイーサリアムのランダム性の原因ですが、マイナーがそれらを改ざんする可能性があるため、リスクがあります。この問題を防ぐために、ブロックハッシュPRNGやOracleRNGなどのソリューションが作成されました。

次のコードスニペットは、最新のブロックハッシュを使用したこのパターンの基本的な実装を示しています。

// This method is predicatable. Use with care!
function random() internal view returns (uint) {
    return uint(blockhash(block.number - 1));
}

上記の関数は、ブロック番号( 、ブロックチェーン上の変数)をrandomNum()ハッシュすることにより、ランダムで一意の整数を生成します。block.number

セキュリティパターン

アクセス制限

Solidityには実行権限を管理するための組み込みの手段がないため、一般的な傾向の1つは、関数の実行を制限することです。関数の実行は、タイミング、呼び出し元またはトランザクション情報、その他の基準などの特定の条件でのみ行う必要があります。

関数の条件付けの例を次に示します。

contract RestrictPayment {
    uint public date_time = now;

    modifier only(address account) {
        require(msg.sender == account);
        _;
    }

    function f() payable onlyowner(date_time + 1 minutes){
      //code comes here
    }
}

上記のRestrictコントラクトは、accountとは異なる機能msg.senderを実行することを防ぎpayableます。関数の要件が満たされてpayableいない場合requireは、関数が実行される前に例外をスローするために使用されます。

効果の相互作用を確認する

チェック効果の相互作用パターンにより、悪意のある契約が外部呼び出しに続いて制御フローを乗っ取ろうとするリスクが減少します。契約は、Ether転送手順中に制御フローを外部エンティティに転送している可能性があります。外部コントラクトが悪意のあるものである場合、制御フローを混乱させ、送信者を望ましくない状態にリバウンドさせる可能性があります。

このパターンを使用するには、機能のどの部分が脆弱であるかを認識して、脆弱性の原因が見つかったら対応できるようにする必要があります。

このパターンの使用例を次に示します。

contract CheckedTransactions {
    mapping(address => uint) balances;
    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        msg.sender.transfer(amount);
    }
}

上記のコードスニペットではrequire()、条件balances[msg.sender] >= amountが失敗した場合に例外をスローするメソッドが使用されています。つまり、ユーザーはamountの残高を超えて引き出すことはできませんmsg.sender

安全なEther転送

暗号通貨の転送はSolidityの主要な機能ではありませんが、頻繁に発生します。前に説明したようTransferに、、、CallおよびSendは、EtherをSolidityに転送するための3つの基本的な手法です。それらの違いを知らない限り、どちらの方法を使用するかを決めることは不可能です。

この記事で前述した2つの方法(TransferおよびCall)に加えて、EtherをSolidityで送信するには、この方法を使用できますSend

SendTransferデフォルト(2300)と同じ量のガスがかかるという点で似ています。ただし、とは異なり、成功したTransferかどうかを示すブール結果を返します。SendほとんどのSolidityプロジェクトは、このメソッドを使用しなくなりましたSend

以下は、Sendメソッドの実装です。

function send(address payable _to) external payable{
    bool sent = _to.send(123);
    require(sent, "send failed");
}

上記の関数は、sentから返されたsentの値がである場合に、この関数をsend使用してrequire()例外をスローします。Boolean_to.send(123)false

プルオーバープッシュ

この設計パターンは、Ether転送のリスクを契約からユーザーにシフトします。Ether転送中に、いくつかの問題が発生し、トランザクションが失敗する可能性があります。プルオーバープッシュパターンでは、転送を開始するエンティティ(契約の作成者)、スマートコントラクト、および受信者の3つの関係者が関与します。

このパターンには、ユーザーの未払い残高の追跡に役立つマッピングが含まれます。コントラクトから受信者にEtherを配信する代わりに、ユーザーは割り当てられたEtherを撤回する関数を呼び出します。転送の1つに不正確さがあったとしても、他のトランザクションには影響しません。

以下は、プルオーバープルの例です。

contract ProfitsWithdrawal {
    mapping(address => uint) profits;
    function allowPull(address owner, uint amount) private {
        profits[owner] += amount;
    }
    function withdrawProfits() public {
        uint amount = profits[msg.sender];
        require(amount != 0);
        require(address(this).balance >= amount);
        profits[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

上記のProfitsWithdrawal契約addressでは、ユーザーの残高がユーザーに割り当てられた利益以上である場合、ユーザーは自分にマップされた利益を引き出すことができます。

緊急停止

監査されたスマートコントラクトには、サイバーインシデントに関与するまで検出されないバグが含まれている可能性があります。契約開始後に発見されたエラーは修正が困難です。この設計の助けを借りて、重要な機能への呼び出しをブロックすることでコントラクトを停止し、スマートコントラクトが修正されるまで攻撃者を防ぐことができます。

許可されたユーザーのみが停止機能を使用して、ユーザーがそれを悪用するのを防ぐことができます。状態変数はからfalseに設定されtrue、契約の終了を決定します。契約を終了した後、アクセス制限パターンを使用して、重要な機能が実行されないようにすることができます。

以下に示すように、状態変数が非常停止缶の開始を示している場合に例外をスローする関数変更を使用して、これを実現します。

contract EmergencyStop {
    bool Running = true;
    address trustedAccount = 0x000...; //Account address
    modifier stillRunning {
        require(Running);
        _;
    }
    modifier NotRunning {
        require(¡Running!);
        _;
    }
    modifier onlyAuthorized(address account) {
        require(msg.sender == account);
        _;
    }
    function stopContract() public onlyAuthorized(trustedAccount) {
        Running = false;
    }
    function resumeContract() public onlyAuthorized(trustedAccount) {
        Running = true;
    }
}

上記のEmergencyStopコントラクトは、修飾子を使用して条件をチェックし、これらの条件のいずれかが満たされた場合に例外をスローします。コントラクトは、stopContract()andresumeContract()関数を使用して緊急事態を処理します。

状態変数をにリセットすることで、コントラクトを再開できますfalse。この方法は、非常停止機能と同じように、不正な呼び出しから保護する必要があります。

アップグレード可能性パターン

プロキシデリゲート

このパターンにより、コンポーネントを壊すことなくスマートコントラクトをアップグレードできます。Delegatecallこのメソッドを使用する場合、呼び出される特定のメッセージが使用されます。関数シグネチャを公開せずに、関数呼び出しをデリゲートに転送します。

プロキシコントラクトのフォールバック機能は、それを使用して、各関数呼び出しの転送メカニズムを開始します。Delegatecall返されるのは、実行が成功したかどうかを示すブール値だけです。関数呼び出しの戻り値にもっと関心があります。コントラクトをアップグレードするときは、ストレージシーケンスを変更してはならないことに注意してください。追加のみが許可されます。

このパターンを実装する例を次に示します。

contract UpgradeProxy {
    address delegate;
    address owner = msg.sender;
    function upgradeDelegate(address newDelegateAddress) public {
        require(msg.sender == owner);
        delegate = newDelegateAddress;
    }
    function() external payable {
        assembly {
            let _target := sload(0)
            calldatacopy(0x01, 0x01, calldatasize)
            let result := delegatecall(gas, _target, 0x01, calldatasize, 0x01, 0)
            returndatacopy(0x01, 0x01, returndatasize)
            switch result case 0 {revert(0, 0)} default {return (0, returndatasize)}
        }
    }
}

上記のコードスニペットでは、コントラクトデータのコピーを新しいバージョンに転送するフォールバック関数を呼び出すことにより、コントラクトの実行後にコントラクトをアップグレードUpgradeProxyできるメカニズムを処理します。delegateownerdelegate

メモリアレイの構築

このメソッドは、コントラクトストレージからデータを迅速かつ効率的に集約および取得します。コントラクトのメモリとの対話は、EVMで最もコストのかかるアクションの1つです。冗長性を確実に削除し、必要なデータのみを保存することで、コストを最小限に抑えることができます。

ビュー機能の変更により、追加費用を発生させることなく、契約ストレージからデータを集約して読み取ることができます。配列をストレージに格納する代わりに、検索が必要になるたびにメモリに再作成されます。

配列などの反復が容易なデータ構造を使用して、データの取得を容易にします。複数の属性を持つデータを処理する場合、structなどのカスタムデータ型を使用してデータを集約します。

各集約インスタンスの予想されるデータ入力数を追跡するには、マッピングも必要です。

以下のコードは、このパターンを示しています。

contract Store {
    struct Item {
        string name;
        uint32 price;
        address owner;
    }
    Item[] public items;
    mapping(address => uint) public itemsOwned;
    function getItems(address _owner) public view returns (uint[] memory) {
        uint\[] memory result = new uint[\](itemsOwned[_owner]);
        uint counter = 0;
        for (uint i = 0; i < items.length; i++) {
            if (items[i].owner == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
}

上記のStore契約structでは、リスト内のアイテムのデータ構造を設計するために使用し、次にアイテムを所有者にマッピングしましたaddress。アドレスが所有するアイテムを取得するには、このgetItems関数を使用して、と呼ばれるメモリを集約しますresult

永遠のストレージ

このパターンは、アップグレードされたスマートコントラクトのメモリを維持します。古いコントラクトと新しいコントラクトはブロックチェーン上で別々にデプロイされるため、蓄積されたストレージは古い場所に残り、ユーザー情報、アカウントの残高、およびその他の貴重な情報への参照が保存されます。

データタイプごとに1つずつ、複数のデータストレージマッピングを実装することにより、データストレージの変更を防ぐために、永遠のストレージは可能な限り独立している必要があります。抽象化された値をsha3ハッシュのマップに変換すると、Key-Valueストアとして機能します。

提案されたソリューションは従来の値ストレージよりも洗練されているため、ラッパーは複雑さを軽減し、コードを読みやすくすることができます。永遠のストレージを使用するアップグレード可能なコントラクトでは、ラッパーを使用すると、見慣れない構文やハッシュを使用したキーの処理が簡単になります。

以下のコードスニペットは、ラッパーを使用して永遠のストレージを実装する方法を示しています。

function getBalance(address account) public view returns(uint) {
    return eternalStorageAdr.getUint(keccak256("balances", account));
}
function setBalance(address account, uint amount) internal {
    eternalStorageAdr.setUint(keccak256("balances", account), amount);
}
function addBalance(address account, uint amount) internal {
    setBalance(account, getBalance(account) + amount);
}

上記のコードスニペットでは、のハッシュ関数を使用して、同様にアカウントの残高を設定するために、account永遠のストレージからの残高を取得しました。keccak256enternalStorageAdr.getUint()

メモリとストレージ

Storage、、、またはは、動的データ型の場所を変数の形式で宣言するときに使用されるメソッドmemoryですcalldataが、ここでは、これから集中しmemoryますstorage。この用語storageは、スマートコントラクトのすべてのインスタンスで共有される状態変数をmemory指しますが、各スマートコントラクト実行インスタンスのデータの一時的な保存場所を指します。以下のコードの例を見て、これがどのように機能するかを見てみましょう。

使用例storage

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense storage cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

上記のBudgetPlan契約では、アカウントの費用のデータ構造を設計しました。各費用( )は、とExpenseを含む構造体です。次に、にnewを追加する関数を宣言しました。priceitempurchaseExpensestorage

使用例memory

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense memory cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

を使用した例とほぼ同じですが、すべて同じですが、コードスニペットでは、関数の実行時にメモリにstorage新しいものを追加します。Expensepurchase

まとめ

特定の目的を達成したり、特定の概念を実装したりするにはさまざまな方法があるため、開発者はデザインパターンに固執する必要があります。

これらのSolidityデザインパターンを実践すると、アプリケーションに大きな変化が見られます。アプリケーションは、貢献しやすく、クリーンで、より安全になります。

このトピックの理解をテストするために、次のSolidityプロジェクトでこれらのパターンの少なくとも1つを使用することをお勧めします。

このトピックに関連する質問をするか、下のコメントセクションにコメントを残してください。 

ソース:https ://blog.logrocket.com/developers-guide-solidity-design-patterns/ 

#solidity #design-pattern 

Solidityデザインパターンの使用方法
许 志强

许 志强

1657652400

如何使用 Solidity 设计模式

由于区块链和DApps (去中心化应用程序)的持续普及,开源 DApps 看到来自各种开发人员的贡献不断增长。大多数 DApp 和区块链应用程序的核心是使用 Solidity 开发的智能合约。

对开源项目的贡献引起了Solidity社区的关注,因为这些项目对人们的金钱产生了现实影响,并且当来自不同背景的开发人员在一个项目上进行协作时,几乎可以肯定应用程序中会出现错误和代码冲突。这就是为什么为 DApp 实践适当的标准如此重要的原因。

为了保持优秀的标准、消除风险、减轻冲突、构建可扩展和安全的智能合约,有必要研究和使用 Solidity 中设计模式和样式的正确实现。

本文将讨论 Solidity 设计模式;您必须熟悉 Solidity 才能继续学习。

什么是 Solidity 设计模式?

作为开发人员,你可以从网上的各种资源中学习使用 Solidity,但是这些资料并不相同,因为在 Solidity 中实现事物的方式和风格有很多不同。

设计模式是可重用的传统解决方案,用于解决重复出现的设计缺陷。从一个地址到另一个地址的转移是 Solidity 中经常关注的一个实际示例,可以通过设计模式进行调节。

在 Solidity 中传输 Ether 时,我们使用SendTransferCall方法。这三种方法具有相同的单一目标:将以太币从智能合约中发送出去。让我们看看如何为此目的使用Transfer和方法。Call以下代码示例演示了不同的实现。

首先是Transfer方法。使用这种方式时,所有接收智能合约都必须定义一个回退函数,否则转账交易将失败。可用gas限制为2300gas,足以完成转账交易,有助于防止重入攻击:

function Transfer(address payable _to) public payable {     
  _to.transfer(msg.value); 
} 

上面的代码片段定义了这个Transfer函数,它接受一个接收地址 as_to并使用该_to.transfer方法来启动指定为 的以太币的传输msg.value

接下来是Call方法。可以使用此方法触发合约中的其他功能,并且可以选择在功能执行时设置要使用的 gas 费用:

 

function Call(address payable _to) public payable {
    (bool sent) = _to.call.gas(1000){value: msg.value}("");
    require("Sent, Ether not sent");
}

上面的代码片段定义了Call函数,它接受接收地址为_to,将交易状态设置为布尔值,返回的结果在数据变量中提供。如果msg.data为空,则receive函数在Call方法之后立即执行。回退在没有实现接收功能的地方运行。

在智能合约之间转移 Ether 的最优选方式是使用该Call方法。

在上面的示例中,我们使用了两种不同的技术来传输 Ether。您可以使用 指定要消耗多少气体Call,而Transfer默认情况下具有固定数量的气体。

这些技术是在 Solidity 中实践的模式,用于实现Transfer.

为了保持上下文,以下部分是 Solidity 规定的一些设计模式。

行为模式

警卫检查

智能合约的主要功能是确保交易的要求通过。如果任何条件失败,合同将恢复到之前的状态。Solidity 通过使用 EVM 的错误处理机制来抛出异常并将合约恢复到异常之前的工作状态来实现这一点。

下面的智能合约展示了如何使用所有三种技术实现警卫检查模式:

contract Contribution {
  function contribute (address _from) payable public {
    require(msg.value != 0);
    require(_from != address(0));
    unit prevBalance = this.balance;
    unit amount;

    if(_from.balance == 0) {
      amount = msg.value;
    } else if (_from.balance < msg.sender.balance) {
      amount = msg.value / 2;
    } else {
      revert("Insufficent Balance!!!");
    }

    _from.transfer(amount);
    assert(this.balance == prevBalance - amount);
  }
}

在上面的代码片段中,Solidity 使用以下方式处理错误异常:

require()声明函数执行的条件。它接受单个条件作为参数,如果条件评估为假,则抛出异常,终止函数的执行而不燃烧任何气体。

assert()评估函数的条件,然后抛出异常,将合约恢复到以前的状态,如果执行后需求失败,则消耗 gas 供应。

revert()抛出异常,返回任何提供的气体,如果对函数的要求失败,则将函数调用恢复到合约的原始状态。该revert()方法不评估或要求任何条件。

状态机

状态机模式基于系统先前和当前的输入来模拟系统的行为。开发人员使用这种方法将大问题分解为简单的阶段和转换,然后用于表示和控制应用程序的执行流程。

状态机模式也可以在智能合约中实现,如下面的代码片段所示:

contract Safe {
    Stages public stage = Stages.AcceptingDeposits;
    uint public creationTime = now;
    mapping (address => uint) balances;

    modifier atStage(Stages _stage) {
      require(stage == _stage);
      _;
    }

    modifier timedTransitions() {
      if (stage == Stages.AcceptingDeposits && now >=
      creationTime + 1 days)
      nextStage();
      if (stage == Stages.FreezingDeposits && now >=
      creationTime + 4 days)
      nextStage();
      _;
    }
    function nextStage() internal {
      stage = Stages(uint(stage) + 1);
    }
    function deposit() public payable timedTransitions atStage(Stages.AcceptingDeposits) {
      balances[msg.sender] += msg.value;
    }
    function withdraw() public timedTransitions atStage(Stages.ReleasingDeposits) {
      uint amount = balances[msg.sender];
      balances[msg.sender] = 0;
      msg.sender.transfer(amount);
    }
}

在上面的代码片段中,Safe合约使用修饰符来更新各个阶段之间的合约状态。这些阶段决定了何时可以进行存款和取款。如果合约当前状态不是AcceptingDeposit,用户不能向合约充值,如果当前状态不是ReleasingDeposit,用户不能退出合约。

甲骨文

以太坊合约有自己的通信生态系统。系统只能通过交易(通过将数据传递给方法)导入外部数据,这是一个缺点,因为许多合约用例涉及来自区块链以外的来源(例如股票市场)的知识。

这个问题的一种解决方案是使用与外部世界连接的预言机模式。当预言机服务和智能合约异步通信时,预言机服务充当 API。交易从调用智能合约功能开始,该功能包括向预言机发送请求的指令。

根据此类请求的参数,oracle 将通过在主合约中执行回调函数来获取结果并返回。基于 Oracle 的合约与去中心化网络的区块链概念不兼容,因为它们依赖于单个组织或团体的诚实。

Oracle 服务2122通过使用提供的数据提供有效性检查来解决此缺陷。请注意,预言机必须为回调调用付费。因此,预言机费用与回调调用所需的以太币一起支付。

下面的代码片段显示了预言机合约与其消费者合约之间的交易:

contract API {
    address trustedAccount = 0x000...; //Account address
    struct Request {
        bytes data;
        function(bytes memory) external callback;
    }
    Request[] requests;
    event NewRequest(uint);

    modifier onlyowner(address account) {
        require(msg.sender == account);
        _;
    }
    function query(bytes data, function(bytes memory) external callback) public {
        requests.push(Request(data, callback));
        NewRequest(requests.length - 1);
    }
    // invoked by outside world
    function reply(uint requestID, bytes response) public
    onlyowner(trustedAccount) {
    requests[requestID].callback(response);
    }
}

在上面的代码片段中,智能合约向使用函数API发送查询请求,后者执行函数并使用函数从外部源收集响应数据。knownSourcequeryexternal callbackreply

随机性

尽管在 Solidity 中生成随机和唯一值是多么棘手,但它的需求量很大。区块时间戳是以太坊中随机性的来源,但它们存在风险,因为矿工可以篡改它们。为防止出现此问题,创建了块哈希 PRNG 和 Oracle RNG 等解决方案。

以下代码片段显示了使用最新块哈希的此模式的基本实现:

// This method is predicatable. Use with care!
function random() internal view returns (uint) {
    return uint(blockhash(block.number - 1));
}

上面的函数通过散列块号( ,它是区块链上的一个变量)randomNum()生成一个随机且唯一的整数。block.number

安全模式

访问限制

因为在 Solidity 中没有内置的方法来管理执行权限,所以一种常见的趋势是限制函数的执行。函数的执行只能在某些条件下执行,例如时间、调用者或事务信息以及其他标准。

下面是一个调节函数的例子:

contract RestrictPayment {
    uint public date_time = now;

    modifier only(address account) {
        require(msg.sender == account);
        _;
    }

    function f() payable onlyowner(date_time + 1 minutes){
      //code comes here
    }
}

上面的 Restrict 合约阻止了与执行函数的任何account不同。如果不满足函数的要求,用于在函数执行前抛出异常。msg.senderpayablepayablerequire

检查效果交互

检查效果交互模式降低了恶意合约在外部调用后试图接管控制流的风险。在以太币转移过程中,合同可能会将控制流转移给外部实体。如果外部合约是恶意的,它有可能破坏控制流并导致发送者反弹到不希望的状态。

要使用这种模式,我们必须知道函数的哪些部分易受攻击,以便在找到可能的漏洞来源后做出响应。

以下是如何使用此模式的示例:

contract CheckedTransactions {
    mapping(address => uint) balances;
    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        msg.sender.transfer(amount);
    }
}

在上面的代码片段中,如果条件失败,require()则使用该方法抛出异常。balances[msg.sender] >= amount这意味着,用户不能提取amount更大的余额msg.sender

安全的以太币转账

尽管加密货币转移不是 Solidity 的主要功能,但它们经常发生。正如我们之前所讨论的,TransferCallSend是在 Solidity 中传输 Ether 的三种基本技术。除非知道它们之间的差异,否则不可能决定使用哪种方法。

除了本文前面讨论的两种方法(Transfer和)之外,还可以使用该方法在 Solidity 中传输 Ether。CallSend

SendTransfer默认值 (2300) 的 gas 成本相同。然而,与 不同Transfer的是,它返回一个布尔结果,指示 是否Send成功。大多数 Solidity 项目不再使用该Send方法。

下面是该Send方法的一个实现:

function send(address payable _to) external payable{
    bool sent = _to.send(123);
    require(sent, "send failed");
}

上面的函数,如果send 返回的值为,则send使用该require()函数引发异常。Boolean_to.send(123)false

拉过推

这种设计模式将以太币转移的风险从合约转移给了用户。在以太币转移过程中,有几件事情可能会出错,导致交易失败。在 pull-over-push 模式中,涉及三方:发起传输的实体(合约的作者)、智能合约和接收方。

此模式包括映射,它有助于跟踪用户的未结余额。用户没有将 Ether 从合约中交付给接收者,而是调用一个函数来提取他们分配的 Ether。其中一项转账的任何不准确之处均不会影响其他交易。

下面是一个pull-over-pull的例子:

contract ProfitsWithdrawal {
    mapping(address => uint) profits;
    function allowPull(address owner, uint amount) private {
        profits[owner] += amount;
    }
    function withdrawProfits() public {
        uint amount = profits[msg.sender];
        require(amount != 0);
        require(address(this).balance >= amount);
        profits[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

在上面的合约中,如果用户的余额大于或等于分配给用户ProfitsWithdrawal的利润,则允许用户提取映射到他们的利润。address

紧急停止

经审计的智能合约可能包含在涉及网络事件之前无法检测到的错误。合约启动后发现的错误将很难修复。借助这种设计,我们可以通过阻止对关键功能的调用来停止合约,从而在智能合约得到纠正之前阻止攻击者。

应该只允许授权用户使用停止功能,以防止用户滥用它。设置一个状态变量falsetrue确定合同的终止。终止合约后,您可以使用访问限制模式来确保没有执行任何关键功能。

如果状态变量指示紧急停止的启动,则抛出异常的函数修改可以用于完成此操作,如下所示:

contract EmergencyStop {
    bool Running = true;
    address trustedAccount = 0x000...; //Account address
    modifier stillRunning {
        require(Running);
        _;
    }
    modifier NotRunning {
        require(¡Running!);
        _;
    }
    modifier onlyAuthorized(address account) {
        require(msg.sender == account);
        _;
    }
    function stopContract() public onlyAuthorized(trustedAccount) {
        Running = false;
    }
    function resumeContract() public onlyAuthorized(trustedAccount) {
        Running = true;
    }
}

上面的EmergencyStop合约使用修饰符来检查条件,如果满足这些条件中的任何一个,就会抛出异常。合约使用stopContract()resumeContract()函数来处理紧急情况。

可以通过将状态变量重置为 来恢复合约false。这种方法应该像紧急停止功能一样防止未经授权的呼叫。

可升级性模式

代理委托

这种模式允许在不破坏任何组件的情况下升级智能合约。Delegatecall使用此方法时会调用一个特定的消息。它将函数调用转发给委托而不暴露函数签名。

代理合约的回退功能使用它来启动每个函数调用的转发机制。唯一Delegatecall返回的是一个布尔值,指示执行是否成功。我们对函数调用的返回值更感兴趣。请记住,升级合约时,存储顺序不得更改;只允许添加。

这是实现此模式的示例:

contract UpgradeProxy {
    address delegate;
    address owner = msg.sender;
    function upgradeDelegate(address newDelegateAddress) public {
        require(msg.sender == owner);
        delegate = newDelegateAddress;
    }
    function() external payable {
        assembly {
            let _target := sload(0)
            calldatacopy(0x01, 0x01, calldatasize)
            let result := delegatecall(gas, _target, 0x01, calldatasize, 0x01, 0)
            returndatacopy(0x01, 0x01, returndatasize)
            switch result case 0 {revert(0, 0)} default {return (0, returndatasize)}
        }
    }
}

在上面的代码片段中,通过调用将合约数据的副本传输到新版本的回退函数,UpgradeProxy处理允许delegate合约在执行合约后升级的机制。ownerdelegate

内存阵列构建

这种方法可以快速有效地从合约存储中聚合和检索数据。与合约的内存交互是 EVM 中最昂贵的操作之一。确保删除冗余并仅存储所需数据有助于最大限度地降低成本。

我们可以使用视图函数修改从合约存储中聚合和读取数据,而不会产生进一步的费用。每次需要搜索时,它都会在内存中重新创建,而不是将数组存储在存储器中。

易于迭代的数据结构(例如数组)用于使数据检索更容易。在处理具有多个属性的数据时,我们使用自定义数据类型(例如 struct)对其进行聚合。

还需要映射来跟踪每个聚合实例的预期数据输入数量。

下面的代码说明了这种模式:

contract Store {
    struct Item {
        string name;
        uint32 price;
        address owner;
    }
    Item[] public items;
    mapping(address => uint) public itemsOwned;
    function getItems(address _owner) public view returns (uint[] memory) {
        uint\[] memory result = new uint[\](itemsOwned[_owner]);
        uint counter = 0;
        for (uint i = 0; i < items.length; i++) {
            if (items[i].owner == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
}

在上面的Store合约中,我们使用struct列表中的项目设计数据结构,然后将项目映射到其所有者的address. 为了获取某个地址所拥有的项目,我们使用该getItems函数来聚合一个名为 的内存result

永恒的存储

这种模式维护升级后的智能合约的内存。因为旧合约和新合约是分开部署在区块链上的,所以累积的存储仍保留在其旧位置,存储用户信息、账户余额以及对其他有价值信息的引用。

永久存储应尽可能独立,以防止通过实现多个数据存储映射来修改数据存储,每个数据类型一个映射。将抽象值转换为 sha3 哈希映射用作键值存储。

因为提议的解决方案比传统的值存储更复杂,所以包装器可以降低复杂性并使代码清晰易读。在使用永久存储的可升级合约中,包装器可以更轻松地处理不熟悉的语法和带有哈希的键。

下面的代码片段展示了如何使用包装器来实现永久存储:

function getBalance(address account) public view returns(uint) {
    return eternalStorageAdr.getUint(keccak256("balances", account));
}
function setBalance(address account, uint amount) internal {
    eternalStorageAdr.setUint(keccak256("balances", account), amount);
}
function addBalance(address account, uint amount) internal {
    setBalance(account, getBalance(account) + amount);
}

在上面的代码片段中,我们使用 in 中的哈希函数account从永久存储中获取余额,同样用于设置帐户余额。keccak256enternalStorageAdr.getUint()

内存与存储

Storage, memory, 或calldata是以变量的形式声明动态数据类型的位置时使用的方法,但我们现在将专注memorystorage。该术语storage是指在所有智能合约实例之间共享的状态变量,而memory指的是每个智能合约执行实例中数据的临时存储位置。让我们看一下下面的代码示例,看看它是如何工作的:

使用示例storage

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense storage cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

在上面的BudgetPlan合约中,我们为账户的费用设计了一个数据结构,其中每个费用 ( Expense) 是一个包含priceand的结构体item。然后,我们声明了purchase要向.Expensestorage

使用示例memory

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense memory cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

几乎和 using 的例子一样storage,一切都是一样的,但是在代码片段中,我们在函数执行Expense时向内存添加了一个新的。purchase

结束的想法

开发人员应该坚持设计模式,因为有不同的方法可以实现特定目标或实现某些概念。

如果您练习这些 Solidity 设计模式,您会注意到您的应用程序发生了重大变化。您的应用程序将更容易贡献、更清洁、更安全。

我建议你在下一个 Solidity 项目中至少使用其中一种模式来测试你对这个主题的理解。

随时提出与此主题相关的任何问题或在下面的评论部分发表评论。 

来源:https ://blog.logrocket.com/developers-guide-solidity-design-patterns/

#solidity #design-pattern 

如何使用 Solidity 设计模式

Cómo Usar Los Patrones De Diseño De Solidity

Debido a la popularidad cada vez mayor de blockchain y DApps (aplicaciones descentralizadas), las DApps de código abierto están experimentando un crecimiento en las contribuciones de una amplia variedad de desarrolladores. El corazón de la mayoría de las aplicaciones DApps y blockchain son los contratos inteligentes desarrollados con Solidity.

La contribución a proyectos de código abierto genera inquietudes dentro de la comunidad de Solidity porque estos proyectos tienen consecuencias en el mundo real para el dinero de las personas, y cuando los desarrolladores de diferentes orígenes colaboran en un proyecto, es casi seguro que habrá errores y conflictos de código en las aplicaciones. Esta es la razón por la cual es tan importante practicar estándares adecuados para DApps.

Para mantener excelentes estándares, eliminar riesgos, mitigar conflictos y construir contratos inteligentes escalables y seguros, es necesario estudiar y utilizar la implementación correcta de patrones y estilos de diseño en Solidity.

Este artículo discutirá el patrón de diseño Solidity; debe estar familiarizado con Solidity para seguirlo.

¿Qué es un patrón de diseño Solidity?

Como desarrollador, puede aprender a usar Solidity de varios recursos en línea, pero estos materiales no son los mismos, porque hay muchas formas y estilos diferentes de implementar cosas en Solidity.

Los patrones de diseño son soluciones convencionales reutilizables que se utilizan para resolver fallas de diseño recurrentes. Hacer una transferencia de una dirección a otra es un ejemplo práctico de preocupación frecuente en Solidity que se puede regular con patrones de diseño.

Al transferir Ether en Solidity, usamos los métodos Send, Transfero . CallEstos tres métodos tienen el mismo objetivo singular: enviar Ether fuera de un contrato inteligente. Echemos un vistazo a cómo usar los métodos Transfery Callpara este propósito. Los siguientes ejemplos de código muestran diferentes implementaciones.

Primero está el Transfermétodo. Al usar este enfoque, todos los contratos inteligentes que reciben deben definir una función de respaldo, o la transacción de transferencia fallará. Hay un límite de gasolina de 2300 gasolina disponible, que es suficiente para completar la transacción de transferencia y ayuda en la prevención de asaltos de reingreso:

function Transfer(address payable _to) public payable {     
  _to.transfer(msg.value); 
} 

El fragmento de código anterior define la Transferfunción, que acepta una dirección de recepción como _toy utiliza el _to.transfermétodo para iniciar la transferencia de Ether especificado como msg.value.

El siguiente es el Callmétodo. Se pueden activar otras funciones en el contrato usando este método y, opcionalmente, establecer una tarifa de gas para usar cuando se ejecuta la función:

 

function Call(address payable _to) public payable {
    (bool sent) = _to.call.gas(1000){value: msg.value}("");
    require("Sent, Ether not sent");
}

El fragmento de código anterior define la Callfunción, que acepta una dirección de recepción como _to, establece el estado de la transacción como booleano y el resultado devuelto se proporciona en la variable de datos. Si msg.dataestá vacío, la receivefunción se ejecuta inmediatamente después del Callmétodo. El respaldo se ejecuta donde no hay implementación de la función de recepción.

La forma preferida de transferir Ether entre contratos inteligentes es mediante el Callmétodo.

En los ejemplos anteriores, usamos dos técnicas diferentes para transferir Ether. Puedes especificar cuánto gas quieres gastar usando Call, mientras que Transfertiene una cantidad fija de gas por defecto.

Estas técnicas son patrones practicados en Solidity para implementar la ocurrencia recurrente de Transfer.

Para mantener las cosas en contexto, las siguientes secciones son algunos de los patrones de diseño que Solidity ha regulado.

Patrones de comportamiento

control de guardia

La función principal de los contratos inteligentes es garantizar que se cumplan los requisitos de las transacciones. Si alguna condición falla, el contrato vuelve a su estado anterior. Solidity logra esto empleando el mecanismo de manejo de errores de EVM para lanzar excepciones y restaurar el contrato a un estado de trabajo antes de la excepción.

El contrato inteligente a continuación muestra cómo implementar el patrón de verificación de guardia utilizando las tres técnicas:

contract Contribution {
  function contribute (address _from) payable public {
    require(msg.value != 0);
    require(_from != address(0));
    unit prevBalance = this.balance;
    unit amount;

    if(_from.balance == 0) {
      amount = msg.value;
    } else if (_from.balance < msg.sender.balance) {
      amount = msg.value / 2;
    } else {
      revert("Insufficent Balance!!!");
    }

    _from.transfer(amount);
    assert(this.balance == prevBalance - amount);
  }
}

En el fragmento de código anterior, Solidity maneja las excepciones de error usando lo siguiente:

require()declara las condiciones bajo las cuales se ejecuta una función. Acepta una sola condición como argumento y lanza una excepción si la condición se evalúa como falsa, finalizando la ejecución de la función sin quemar nada.

assert()evalúa las condiciones para una función, luego lanza una excepción, revierte el contrato al estado anterior y consume el suministro de gas si los requisitos fallan después de la ejecución.

revert()genera una excepción, devuelve el gas suministrado y revierte la llamada de función al estado original del contrato si falla el requisito de la función. El revert()método no evalúa ni requiere ninguna condición.

Máquina estatal

El patrón de máquina de estado simula el comportamiento de un sistema en función de sus entradas anteriores y actuales. Los desarrolladores utilizan este enfoque para dividir grandes problemas en etapas y transiciones simples, que luego se utilizan para representar y controlar el flujo de ejecución de una aplicación.

El patrón de máquina de estado también se puede implementar en contratos inteligentes, como se muestra en el fragmento de código a continuación:

contract Safe {
    Stages public stage = Stages.AcceptingDeposits;
    uint public creationTime = now;
    mapping (address => uint) balances;

    modifier atStage(Stages _stage) {
      require(stage == _stage);
      _;
    }

    modifier timedTransitions() {
      if (stage == Stages.AcceptingDeposits && now >=
      creationTime + 1 days)
      nextStage();
      if (stage == Stages.FreezingDeposits && now >=
      creationTime + 4 days)
      nextStage();
      _;
    }
    function nextStage() internal {
      stage = Stages(uint(stage) + 1);
    }
    function deposit() public payable timedTransitions atStage(Stages.AcceptingDeposits) {
      balances[msg.sender] += msg.value;
    }
    function withdraw() public timedTransitions atStage(Stages.ReleasingDeposits) {
      uint amount = balances[msg.sender];
      balances[msg.sender] = 0;
      msg.sender.transfer(amount);
    }
}

En el fragmento de código anterior, el Safecontrato usa modificadores para actualizar el estado del contrato entre varias etapas. Las etapas determinan cuándo se pueden realizar depósitos y retiros. Si el estado actual del contrato no es AcceptingDeposit, los usuarios no pueden depositar en el contrato, y si el estado actual no es ReleasingDeposit, los usuarios no pueden rescindir el contrato.

Oráculo

Los contratos de Ethereum tienen su propio ecosistema donde se comunican. El sistema solo puede importar datos externos a través de una transacción (pasando datos a un método), lo cual es un inconveniente porque muchos casos de uso de contratos involucran conocimiento de fuentes distintas a la cadena de bloques (por ejemplo, el mercado de valores).

Una solución a este problema es usar el patrón del oráculo con una conexión con el mundo exterior. Cuando un servicio de Oracle y un contrato inteligente se comunican de forma asíncrona, el servicio de Oracle actúa como una API. Una transacción comienza invocando una función de contrato inteligente, que comprende una instrucción para enviar una solicitud a un oráculo.

Según los parámetros de dicha solicitud, el oráculo obtendrá un resultado y lo devolverá ejecutando una función de devolución de llamada en el contrato principal. Los contratos basados ​​en Oracle son incompatibles con el concepto de cadena de bloques de una red descentralizada, porque se basan en la honestidad de una sola organización o grupo.

Los servicios de Oracle 21 y 22 abordan esta falla al proporcionar una verificación de validez con los datos suministrados. Tenga en cuenta que un oráculo debe pagar por la invocación de devolución de llamada. Por lo tanto, se paga un cargo de Oracle junto con el Ether requerido para la invocación de devolución de llamada.

El fragmento de código a continuación muestra la transacción entre un contrato de Oracle y su contrato de consumidor:

contract API {
    address trustedAccount = 0x000...; //Account address
    struct Request {
        bytes data;
        function(bytes memory) external callback;
    }
    Request[] requests;
    event NewRequest(uint);

    modifier onlyowner(address account) {
        require(msg.sender == account);
        _;
    }
    function query(bytes data, function(bytes memory) external callback) public {
        requests.push(Request(data, callback));
        NewRequest(requests.length - 1);
    }
    // invoked by outside world
    function reply(uint requestID, bytes response) public
    onlyowner(trustedAccount) {
    requests[requestID].callback(response);
    }
}

En el fragmento de código anterior, el APIcontrato inteligente envía una solicitud de consulta a un knownSourceusuario de la queryfunción, que ejecuta la external callbackfunción y la usa replypara recopilar datos de respuesta de la fuente externa.

Aleatoriedad

A pesar de lo complicado que es generar valores aleatorios y únicos en Solidity, tiene una gran demanda. Las marcas de tiempo de bloque son una fuente de aleatoriedad en Ethereum, pero son riesgosas porque el minero puede manipularlas. Para evitar este problema, se crearon soluciones como block-hash PRNG y Oracle RNG.

El siguiente fragmento de código muestra una implementación básica de este patrón utilizando el hash de bloque más reciente:

// This method is predicatable. Use with care!
function random() internal view returns (uint) {
    return uint(blockhash(block.number - 1));
}

La randomNum()función anterior genera un número entero aleatorio y único al codificar el número de bloque ( block.number, que es una variable en la cadena de bloques).

Patrones de seguridad

Restricción de acceso

Debido a que no existen medios integrados para administrar los privilegios de ejecución en Solidity, una tendencia común es limitar la ejecución de funciones. La ejecución de las funciones solo debe realizarse en determinadas condiciones, como el tiempo, la información de la persona que llama o de la transacción, y otros criterios.

Aquí hay un ejemplo de condicionamiento de una función:

contract RestrictPayment {
    uint public date_time = now;

    modifier only(address account) {
        require(msg.sender == account);
        _;
    }

    function f() payable onlyowner(date_time + 1 minutes){
      //code comes here
    }
}

El contrato de restricción anterior impide que cualquier accountotro msg.senderejecute la payablefunción. Si no se cumplen los requisitos para la payablefunción, requirese utiliza para lanzar una excepción antes de que se ejecute la función.

Comprobar interacciones de efectos

El patrón de interacción de los efectos de verificación disminuye el riesgo de que los contratos maliciosos intenten hacerse cargo del flujo de control después de una llamada externa. Es probable que el contrato transfiera el flujo de control a una entidad externa durante el procedimiento de transferencia de Ether. Si el contrato externo es malicioso, tiene el potencial de interrumpir el flujo de control y hacer que el remitente rebote a un estado no deseado.

Para usar este patrón, debemos ser conscientes de qué partes de nuestra función son vulnerables para que podamos responder una vez que encontremos la posible fuente de vulnerabilidad.

El siguiente es un ejemplo de cómo usar este patrón:

contract CheckedTransactions {
    mapping(address => uint) balances;
    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        msg.sender.transfer(amount);
    }
}

En el fragmento de código anterior, el require()método se usa para lanzar una excepción si la condición balances[msg.sender] >= amountfalla. Esto significa que un usuario no puede retirar amountmás el saldo del msg.sender.

Transferencia segura de éter

Aunque las transferencias de criptomonedas no son la función principal de Solidity, ocurren con frecuencia. Como discutimos anteriormente, Transfer, Call, y Sendson las tres técnicas fundamentales para transferir Ether en Solidity. Es imposible decidir qué método usar a menos que uno sea consciente de sus diferencias.

Además de los dos métodos ( Transfery Call) discutidos anteriormente en este artículo, la transmisión de Ether en Solidity se puede realizar utilizando el Sendmétodo.

Sendes similar a Transferque cuesta la misma cantidad de gas que el predeterminado (2300). Sin embargo, a diferencia Transferde , devuelve un resultado booleano que indica si Sendtuvo éxito o no. La mayoría de los proyectos de Solidity ya no utilizan el Sendmétodo.

A continuación se muestra una implementación del Sendmétodo:

function send(address payable _to) external payable{
    bool sent = _to.send(123);
    require(sent, "send failed");
}

La sendfunción anterior utiliza la require()función para generar una excepción si el Booleanvalor de enviado devuelto _to.send(123)es false.

Tirar sobre empujar

Este patrón de diseño traslada el riesgo de transferencia de Ether del contrato a los usuarios. Durante la transferencia de Ether, varias cosas pueden salir mal y hacer que la transacción falle. En el patrón pull-over-push, participan tres partes: la entidad que inicia la transferencia (el autor del contrato), el contrato inteligente y el receptor.

Este patrón incluye mapeo, que ayuda en el seguimiento de los saldos pendientes de los usuarios. En lugar de entregar Ether del contrato a un destinatario, el usuario invoca una función para retirar su Ether asignado. Cualquier inexactitud en una de las transferencias no tiene impacto en las demás transacciones.

El siguiente es un ejemplo de pull-over-pull:

contract ProfitsWithdrawal {
    mapping(address => uint) profits;
    function allowPull(address owner, uint amount) private {
        profits[owner] += amount;
    }
    function withdrawProfits() public {
        uint amount = profits[msg.sender];
        require(amount != 0);
        require(address(this).balance >= amount);
        profits[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

En el ProfitsWithdrawalcontrato anterior, permite a los usuarios retirar las ganancias asignadas a su cuenta addresssi el saldo del usuario es mayor o igual a las ganancias asignadas al usuario.

Parada de emergencia

Los contratos inteligentes auditados pueden contener errores que no se detectan hasta que están involucrados en un incidente cibernético. Los errores descubiertos después del lanzamiento del contrato serán difíciles de corregir. Con la ayuda de este diseño, podemos detener un contrato bloqueando las llamadas a funciones críticas, previniendo a los atacantes hasta la rectificación del contrato inteligente.

Solo los usuarios autorizados deben poder usar la función de detención para evitar que los usuarios abusen de ella. Se establece una variable de estado de falsea truepara determinar la terminación del contrato. Después de rescindir el contrato, puede usar el patrón de restricción de acceso para asegurarse de que no se ejecute ninguna función crítica.

Para lograr esto, se usa una modificación de función que arroja una excepción si la variable de estado indica el inicio de una parada de emergencia, como se muestra a continuación:

contract EmergencyStop {
    bool Running = true;
    address trustedAccount = 0x000...; //Account address
    modifier stillRunning {
        require(Running);
        _;
    }
    modifier NotRunning {
        require(¡Running!);
        _;
    }
    modifier onlyAuthorized(address account) {
        require(msg.sender == account);
        _;
    }
    function stopContract() public onlyAuthorized(trustedAccount) {
        Running = false;
    }
    function resumeContract() public onlyAuthorized(trustedAccount) {
        Running = true;
    }
}

El EmergencyStopcontrato anterior utiliza modificadores para verificar las condiciones y lanzar excepciones si se cumple alguna de estas condiciones. El contrato utiliza las funciones stopContract()y resumeContract()para manejar situaciones de emergencia.

El contrato se puede reanudar restableciendo la variable de estado a false. Este método debe protegerse contra llamadas no autorizadas de la misma manera que la función de parada de emergencia.

Patrones de capacidad de actualización

delegado apoderado

Este patrón permite actualizar contratos inteligentes sin romper ninguno de sus componentes. Se emplea un mensaje particular llamado Delegatecallcuando se utiliza este método. Reenvía la llamada de función al delegado sin exponer la firma de la función.

La función de reserva del contrato de proxy lo utiliza para iniciar el mecanismo de reenvío para cada llamada de función. Lo único que Delegatecalldevuelve es un valor booleano que indica si la ejecución fue exitosa o no. Estamos más interesados ​​en el valor de retorno de la llamada a la función. Tenga en cuenta que, al actualizar un contrato, la secuencia de almacenamiento no debe cambiar; sólo se permiten adiciones.

He aquí un ejemplo de la implementación de este patrón:

contract UpgradeProxy {
    address delegate;
    address owner = msg.sender;
    function upgradeDelegate(address newDelegateAddress) public {
        require(msg.sender == owner);
        delegate = newDelegateAddress;
    }
    function() external payable {
        assembly {
            let _target := sload(0)
            calldatacopy(0x01, 0x01, calldatasize)
            let result := delegatecall(gas, _target, 0x01, calldatasize, 0x01, 0)
            returndatacopy(0x01, 0x01, returndatasize)
            switch result case 0 {revert(0, 0)} default {return (0, returndatasize)}
        }
    }
}

En el fragmento de código anterior, UpgradeProxymaneja un mecanismo que permite delegateactualizar el contrato una vez que se ownerejecuta llamando a la función alternativa que transfiere una copia de los delegatedatos del contrato a la nueva versión.

Construcción de matriz de memoria

Este método agrega y recupera datos del almacenamiento de contratos de forma rápida y eficiente. Interactuar con la memoria de un contrato es una de las acciones más caras de la EVM. Garantizar la eliminación de redundancias y el almacenamiento de solo los datos necesarios puede ayudar a minimizar los costos.

Podemos agregar y leer datos del almacenamiento del contrato sin incurrir en gastos adicionales utilizando la modificación de la función de vista. En lugar de almacenar una matriz en el almacenamiento, se vuelve a crear en la memoria cada vez que se requiere una búsqueda.

Se utiliza una estructura de datos que se puede iterar fácilmente, como una matriz, para facilitar la recuperación de datos. Cuando manejamos datos que tienen varios atributos, los agregamos usando un tipo de datos personalizado como struct.

También se requiere el mapeo para realizar un seguimiento de la cantidad esperada de entradas de datos para cada instancia agregada.

El siguiente código ilustra este patrón:

contract Store {
    struct Item {
        string name;
        uint32 price;
        address owner;
    }
    Item[] public items;
    mapping(address => uint) public itemsOwned;
    function getItems(address _owner) public view returns (uint[] memory) {
        uint\[] memory result = new uint[\](itemsOwned[_owner]);
        uint counter = 0;
        for (uint i = 0; i < items.length; i++) {
            if (items[i].owner == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
}

En el Storecontrato anterior, usamos structpara diseñar una estructura de datos de elementos en una lista, luego asignamos los elementos a sus propietarios address. Para obtener los elementos que pertenecen a una dirección, usamos la getItemsfunción para agregar una memoria llamada result.

Almacenamiento eterno

Este patrón mantiene la memoria de un contrato inteligente actualizado. Debido a que el contrato anterior y el contrato nuevo se implementan por separado en la cadena de bloques, el almacenamiento acumulado permanece en su ubicación anterior, donde se almacena la información del usuario, los saldos de las cuentas y las referencias a otra información valiosa.

El almacenamiento eterno debe ser lo más independiente posible para evitar modificaciones en el almacenamiento de datos mediante la implementación de múltiples asignaciones de almacenamiento de datos, una para cada tipo de datos. Convertir el valor abstraído en un mapa de hash sha3 sirve como un almacén de clave-valor.

Debido a que la solución propuesta es más sofisticada que el almacenamiento de valor convencional, los contenedores pueden reducir la complejidad y hacer que el código sea legible. En un contrato actualizable que usa almacenamiento eterno, los contenedores facilitan el manejo de sintaxis desconocida y claves con hash.

Los fragmentos de código a continuación muestran cómo usar contenedores para implementar el almacenamiento eterno:

function getBalance(address account) public view returns(uint) {
    return eternalStorageAdr.getUint(keccak256("balances", account));
}
function setBalance(address account, uint amount) internal {
    eternalStorageAdr.setUint(keccak256("balances", account), amount);
}
function addBalance(address account, uint amount) internal {
    setBalance(account, getBalance(account) + amount);
}

En el fragmento de código anterior, obtuvimos el saldo de un accountalmacenamiento eterno usando la keccak256función hash en enternalStorageAdr.getUint(), y también para establecer el saldo de la cuenta.

Memoria frente a almacenamiento

Storage, memoryo calldatason los métodos que se usan cuando se declara la ubicación de un tipo de datos dinámicos en forma de variable, pero por ahora nos concentraremos en memoryy storage. El término storagese refiere a una variable de estado compartida en todas las instancias de contrato inteligente, mientras que memoryse refiere a una ubicación de almacenamiento temporal de datos en cada instancia de ejecución de contrato inteligente. Veamos un ejemplo de código a continuación para ver cómo funciona esto:

Ejemplo usando storage:

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense storage cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

En el BudgetPlancontrato anterior, diseñamos una estructura de datos para los gastos de una cuenta donde cada gasto ( Expense) es una estructura que contiene pricey item. Luego declaramos la purchasefunción para agregar un nuevo Expensea storage.

Ejemplo usando memory:

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense memory cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

Casi como en el ejemplo usando storage, todo es igual, pero en el fragmento de código agregamos un nuevo Expensea la memoria cuando purchasese ejecuta la función.

Pensamientos finales

Los desarrolladores deben apegarse a los patrones de diseño porque existen diferentes métodos para lograr objetivos específicos o implementar ciertos conceptos.

Notará un cambio sustancial en sus aplicaciones si practica estos patrones de diseño de Solidity. Su aplicación será más fácil de contribuir, más limpia y más segura.

Le recomiendo que use al menos uno de estos patrones en su próximo proyecto de Solidity para probar su comprensión de este tema.

No dude en hacer cualquier pregunta relacionada con este tema o dejar un comentario en la sección de comentarios a continuación. 

Fuente: https://blog.logrocket.com/developers-guide-solidity-design-patterns/  

#solidity #design-pattern 

Cómo Usar Los Patrones De Diseño De Solidity

Como Usar Os Padrões De Projeto Do Solidity

Devido à crescente popularidade do blockchain e DApps (aplicativos descentralizados), os DApps de código aberto estão vendo um crescimento nas contribuições de uma ampla variedade de desenvolvedores. O coração da maioria dos DApps e aplicativos blockchain são contratos inteligentes desenvolvidos usando o Solidity.

A contribuição para projetos de código aberto gera preocupações na comunidade Solidity porque esses projetos têm consequências reais para o dinheiro das pessoas e, quando desenvolvedores de diferentes origens colaboram em um projeto, é quase certo que haverá erros e conflitos de código nos aplicativos. É por isso que praticar padrões adequados para DApps é tão crítico.

Para manter padrões excelentes, eliminar riscos, mitigar conflitos e construir contratos inteligentes escaláveis ​​e seguros, é necessário estudar e utilizar a implementação correta de padrões e estilos de projeto no Solidity.

Este artigo discutirá o padrão de projeto Solidity; você deve estar familiarizado com o Solidity para acompanhar.

O que é um padrão de projeto Solidity?

Como desenvolvedor, você pode aprender a usar o Solidity a partir de vários recursos online, mas esses materiais não são os mesmos, porque existem muitas maneiras e estilos diferentes de implementar coisas no Solidity.

Padrões de projeto são soluções convencionais reutilizáveis ​​usadas para resolver falhas recorrentes de projeto. Fazer uma transferência de um endereço para outro é um exemplo prático de preocupação frequente no Solidity que pode ser regulado com padrões de projeto.

Ao transferir Ether no Solidity, usamos os métodos Send, Transfer, ou . CallEsses três métodos têm o mesmo objetivo singular: enviar Ether para fora de um contrato inteligente. Vamos dar uma olhada em como usar os métodos Transfere para essa finalidade. CallOs exemplos de código a seguir demonstram diferentes implementações.

Primeiro é o Transfermétodo. Ao usar essa abordagem, todos os contratos inteligentes de recebimento devem definir uma função de fallback ou a transação de transferência falhará. Há um limite de gás de 2300 gás disponível, o que é suficiente para completar a transação de transferência e auxilia na prevenção de assaltos de reentrada:

function Transfer(address payable _to) public payable {     
  _to.transfer(msg.value); 
} 

O trecho de código acima define a Transferfunção, que aceita um endereço de recebimento como _toe usa o _to.transfermétodo para iniciar a transferência do Ether especificado como msg.value.

Em seguida é o Callmétodo. Outras funções no contrato podem ser acionadas usando este método e, opcionalmente, definir uma taxa de gás para usar quando a função for executada:

 

function Call(address payable _to) public payable {
    (bool sent) = _to.call.gas(1000){value: msg.value}("");
    require("Sent, Ether not sent");
}

O trecho de código acima define a Callfunção, que aceita um endereço de recebimento como _to, define o status da transação como booleano e o resultado retornado é fornecido na variável data. Se msg.dataestiver vazio, a receivefunção é executada imediatamente após o Callmétodo. O fallback é executado onde não há implementação da função de recebimento.

A maneira mais preferida de transferir Ether entre contratos inteligentes é usando o Callmétodo.

Nos exemplos acima, usamos duas técnicas diferentes para transferir o Ether. Você pode especificar a quantidade de gás que deseja gastar usando Call, enquanto Transferpor padrão tem uma quantidade fixa de gás.

Essas técnicas são padrões praticados no Solidity para implementar a ocorrência recorrente de Transfer.

Para manter as coisas em contexto, as seções a seguir são alguns dos padrões de projeto que o Solidity regulou.

Padrões comportamentais

Verificação de guarda

A principal função dos contratos inteligentes é garantir que os requisitos das transações sejam aprovados. Se alguma condição falhar, o contrato volta ao estado anterior. O Solidity consegue isso empregando o mecanismo de tratamento de erros do EVM para lançar exceções e restaurar o contrato para um estado de funcionamento antes da exceção.

O contrato inteligente abaixo mostra como implementar o padrão de verificação de guarda usando todas as três técnicas:

contract Contribution {
  function contribute (address _from) payable public {
    require(msg.value != 0);
    require(_from != address(0));
    unit prevBalance = this.balance;
    unit amount;

    if(_from.balance == 0) {
      amount = msg.value;
    } else if (_from.balance < msg.sender.balance) {
      amount = msg.value / 2;
    } else {
      revert("Insufficent Balance!!!");
    }

    _from.transfer(amount);
    assert(this.balance == prevBalance - amount);
  }
}

No snippet de código acima, o Solidity trata as exceções de erro usando o seguinte:

require()declara as condições sob as quais uma função é executada. Ele aceita uma única condição como argumento e lança uma exceção se a condição for avaliada como falsa, encerrando a execução da função sem queimar nenhum gás.

assert()avalia as condições para uma função, então lança uma exceção, reverte o contrato para o estado anterior e consome o suprimento de gás se os requisitos falharem após a execução.

revert()lança uma exceção, retorna qualquer gás fornecido e reverte a chamada da função para o estado original do contrato se o requisito para a função falhar. O revert()método não avalia ou requer quaisquer condições.

Máquina de estado

O padrão de máquina de estado simula o comportamento de um sistema com base em suas entradas anteriores e atuais. Os desenvolvedores usam essa abordagem para dividir grandes problemas em estágios e transições simples, que são usados ​​para representar e controlar o fluxo de execução de um aplicativo.

O padrão de máquina de estado também pode ser implementado em contratos inteligentes, conforme mostrado no trecho de código abaixo:

contract Safe {
    Stages public stage = Stages.AcceptingDeposits;
    uint public creationTime = now;
    mapping (address => uint) balances;

    modifier atStage(Stages _stage) {
      require(stage == _stage);
      _;
    }

    modifier timedTransitions() {
      if (stage == Stages.AcceptingDeposits && now >=
      creationTime + 1 days)
      nextStage();
      if (stage == Stages.FreezingDeposits && now >=
      creationTime + 4 days)
      nextStage();
      _;
    }
    function nextStage() internal {
      stage = Stages(uint(stage) + 1);
    }
    function deposit() public payable timedTransitions atStage(Stages.AcceptingDeposits) {
      balances[msg.sender] += msg.value;
    }
    function withdraw() public timedTransitions atStage(Stages.ReleasingDeposits) {
      uint amount = balances[msg.sender];
      balances[msg.sender] = 0;
      msg.sender.transfer(amount);
    }
}

No trecho de código acima, o Safecontrato usa modificadores para atualizar o estado do contrato entre vários estágios. Os estágios determinam quando depósitos e saques podem ser feitos. Se o estado atual do contrato não for AcceptingDeposit, os usuários não poderão depositar no contrato e, se o estado atual não for ReleasingDeposit, os usuários não poderão rescindir o contrato.

Oráculo

Os contratos Ethereum têm seu próprio ecossistema onde se comunicam. O sistema só pode importar dados externos por meio de uma transação (passando dados para um método), o que é uma desvantagem porque muitos casos de uso de contrato envolvem conhecimento de outras fontes que não o blockchain (por exemplo, o mercado de ações).

Uma solução para este problema é usar o padrão oráculo com uma conexão com o mundo exterior. Quando um serviço oracle e um contrato inteligente se comunicam de forma assíncrona, o serviço oracle funciona como uma API. Uma transação começa invocando uma função de contrato inteligente, que compreende uma instrução para enviar uma solicitação a um oráculo.

Com base nos parâmetros de tal solicitação, o oráculo buscará um resultado e o retornará executando uma função de retorno de chamada no contrato primário. Os contratos baseados em Oracle são incompatíveis com o conceito blockchain de uma rede descentralizada, porque dependem da honestidade de uma única organização ou grupo.

Os serviços Oracle 21 e 22 resolvem essa falha fornecendo uma verificação de validade com os dados fornecidos. Observe que um oráculo deve pagar pela invocação de retorno de chamada. Portanto, uma cobrança oracle é paga juntamente com o Ether necessário para a invocação de retorno de chamada.

O snippet de código abaixo mostra a transação entre um contrato oracle e seu contrato de consumidor:

contract API {
    address trustedAccount = 0x000...; //Account address
    struct Request {
        bytes data;
        function(bytes memory) external callback;
    }
    Request[] requests;
    event NewRequest(uint);

    modifier onlyowner(address account) {
        require(msg.sender == account);
        _;
    }
    function query(bytes data, function(bytes memory) external callback) public {
        requests.push(Request(data, callback));
        NewRequest(requests.length - 1);
    }
    // invoked by outside world
    function reply(uint requestID, bytes response) public
    onlyowner(trustedAccount) {
    requests[requestID].callback(response);
    }
}

No trecho de código acima, o APIcontrato inteligente envia uma solicitação de consulta para um knownSourceusando a queryfunção, que executa a external callbackfunção e usa a replyfunção para coletar dados de resposta da fonte externa.

Aleatoriedade

Apesar de ser complicado gerar valores aleatórios e únicos no Solidity, ele está em alta demanda. Os timestamps de bloco são uma fonte de aleatoriedade no Ethereum, mas são arriscados porque o minerador pode adulterá-los. Para evitar esse problema, soluções como PRNG de hash de bloco e Oracle RNG foram criadas.

O trecho de código a seguir mostra uma implementação básica desse padrão usando o hash de bloco mais recente:

// This method is predicatable. Use with care!
function random() internal view returns (uint) {
    return uint(blockhash(block.number - 1));
}

A randomNum()função acima gera um inteiro aleatório e único por meio do hash do número do bloco ( block.number, que é uma variável no blockchain).

Padrões de segurança

restrição de acesso

Como não há meios integrados para gerenciar privilégios de execução no Solidity, uma tendência comum é limitar a execução de funções. A execução de funções deve ocorrer apenas em determinadas condições, como tempo, informações do chamador ou da transação e outros critérios.

Aqui está um exemplo de condicionamento de uma função:

contract RestrictPayment {
    uint public date_time = now;

    modifier only(address account) {
        require(msg.sender == account);
        _;
    }

    function f() payable onlyowner(date_time + 1 minutes){
      //code comes here
    }
}

O contrato Restrict acima impede que qualquer accountdiferente do msg.senderexecute a payablefunção. Se os requisitos da payablefunção não forem atendidos, requireé usado para lançar uma exceção antes que a função seja executada.

Verifique as interações de efeitos

O padrão de interação de efeitos de verificação diminui o risco de contratos maliciosos tentarem assumir o controle do fluxo após uma chamada externa. O contrato provavelmente está transferindo o fluxo de controle para uma entidade externa durante o procedimento de transferência de Ether. Se o contrato externo for mal-intencionado, ele poderá interromper o fluxo de controle e fazer com que o remetente retorne a um estado indesejável.

Para usar esse padrão, devemos estar cientes de quais partes de nossa função são vulneráveis ​​para que possamos responder assim que encontrarmos a possível fonte de vulnerabilidade.

Veja a seguir um exemplo de como usar esse padrão:

contract CheckedTransactions {
    mapping(address => uint) balances;
    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        msg.sender.transfer(amount);
    }
}

No trecho de código acima, o require()método é usado para lançar uma exceção se a condição balances[msg.sender] >= amountfalhar. Isso significa que um usuário não pode sacar um amountsaldo maior do msg.sender.

Transferência segura de éter

Embora as transferências de criptomoedas não sejam a função principal do Solidity, elas acontecem com frequência. Como discutimos anteriormente, Transfer, Call, e Sendsão as três técnicas fundamentais para transferir Ether em Solidity. É impossível decidir qual método usar, a menos que se esteja ciente de suas diferenças.

Além dos dois métodos ( Transfere Call) discutidos anteriormente neste artigo, a transmissão de Ether em Solidity pode ser feita usando o Sendmétodo.

Sendé semelhante a Transferque custa a mesma quantidade de gás que o padrão (2300). Ao contrário Transferde , no entanto, ele retorna um resultado booleano indicando se Sendfoi bem-sucedido ou não. A maioria dos projetos do Solidity não usa mais o Sendmétodo.

Abaixo está uma implementação do Sendmétodo:

function send(address payable _to) external payable{
    bool sent = _to.send(123);
    require(sent, "send failed");
}

A sendfunção acima usa a require()função para lançar uma exceção se o Booleanvalor de enviado retornado de _to.send(123)for false.

Puxar sobre empurrar

Esse padrão de design transfere o risco de transferência de Ether do contrato para os usuários. Durante a transferência de Ether, várias coisas podem dar errado, fazendo com que a transação falhe. No padrão pull-over-push, três partes estão envolvidas: a entidade que inicia a transferência (o autor do contrato), o contrato inteligente e o receptor.

Esse padrão inclui mapeamento, que auxilia no acompanhamento dos saldos devedores dos usuários. Em vez de entregar o Ether do contrato a um destinatário, o usuário invoca uma função para retirar o Ether atribuído. Qualquer imprecisão em uma das transferências não tem impacto nas outras transações.

O seguinte é um exemplo de pull-over-pull:

contract ProfitsWithdrawal {
    mapping(address => uint) profits;
    function allowPull(address owner, uint amount) private {
        profits[owner] += amount;
    }
    function withdrawProfits() public {
        uint amount = profits[msg.sender];
        require(amount != 0);
        require(address(this).balance >= amount);
        profits[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

No ProfitsWithdrawalcontrato acima, permite aos usuários sacar os lucros mapeados para o seu addresscaso o saldo do usuário seja maior ou igual aos lucros atribuídos ao usuário.

Parada de emergência

Contratos inteligentes auditados podem conter bugs que não são detectados até que estejam envolvidos em um incidente cibernético. Erros descobertos após o lançamento do contrato serão difíceis de corrigir. Com a ajuda desse design, podemos interromper um contrato bloqueando chamadas para funções críticas, impedindo invasores até a retificação do contrato inteligente.

Somente usuários autorizados devem ter permissão para usar a funcionalidade de interrupção para evitar que os usuários abusem dela. Uma variável de estado é definida de falsea truepara determinar a rescisão do contrato. Após rescindir o contrato, você pode usar o padrão de restrição de acesso para garantir que não haja execução de nenhuma função crítica.

Uma modificação de função que lança uma exceção se a variável de estado indica o início de uma parada de emergência pode ser usada para fazer isso, conforme mostrado abaixo:

contract EmergencyStop {
    bool Running = true;
    address trustedAccount = 0x000...; //Account address
    modifier stillRunning {
        require(Running);
        _;
    }
    modifier NotRunning {
        require(¡Running!);
        _;
    }
    modifier onlyAuthorized(address account) {
        require(msg.sender == account);
        _;
    }
    function stopContract() public onlyAuthorized(trustedAccount) {
        Running = false;
    }
    function resumeContract() public onlyAuthorized(trustedAccount) {
        Running = true;
    }
}

O EmergencyStopcontrato acima faz uso de modificadores para verificar as condições e lançar exceções se alguma dessas condições for atendida. O contrato usa as funções stopContract()e resumeContract()para lidar com situações de emergência.

O contrato pode ser retomado redefinindo a variável de estado para false. Este método deve ser protegido contra chamadas não autorizadas da mesma forma que a função de parada de emergência.

Padrões de capacidade de atualização

Delegado por procuração

Esse padrão permite atualizar contratos inteligentes sem quebrar nenhum de seus componentes. Uma mensagem específica chamada Delegatecallé empregada ao usar esse método. Ele encaminha a chamada de função para o delegado sem expor a assinatura da função.

A função de fallback do contrato de proxy o utiliza para iniciar o mecanismo de encaminhamento para cada chamada de função. A única coisa que Delegatecallretorna é um valor booleano que indica se a execução foi bem-sucedida ou não. Estamos mais interessados ​​no valor de retorno da chamada de função. Lembre-se de que, ao atualizar um contrato, a sequência de armazenamento não deve ser alterada; apenas adições são permitidas.

Aqui está um exemplo de implementação desse padrão:

contract UpgradeProxy {
    address delegate;
    address owner = msg.sender;
    function upgradeDelegate(address newDelegateAddress) public {
        require(msg.sender == owner);
        delegate = newDelegateAddress;
    }
    function() external payable {
        assembly {
            let _target := sload(0)
            calldatacopy(0x01, 0x01, calldatasize)
            let result := delegatecall(gas, _target, 0x01, calldatasize, 0x01, 0)
            returndatacopy(0x01, 0x01, returndatasize)
            switch result case 0 {revert(0, 0)} default {return (0, returndatasize)}
        }
    }
}

No trecho de código acima, UpgradeProxytrata de um mecanismo que permite que o delegatecontrato seja atualizado após a ownerexecução do contrato, chamando a função de fallback que transfere uma cópia dos delegatedados do contrato para a nova versão.

Construção de array de memória

Esse método agrega e recupera de forma rápida e eficiente os dados do armazenamento do contrato. Interagir com a memória de um contrato é uma das ações mais caras do EVM. Garantir a remoção de redundâncias e o armazenamento apenas dos dados necessários pode ajudar a minimizar os custos.

Podemos agregar e ler dados do armazenamento do contrato sem incorrer em despesas adicionais usando a modificação da função de visualização. Em vez de armazenar uma matriz no armazenamento, ela é recriada na memória sempre que uma pesquisa é necessária.

Uma estrutura de dados que é facilmente iterável, como uma matriz, é usada para facilitar a recuperação de dados. Ao manipular dados com vários atributos, nós os agregamos usando um tipo de dados personalizado, como struct.

O mapeamento também é necessário para acompanhar o número esperado de entradas de dados para cada instância agregada.

O código abaixo ilustra esse padrão:

contract Store {
    struct Item {
        string name;
        uint32 price;
        address owner;
    }
    Item[] public items;
    mapping(address => uint) public itemsOwned;
    function getItems(address _owner) public view returns (uint[] memory) {
        uint\[] memory result = new uint[\](itemsOwned[_owner]);
        uint counter = 0;
        for (uint i = 0; i < items.length; i++) {
            if (items[i].owner == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
}

No Storecontrato acima, usamos structpara projetar uma estrutura de dados de itens em uma lista, então mapeamos os itens para seus proprietários address. Para obter os itens pertencentes a um endereço, usamos a getItemsfunção para agregar uma memória chamada result.

Armazenamento eterno

Esse padrão mantém a memória de um contrato inteligente atualizado. Como o contrato antigo e o novo contrato são implantados separadamente no blockchain, o armazenamento acumulado permanece em seu local antigo, onde são armazenadas informações do usuário, saldos de contas e referências a outras informações valiosas.

O armazenamento eterno deve ser o mais independente possível para evitar modificações no armazenamento de dados, implementando vários mapeamentos de armazenamento de dados, um para cada tipo de dados. Converter o valor abstraído em um mapa de hash sha3 serve como um armazenamento de valor-chave.

Como a solução proposta é mais sofisticada do que o armazenamento de valor convencional, os wrappers podem reduzir a complexidade e tornar o código legível. Em um contrato atualizável que usa armazenamento eterno, os wrappers tornam mais fácil lidar com sintaxes desconhecidas e chaves com hashes.

Os trechos de código abaixo mostram como usar wrappers para implementar armazenamento eterno:

function getBalance(address account) public view returns(uint) {
    return eternalStorageAdr.getUint(keccak256("balances", account));
}
function setBalance(address account, uint amount) internal {
    eternalStorageAdr.setUint(keccak256("balances", account), amount);
}
function addBalance(address account, uint amount) internal {
    setBalance(account, getBalance(account) + amount);
}

No trecho de código acima, obtivemos o saldo de um accountarmazenamento eterno usando a keccak256função hash em enternalStorageAdr.getUint(), e da mesma forma para definir o saldo da conta.

Memória vs. armazenamento

Storage, memory, ou calldatasão os métodos usados ​​ao declarar a localização de um tipo de dados dinâmico na forma de uma variável, mas vamos nos concentrar em memorye storagepor enquanto. O termo storagerefere-se a uma variável de estado compartilhada em todas as instâncias de contrato inteligente, enquanto memoryse refere a um local de armazenamento temporário para dados em cada instância de execução de contrato inteligente. Vejamos um exemplo de código abaixo para ver como isso funciona:

Exemplo usando storage:

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense storage cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

No BudgetPlancontrato acima, projetamos uma estrutura de dados para as despesas de uma conta onde cada despesa ( Expense) é uma estrutura contendo pricee item. Em seguida, declaramos a purchasefunção para adicionar um novo Expensea storage.

Exemplo usando memory:

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense memory cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

Quase como no exemplo usando storage, tudo é igual, mas no trecho de código adicionamos um novo Expenseà memória quando a purchasefunção é executada.

Pensamentos finais

Os desenvolvedores devem se ater aos padrões de design porque existem métodos diferentes para atingir objetivos específicos ou implementar determinados conceitos.

Você notará uma mudança substancial em seus aplicativos se praticar esses padrões de projeto do Solidity. Seu aplicativo será mais fácil de contribuir, mais limpo e mais seguro.

Eu recomendo que você use pelo menos um desses padrões em seu próximo projeto Solidity para testar sua compreensão deste tópico.

Sinta-se à vontade para fazer qualquer pergunta relacionada a este tópico ou deixar um comentário na seção de comentários abaixo. 

Fonte: https://blog.logrocket.com/developers-guide-solidity-design-patterns/

#solidity #design-pattern 

Como Usar Os Padrões De Projeto Do Solidity
Léon  Peltier

Léon Peltier

1657647480

Comment Utiliser Les Modèles De Conception Solidity

En raison de la popularité croissante de la blockchain et des DApps (applications décentralisées), les DApps open source connaissent une croissance des contributions d'une grande variété de développeurs. Le cœur de la plupart des applications DApp et blockchain sont des contrats intelligents développés à l'aide de Solidity.

La contribution à des projets open source suscite des inquiétudes au sein de la communauté Solidity car ces projets ont des conséquences réelles sur l'argent des gens, et lorsque des développeurs d'horizons différents collaborent sur un projet, il est presque certain qu'il y aura des erreurs et des conflits de code dans les applications. C'est pourquoi la pratique de normes appropriées pour les DApps est si essentielle.

Pour maintenir d'excellents standards, éliminer les risques, atténuer les conflits et construire des contrats intelligents évolutifs et sécurisés, il est nécessaire d'étudier et d'utiliser la mise en œuvre correcte des modèles et des styles de conception dans Solidity.

Cet article traitera du modèle de conception Solidity ; vous devez être familier avec Solidity pour suivre.

Qu'est-ce qu'un modèle de conception Solidity ?

En tant que développeur, vous pouvez apprendre à utiliser Solidity à partir de diverses ressources en ligne, mais ces matériaux ne sont pas les mêmes, car il existe de nombreuses manières et styles différents d'implémenter les choses dans Solidity.

Les modèles de conception sont des solutions conventionnelles réutilisables utilisées pour résoudre les défauts de conception récurrents. Faire un transfert d'une adresse à une autre est un exemple pratique de souci fréquent dans Solidity qui peut être régulé avec des patrons de conception.

Lors du transfert d'Ether in Solidity, nous utilisons les méthodes Send, Transferou Call. Ces trois méthodes ont le même objectif singulier : envoyer Ether hors d'un contrat intelligent. Voyons comment utiliser les méthodes Transferet Callà cette fin. Les exemples de code suivants illustrent différentes implémentations.

La première est la Transferméthode. Lors de l'utilisation de cette approche, tous les contrats intelligents récepteurs doivent définir une fonction de secours, sinon la transaction de transfert échouera. Il y a une limite de gaz de 2300 gaz disponible, ce qui est suffisant pour terminer la transaction de transfert et aide à prévenir les agressions de rentrée :

function Transfer(address payable _to) public payable {     
  _to.transfer(msg.value); 
} 

L'extrait de code ci-dessus définit la Transferfonction, qui accepte une adresse de réception en tant que _toet utilise la _to.transferméthode pour initier le transfert d'Ether spécifié en tant que msg.value.

Vient ensuite la Callméthode. D'autres fonctions du contrat peuvent être déclenchées à l'aide de cette méthode et éventuellement définir des frais de gaz à utiliser lorsque la fonction s'exécute :

 

function Call(address payable _to) public payable {
    (bool sent) = _to.call.gas(1000){value: msg.value}("");
    require("Sent, Ether not sent");
}

L'extrait de code ci-dessus définit la Callfonction, qui accepte une adresse de réception sous la forme _to, définit le statut de la transaction sur booléen et le résultat renvoyé est fourni dans la variable de données. Si msg.dataest vide, la receivefonction s'exécute immédiatement après la Callméthode. Le repli s'exécute là où il n'y a pas d'implémentation de la fonction de réception.

La méthode la plus préférée pour transférer Ether entre des contrats intelligents consiste à utiliser la Callméthode.

Dans les exemples ci-dessus, nous avons utilisé deux techniques différentes pour transférer Ether. Vous pouvez spécifier la quantité de gaz que vous souhaitez dépenser à l'aide de Call, alors qu'il Transfera une quantité fixe de gaz par défaut.

Ces techniques sont des modèles pratiqués dans Solidity pour implémenter l'occurrence récurrente de Transfer.

Pour garder les choses dans leur contexte, les sections suivantes présentent certains des modèles de conception réglementés par Solidity.

Modèles comportementaux

Contrôle de garde

La fonction principale des contrats intelligents est de s'assurer que les exigences des transactions passent. Si une condition échoue, le contrat revient à son état précédent. Solidity y parvient en utilisant le mécanisme de gestion des erreurs de l'EVM pour lancer des exceptions et restaurer le contrat dans un état de fonctionnement avant l'exception.

Le contrat intelligent ci-dessous montre comment implémenter le modèle de contrôle de garde en utilisant les trois techniques :

contract Contribution {
  function contribute (address _from) payable public {
    require(msg.value != 0);
    require(_from != address(0));
    unit prevBalance = this.balance;
    unit amount;

    if(_from.balance == 0) {
      amount = msg.value;
    } else if (_from.balance < msg.sender.balance) {
      amount = msg.value / 2;
    } else {
      revert("Insufficent Balance!!!");
    }

    _from.transfer(amount);
    assert(this.balance == prevBalance - amount);
  }
}

Dans l'extrait de code ci-dessus, Solidity gère les exceptions d'erreur en utilisant ce qui suit :

require()déclare les conditions dans lesquelles une fonction s'exécute. Il accepte une seule condition comme argument et lève une exception si la condition est évaluée comme fausse, mettant fin à l'exécution de la fonction sans brûler de gaz.

assert()évalue les conditions d'une fonction, puis lève une exception, ramène le contrat à l'état précédent et consomme l'approvisionnement en gaz si les exigences échouent après l'exécution.

revert()lève une exception, renvoie tout gaz fourni et rétablit l'appel de fonction à l'état d'origine du contrat si l'exigence de la fonction échoue. La revert()méthode n'évalue ni ne requiert aucune condition.

Machine d'état

Le modèle de machine d'état simule le comportement d'un système en fonction de ses entrées précédentes et actuelles. Les développeurs utilisent cette approche pour décomposer les gros problèmes en étapes et transitions simples, qui sont ensuite utilisées pour représenter et contrôler le flux d'exécution d'une application.

Le modèle de machine d'état peut également être implémenté dans des contrats intelligents, comme indiqué dans l'extrait de code ci-dessous :

contract Safe {
    Stages public stage = Stages.AcceptingDeposits;
    uint public creationTime = now;
    mapping (address => uint) balances;

    modifier atStage(Stages _stage) {
      require(stage == _stage);
      _;
    }

    modifier timedTransitions() {
      if (stage == Stages.AcceptingDeposits && now >=
      creationTime + 1 days)
      nextStage();
      if (stage == Stages.FreezingDeposits && now >=
      creationTime + 4 days)
      nextStage();
      _;
    }
    function nextStage() internal {
      stage = Stages(uint(stage) + 1);
    }
    function deposit() public payable timedTransitions atStage(Stages.AcceptingDeposits) {
      balances[msg.sender] += msg.value;
    }
    function withdraw() public timedTransitions atStage(Stages.ReleasingDeposits) {
      uint amount = balances[msg.sender];
      balances[msg.sender] = 0;
      msg.sender.transfer(amount);
    }
}

Dans l'extrait de code ci-dessus, le Safecontrat utilise des modificateurs pour mettre à jour l'état du contrat entre les différentes étapes. Les étapes déterminent quand les dépôts et les retraits peuvent être effectués. Si l'état actuel du contrat n'est pas AcceptingDeposit, les utilisateurs ne peuvent pas déposer au contrat, et si l'état actuel n'est pas ReleasingDeposit, les utilisateurs ne peuvent pas se retirer du contrat.

Oracle

Les contrats Ethereum ont leur propre écosystème où ils communiquent. Le système ne peut importer des données externes que via une transaction (en transmettant des données à une méthode), ce qui est un inconvénient car de nombreux cas d'utilisation de contrats impliquent des connaissances provenant de sources autres que la blockchain (par exemple, la bourse).

Une solution à ce problème consiste à utiliser le modèle oracle avec une connexion au monde extérieur. Lorsqu'un service oracle et un contrat intelligent communiquent de manière asynchrone, le service oracle sert d'API. Une transaction commence par invoquer une fonction de contrat intelligent, qui comprend une instruction pour envoyer une requête à un oracle.

En fonction des paramètres d'une telle requête, l'oracle récupère un résultat et le renvoie en exécutant une fonction de rappel dans le contrat principal. Les contrats basés sur Oracle sont incompatibles avec le concept de blockchain d'un réseau décentralisé, car ils reposent sur l'honnêteté d'une seule organisation ou d'un seul groupe.

Les services Oracle 21 et 22 corrigent ce défaut en fournissant un contrôle de validité avec les données fournies. Notez qu'un oracle doit payer pour l'appel de rappel. Par conséquent, des frais d'oracle sont payés en même temps que l'éther requis pour l'appel de rappel.

L'extrait de code ci-dessous montre la transaction entre un contrat oracle et son contrat consommateur :

contract API {
    address trustedAccount = 0x000...; //Account address
    struct Request {
        bytes data;
        function(bytes memory) external callback;
    }
    Request[] requests;
    event NewRequest(uint);

    modifier onlyowner(address account) {
        require(msg.sender == account);
        _;
    }
    function query(bytes data, function(bytes memory) external callback) public {
        requests.push(Request(data, callback));
        NewRequest(requests.length - 1);
    }
    // invoked by outside world
    function reply(uint requestID, bytes response) public
    onlyowner(trustedAccount) {
    requests[requestID].callback(response);
    }
}

Dans l'extrait de code ci-dessus, le APIcontrat intelligent envoie une demande de requête à knownSourcel'aide de la queryfonction, qui exécute la external callbackfonction et utilise la replyfonction pour collecter les données de réponse de la source externe.

Aléatoire

Malgré la difficulté de générer des valeurs aléatoires et uniques dans Solidity, il est très demandé. Les horodatages de bloc sont une source d'aléatoire dans Ethereum, mais ils sont risqués car le mineur peut les falsifier. Pour éviter ce problème, des solutions telles que PRNG à bloc de hachage et Oracle RNG ont été créées.

L'extrait de code suivant montre une implémentation de base de ce modèle en utilisant le hachage de bloc le plus récent :

// This method is predicatable. Use with care!
function random() internal view returns (uint) {
    return uint(blockhash(block.number - 1));
}

La randomNum()fonction ci-dessus génère un entier aléatoire et unique en hachant le numéro de bloc ( block.number, qui est une variable sur la blockchain).

Modèles de sécurité

Restriction d'accès

Comme il n'existe aucun moyen intégré pour gérer les privilèges d'exécution dans Solidity, une tendance courante consiste à limiter l'exécution des fonctions. L'exécution des fonctions ne doit être soumise qu'à certaines conditions telles que le moment, les informations sur l'appelant ou la transaction et d'autres critères.

Voici un exemple de conditionnement d'une fonction :

contract RestrictPayment {
    uint public date_time = now;

    modifier only(address account) {
        require(msg.sender == account);
        _;
    }

    function f() payable onlyowner(date_time + 1 minutes){
      //code comes here
    }
}

Le contrat Restrict ci-dessus empêche tout accountautre que le msg.senderd'exécuter la payablefonction. Si les exigences de la payablefonction ne sont pas remplies, requireest utilisé pour lever une exception avant l'exécution de la fonction.

Vérifier les interactions des effets

Le modèle d'interaction des effets de contrôle réduit le risque que des contrats malveillants tentent de prendre le contrôle du flux de contrôle suite à un appel externe. Le contrat transfère probablement le flux de contrôle à une entité externe pendant la procédure de transfert Ether. Si le contrat externe est malveillant, il a le potentiel de perturber le flux de contrôle et de faire rebondir l'expéditeur dans un état indésirable.

Pour utiliser ce modèle, nous devons être conscients des parties de notre fonction qui sont vulnérables afin de pouvoir réagir une fois que nous avons trouvé la source possible de vulnérabilité.

Voici un exemple d'utilisation de ce modèle :

contract CheckedTransactions {
    mapping(address => uint) balances;
    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        msg.sender.transfer(amount);
    }
}

Dans l'extrait de code ci-dessus, la require()méthode est utilisée pour lancer une exception si la condition balances[msg.sender] >= amountéchoue. Cela signifie qu'un utilisateur ne peut pas retirer un amountmontant supérieur au solde du msg.sender.

Transfert Ether sécurisé

Bien que les transferts de crypto-monnaie ne soient pas la fonction principale de Solidity, ils se produisent fréquemment. Comme nous en avons discuté précédemment, Transfer, Call, et Sendsont les trois techniques fondamentales pour transférer Ether in Solidity. Il est impossible de décider quelle méthode utiliser à moins d'être conscient de leurs différences.

En plus des deux méthodes ( Transferet Call) décrites plus haut dans cet article, la transmission d'Ether in Solidity peut être effectuée à l'aide de la Sendméthode.

Sendest similaire à Transferen ce sens qu'il coûte la même quantité de gaz que la valeur par défaut (2300). Contrairement à Transfer, cependant, il renvoie un résultat booléen indiquant si le Senda réussi ou non. La plupart des projets Solidity n'utilisent plus la Sendméthode.

Ci-dessous une implémentation de la Sendméthode :

function send(address payable _to) external payable{
    bool sent = _to.send(123);
    require(sent, "send failed");
}

La sendfonction ci-dessus utilise la require()fonction pour lever une exception si la Booleanvaleur de envoyé renvoyée par _to.send(123)est false.

Pull-over-push

Ce modèle de conception déplace le risque de transfert d'Ether du contrat vers les utilisateurs. Pendant le transfert Ether, plusieurs choses peuvent mal tourner, provoquant l'échec de la transaction. Dans le modèle pull-over-push, trois parties sont impliquées : l'entité à l'origine du transfert (l'auteur du contrat), le contrat intelligent et le destinataire.

Ce modèle inclut la cartographie, qui facilite le suivi des soldes impayés des utilisateurs. Au lieu de livrer l'Ether du contrat à un destinataire, l'utilisateur invoque une fonction pour retirer son Ether alloué. Toute inexactitude dans l'un des virements n'a aucun impact sur les autres transactions.

Voici un exemple de pull-over-pull :

contract ProfitsWithdrawal {
    mapping(address => uint) profits;
    function allowPull(address owner, uint amount) private {
        profits[owner] += amount;
    }
    function withdrawProfits() public {
        uint amount = profits[msg.sender];
        require(amount != 0);
        require(address(this).balance >= amount);
        profits[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

Dans le ProfitsWithdrawalcontrat ci-dessus, permet aux utilisateurs de retirer les bénéfices mappés à leur addresssi le solde de l'utilisateur est supérieur ou égal aux bénéfices attribués à l'utilisateur.

Arrêt d'urgence

Les contrats intelligents audités peuvent contenir des bogues qui ne sont pas détectés tant qu'ils ne sont pas impliqués dans un cyberincident. Les erreurs découvertes après le lancement du contrat seront difficiles à corriger. Avec l'aide de cette conception, nous pouvons arrêter un contrat en bloquant les appels aux fonctions critiques, empêchant les attaquants jusqu'à la rectification du contrat intelligent.

Seuls les utilisateurs autorisés doivent être autorisés à utiliser la fonctionnalité d'arrêt pour empêcher les utilisateurs d'en abuser. Une variable d'état est définie de falseà truepour déterminer la résiliation du contrat. Après la résiliation du contrat, vous pouvez utiliser le modèle de restriction d'accès pour vous assurer qu'aucune fonction critique n'est exécutée.

Une modification de fonction qui lève une exception si la variable d'état indique le déclenchement d'un arrêt d'urgence peut être utilisée pour accomplir cela, comme indiqué ci-dessous :

contract EmergencyStop {
    bool Running = true;
    address trustedAccount = 0x000...; //Account address
    modifier stillRunning {
        require(Running);
        _;
    }
    modifier NotRunning {
        require(¡Running!);
        _;
    }
    modifier onlyAuthorized(address account) {
        require(msg.sender == account);
        _;
    }
    function stopContract() public onlyAuthorized(trustedAccount) {
        Running = false;
    }
    function resumeContract() public onlyAuthorized(trustedAccount) {
        Running = true;
    }
}

Le EmergencyStopcontrat ci-dessus utilise des modificateurs pour vérifier les conditions et lever des exceptions si l'une de ces conditions est remplie. Le contrat utilise les fonctions stopContract()et resumeContract()pour gérer les situations d'urgence.

Le contrat peut être repris en réinitialisant la variable d'état à false. Cette méthode doit être sécurisée contre les appels non autorisés de la même manière que la fonction d'arrêt d'urgence.

Modèles d'évolutivité

Délégué mandataire

Ce modèle permet de mettre à niveau les contrats intelligents sans casser aucun de leurs composants. Un message particulier appelé Delegatecallest utilisé lors de l'utilisation de cette méthode. Il transmet l'appel de fonction au délégué sans exposer la signature de la fonction.

La fonction de secours du contrat de proxy l'utilise pour initier le mécanisme de transfert pour chaque appel de fonction. La seule chose Delegatecallrenvoyée est une valeur booléenne qui indique si l'exécution a réussi ou non. Nous sommes plus intéressés par la valeur de retour de l'appel de fonction. Gardez à l'esprit que, lors de la mise à niveau d'un contrat, la séquence de stockage ne doit pas changer ; seuls les ajouts sont autorisés.

Voici un exemple d'implémentation de ce modèle :

contract UpgradeProxy {
    address delegate;
    address owner = msg.sender;
    function upgradeDelegate(address newDelegateAddress) public {
        require(msg.sender == owner);
        delegate = newDelegateAddress;
    }
    function() external payable {
        assembly {
            let _target := sload(0)
            calldatacopy(0x01, 0x01, calldatasize)
            let result := delegatecall(gas, _target, 0x01, calldatasize, 0x01, 0)
            returndatacopy(0x01, 0x01, returndatasize)
            switch result case 0 {revert(0, 0)} default {return (0, returndatasize)}
        }
    }
}

Dans l'extrait de code ci-dessus, UpgradeProxygère un mécanisme qui permet delegatede mettre à niveau le contrat une fois le ownercontrat exécuté en appelant la fonction de secours qui transfère une copie des delegatedonnées du contrat vers la nouvelle version.

Construction de matrice de mémoire

Cette méthode agrège et récupère rapidement et efficacement les données du stockage contractuel. Interagir avec la mémoire d'un contrat est l'une des actions les plus coûteuses de l'EVM. Assurer la suppression des redondances et le stockage des seules données requises peut aider à minimiser les coûts.

Nous pouvons agréger et lire les données du stockage sous contrat sans engager de dépenses supplémentaires en utilisant la modification de la fonction d'affichage. Au lieu de stocker un tableau dans le stockage, il est recréé en mémoire chaque fois qu'une recherche est requise.

Une structure de données facilement itérable, telle qu'un tableau, est utilisée pour faciliter la récupération des données. Lors du traitement de données ayant plusieurs attributs, nous les agrégeons à l'aide d'un type de données personnalisé tel que struct.

Le mappage est également nécessaire pour suivre le nombre attendu d'entrées de données pour chaque instance agrégée.

Le code ci-dessous illustre ce modèle :

contract Store {
    struct Item {
        string name;
        uint32 price;
        address owner;
    }
    Item[] public items;
    mapping(address => uint) public itemsOwned;
    function getItems(address _owner) public view returns (uint[] memory) {
        uint\[] memory result = new uint[\](itemsOwned[_owner]);
        uint counter = 0;
        for (uint i = 0; i < items.length; i++) {
            if (items[i].owner == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
}

Dans le Storecontrat ci-dessus, nous utilisons structpour concevoir une structure de données d'éléments dans une liste, puis nous mappons les éléments à leurs propriétaires address. Pour obtenir les éléments appartenant à une adresse, nous utilisons la getItemsfonction pour agréger une mémoire appelée result.

Stockage éternel

Ce modèle conserve la mémoire d'un contrat intelligent mis à niveau. Étant donné que l'ancien contrat et le nouveau contrat sont déployés séparément sur la blockchain, le stockage accumulé reste à son ancien emplacement, où les informations de l'utilisateur, les soldes des comptes et les références à d'autres informations précieuses sont stockées.

Le stockage éternel doit être aussi indépendant que possible pour empêcher les modifications du stockage des données en implémentant plusieurs mappages de stockage de données, un pour chaque type de données. La conversion de la valeur abstraite en une carte de hachage sha3 sert de magasin clé-valeur.

Étant donné que la solution proposée est plus sophistiquée que le stockage de valeur conventionnel, les wrappers peuvent réduire la complexité et rendre le code lisible. Dans un contrat évolutif qui utilise le stockage éternel, les wrappers facilitent la gestion de la syntaxe inconnue et des clés avec des hachages.

Les extraits de code ci-dessous montrent comment utiliser des wrappers pour implémenter le stockage éternel :

function getBalance(address account) public view returns(uint) {
    return eternalStorageAdr.getUint(keccak256("balances", account));
}
function setBalance(address account, uint amount) internal {
    eternalStorageAdr.setUint(keccak256("balances", account), amount);
}
function addBalance(address account, uint amount) internal {
    setBalance(account, getBalance(account) + amount);
}

Dans l'extrait de code ci-dessus, nous avons obtenu le solde d'un accountstockage éternel en utilisant la keccak256fonction de hachage dans enternalStorageAdr.getUint(), et de même pour définir le solde du compte.

Mémoire vs stockage

Storage, memoryou calldatasont les méthodes utilisées lors de la déclaration de l'emplacement d'un type de données dynamique sous la forme d'une variable, mais nous nous concentrerons sur memoryet storagepour l'instant. Le terme storagefait référence à une variable d'état partagée entre toutes les instances de contrat intelligent, tandis memoryqu'il fait référence à un emplacement de stockage temporaire pour les données dans chaque instance d'exécution de contrat intelligent. Regardons un exemple de code ci-dessous pour voir comment cela fonctionne :

Exemple utilisant storage:

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense storage cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

Dans le BudgetPlancontrat ci-dessus, nous avons conçu une structure de données pour les dépenses d'un compte où chaque dépense ( Expense) est une structure contenant priceet item. Nous avons ensuite déclaré la purchasefonction pour ajouter un nouveau Expenseà storage.

Exemple utilisant memory:

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense memory cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

Presque comme dans l'exemple utilisant storage, tout est pareil, mais dans l'extrait de code, nous ajoutons un nouveau Expenseà la mémoire lorsque la purchasefonction est exécutée.

Réflexions finales

Les développeurs doivent s'en tenir aux modèles de conception car il existe différentes méthodes pour atteindre des objectifs spécifiques ou mettre en œuvre certains concepts.

Vous remarquerez un changement substantiel dans vos applications si vous pratiquez ces modèles de conception Solidity. Votre application sera plus facile à contribuer, plus propre et plus sécurisée.

Je vous recommande d'utiliser au moins un de ces modèles dans votre prochain projet Solidity pour tester votre compréhension de ce sujet.

N'hésitez pas à poser des questions sur ce sujet ou à laisser un commentaire dans la section des commentaires ci-dessous. 

Source : https://blog.logrocket.com/developers-guide-solidity-design-patterns/

#solidity #design-pattern 

Comment Utiliser Les Modèles De Conception Solidity
Hoang  Ha

Hoang Ha

1657645200

Cách Sử Dụng Các Mẫu Thiết Kế Solidity

Do sự phổ biến ngày càng tăng của blockchain và DApps (các ứng dụng phi tập trung), DApps nguồn mở đang chứng kiến ​​sự tăng trưởng đóng góp từ nhiều nhà phát triển. Trung tâm của hầu hết các DApp và ứng dụng blockchain là các hợp đồng thông minh được phát triển bằng Solidity.

Việc đóng góp vào các dự án mã nguồn mở làm dấy lên mối lo ngại trong cộng đồng Solidity vì những dự án này có hậu quả thực tế đối với tiền của mọi người và khi các nhà phát triển từ các nền tảng khác nhau cộng tác trong một dự án, gần như chắc chắn rằng sẽ có lỗi và xung đột mã trong các ứng dụng. Đây là lý do tại sao việc thực hành các tiêu chuẩn thích hợp cho DApps là rất quan trọng.

Để duy trì các tiêu chuẩn xuất sắc, loại bỏ rủi ro, giảm thiểu xung đột và xây dựng các hợp đồng thông minh có thể mở rộng và an toàn, cần phải nghiên cứu và sử dụng việc triển khai chính xác các mẫu và phong cách thiết kế trong Solidity.

Bài viết này sẽ thảo luận về mẫu thiết kế Solidity; bạn phải làm quen với Solidity để làm theo cùng.

Mẫu thiết kế Solidity là gì?

Là một nhà phát triển, bạn có thể học cách sử dụng Solidity từ nhiều tài nguyên khác nhau trên mạng, nhưng những tài liệu này không giống nhau, vì có nhiều cách và phong cách khác nhau để triển khai mọi thứ trong Solidity.

Các mẫu thiết kế có thể tái sử dụng, các giải pháp thông thường được sử dụng để giải quyết các lỗi thiết kế tái phát. Thực hiện chuyển từ địa chỉ này sang địa chỉ khác là một ví dụ thực tế về mối quan tâm thường xuyên trong Solidity có thể được điều chỉnh bằng các mẫu thiết kế.

Khi chuyển Ether trong Solidity, chúng tôi sử dụng Send, Transferhoặc Callcác phương thức. Ba phương pháp này có cùng một mục tiêu duy nhất: gửi Ether ra khỏi hợp đồng thông minh. Chúng ta hãy xem cách sử dụng TransferCallcác phương pháp cho mục đích này. Các mẫu mã sau đây chứng minh các triển khai khác nhau.

Đầu tiên là Transferphương pháp. Khi sử dụng cách tiếp cận này, tất cả các hợp đồng thông minh nhận được phải xác định chức năng dự phòng, nếu không giao dịch chuyển sẽ không thành công. Có sẵn giới hạn khí là 2300 khí, đủ để hoàn thành giao dịch chuyển tiền và hỗ trợ ngăn chặn các cuộc tấn công vào lại:

function Transfer(address payable _to) public payable {     
  _to.transfer(msg.value); 
} 

Đoạn mã ở trên xác định Transferhàm, hàm này chấp nhận địa chỉ nhận là _tovà sử dụng _to.transferphương thức để bắt đầu chuyển Ether được chỉ định là msg.value.

Tiếp theo là Callphương pháp. Các chức năng khác trong hợp đồng có thể được kích hoạt bằng cách sử dụng phương pháp này và tùy chọn đặt phí gas để sử dụng khi chức năng thực thi:

 

function Call(address payable _to) public payable {
    (bool sent) = _to.call.gas(1000){value: msg.value}("");
    require("Sent, Ether not sent");
}

Đoạn mã ở trên xác định Callhàm, chấp nhận địa chỉ nhận là _to, đặt trạng thái giao dịch là boolean và kết quả trả về được cung cấp trong biến dữ liệu. Nếu msg.datatrống, receivehàm thực thi ngay sau Callphương thức. Dự phòng chạy ở nơi không có triển khai chức năng nhận.

Cách ưa thích nhất để chuyển Ether giữa các hợp đồng thông minh là sử dụng Callphương pháp này.

Trong các ví dụ trên, chúng tôi đã sử dụng hai kỹ thuật khác nhau để chuyển Ether. Bạn có thể chỉ định lượng khí đốt bạn muốn sử dụng Call, trong khi Transfermặc định có một lượng khí đốt cố định.

Các kỹ thuật này là các mẫu được thực hành trong Solidity để triển khai sự xuất hiện định kỳ của Transfer.

Để giữ mọi thứ trong ngữ cảnh, các phần sau đây là một số mẫu thiết kế mà Solidity đã quy định.

Mẫu hành vi

Kiểm tra bảo vệ

Chức năng chính của hợp đồng thông minh là đảm bảo các yêu cầu của giao dịch được thông qua. Nếu bất kỳ điều kiện nào không thành công, hợp đồng sẽ trở lại trạng thái trước đó. Solidity đạt được điều này bằng cách sử dụng cơ chế xử lý lỗi của EVM để loại bỏ ngoại lệ và khôi phục hợp đồng về trạng thái hoạt động trước ngoại lệ.

Hợp đồng thông minh dưới đây cho thấy cách triển khai mô hình kiểm tra bảo vệ bằng cách sử dụng cả ba kỹ thuật:

contract Contribution {
  function contribute (address _from) payable public {
    require(msg.value != 0);
    require(_from != address(0));
    unit prevBalance = this.balance;
    unit amount;

    if(_from.balance == 0) {
      amount = msg.value;
    } else if (_from.balance < msg.sender.balance) {
      amount = msg.value / 2;
    } else {
      revert("Insufficent Balance!!!");
    }

    _from.transfer(amount);
    assert(this.balance == prevBalance - amount);
  }
}

Trong đoạn mã ở trên, Solidity xử lý các ngoại lệ lỗi bằng cách sử dụng như sau:

require()khai báo các điều kiện mà theo đó một hàm thực thi. Nó chấp nhận một điều kiện duy nhất làm đối số và ném ra một ngoại lệ nếu điều kiện cho giá trị là false, chấm dứt thực thi của hàm mà không đốt cháy bất kỳ khí nào.

assert()đánh giá các điều kiện cho một chức năng, sau đó ném một ngoại lệ, hoàn nguyên hợp đồng về trạng thái trước đó và tiêu thụ nguồn cung cấp khí nếu các yêu cầu không thành công sau khi thực hiện.

revert()ném một ngoại lệ, trả về bất kỳ khí được cung cấp nào và hoàn nguyên lệnh gọi hàm về trạng thái ban đầu của hợp đồng nếu yêu cầu đối với hàm không thành công. Phương revert()pháp không đánh giá hoặc yêu cầu bất kỳ điều kiện nào.

Máy trạng thái

Mẫu máy trạng thái mô phỏng hành vi của một hệ thống dựa trên các đầu vào trước đây và hiện tại của nó. Các nhà phát triển sử dụng cách tiếp cận này để chia nhỏ các vấn đề lớn thành các giai đoạn và quá trình chuyển đổi đơn giản, sau đó được sử dụng để đại diện và kiểm soát luồng thực thi của ứng dụng.

Mẫu máy trạng thái cũng có thể được triển khai trong hợp đồng thông minh, như được hiển thị trong đoạn mã bên dưới:

contract Safe {
    Stages public stage = Stages.AcceptingDeposits;
    uint public creationTime = now;
    mapping (address => uint) balances;

    modifier atStage(Stages _stage) {
      require(stage == _stage);
      _;
    }

    modifier timedTransitions() {
      if (stage == Stages.AcceptingDeposits && now >=
      creationTime + 1 days)
      nextStage();
      if (stage == Stages.FreezingDeposits && now >=
      creationTime + 4 days)
      nextStage();
      _;
    }
    function nextStage() internal {
      stage = Stages(uint(stage) + 1);
    }
    function deposit() public payable timedTransitions atStage(Stages.AcceptingDeposits) {
      balances[msg.sender] += msg.value;
    }
    function withdraw() public timedTransitions atStage(Stages.ReleasingDeposits) {
      uint amount = balances[msg.sender];
      balances[msg.sender] = 0;
      msg.sender.transfer(amount);
    }
}

Trong đoạn mã ở trên, Safehợp đồng sử dụng các công cụ sửa đổi để cập nhật trạng thái của hợp đồng giữa các giai đoạn khác nhau. Các giai đoạn xác định khi nào có thể thực hiện gửi tiền và rút tiền. Nếu trạng thái hiện tại của hợp đồng là không AcceptingDeposit, người dùng không thể đặt cọc vào hợp đồng, và nếu trạng thái hiện tại không phải là trạng thái hiện tại ReleasingDeposit, người dùng không thể rút khỏi hợp đồng.

Oracle

Các hợp đồng Ethereum có hệ sinh thái riêng để chúng giao tiếp. Hệ thống chỉ có thể nhập dữ liệu bên ngoài thông qua một giao dịch (bằng cách chuyển dữ liệu đến một phương thức), đây là một nhược điểm vì nhiều trường hợp sử dụng hợp đồng liên quan đến kiến ​​thức từ các nguồn khác ngoài blockchain (ví dụ: thị trường chứng khoán).

Một giải pháp cho vấn đề này là sử dụng mô hình tiên tri với kết nối với thế giới bên ngoài. Khi một dịch vụ tiên tri và hợp đồng thông minh giao tiếp không đồng bộ, dịch vụ tiên tri sẽ đóng vai trò là một API. Một giao dịch bắt đầu bằng cách gọi một chức năng hợp đồng thông minh, bao gồm một hướng dẫn để gửi một yêu cầu đến một nhà tiên tri.

Dựa trên các tham số của một yêu cầu như vậy, oracle sẽ tìm nạp một kết quả và trả về nó bằng cách thực hiện một hàm gọi lại trong hợp đồng chính. Các hợp đồng dựa trên Oracle không tương thích với khái niệm blockchain của một mạng phi tập trung, vì chúng dựa trên sự trung thực của một tổ chức hoặc nhóm duy nhất.

Các dịch vụ 2122 của Oracle giải quyết lỗ hổng này bằng cách kiểm tra tính hợp lệ với dữ liệu được cung cấp. Lưu ý rằng một oracle phải trả tiền cho lệnh gọi lại. Do đó, một khoản phí oracle được trả cùng với Ether cần thiết cho lệnh gọi lại.

Đoạn mã dưới đây cho thấy giao dịch giữa hợp đồng oracle và hợp đồng người tiêu dùng của nó:

contract API {
    address trustedAccount = 0x000...; //Account address
    struct Request {
        bytes data;
        function(bytes memory) external callback;
    }
    Request[] requests;
    event NewRequest(uint);

    modifier onlyowner(address account) {
        require(msg.sender == account);
        _;
    }
    function query(bytes data, function(bytes memory) external callback) public {
        requests.push(Request(data, callback));
        NewRequest(requests.length - 1);
    }
    // invoked by outside world
    function reply(uint requestID, bytes response) public
    onlyowner(trustedAccount) {
    requests[requestID].callback(response);
    }
}

Trong đoạn mã ở trên, APIhợp đồng thông minh sẽ gửi một yêu cầu truy vấn đến một hàm knownSourcebằng cách sử dụng query, thực thi external callbackhàm và sử dụng replyhàm để thu thập dữ liệu phản hồi từ nguồn bên ngoài.

Ngẫu nhiên

Mặc dù việc tạo ra các giá trị ngẫu nhiên và duy nhất trong Solidity phức tạp đến mức nào, nhưng nó vẫn có nhu cầu cao. Dấu thời gian khối là một nguồn ngẫu nhiên trong Ethereum, nhưng chúng rất rủi ro vì người khai thác có thể giả mạo chúng. Để ngăn chặn vấn đề này, các giải pháp như khối băm PRNG và Oracle RNG đã được tạo ra.

Đoạn mã sau đây cho thấy cách triển khai cơ bản của mẫu này bằng cách sử dụng hàm băm khối gần đây nhất:

// This method is predicatable. Use with care!
function random() internal view returns (uint) {
    return uint(blockhash(block.number - 1));
}

Hàm randomNum()trên tạo ra một số nguyên ngẫu nhiên và duy nhất bằng cách băm số khối ( block.number, là một biến trên blockchain).

Các mẫu bảo mật

hạn chế tiếp cận

Vì không có phương tiện tích hợp nào để quản lý các đặc quyền thực thi trong Solidity, một xu hướng phổ biến là giới hạn việc thực thi chức năng. Việc thực thi các chức năng chỉ nên dựa trên các điều kiện nhất định như thời gian, người gọi hoặc thông tin giao dịch và các tiêu chí khác.

Đây là một ví dụ về điều chỉnh một hàm:

contract RestrictPayment {
    uint public date_time = now;

    modifier only(address account) {
        require(msg.sender == account);
        _;
    }

    function f() payable onlyowner(date_time + 1 minutes){
      //code comes here
    }
}

Hợp đồng hạn chế ở trên ngăn chặn bất kỳ điều gì accountkhác với hợp đồng msg.senderthực thi payablechức năng. Nếu các yêu cầu cho payablehàm không được đáp ứng, requiređược sử dụng để ném một ngoại lệ trước khi hàm được thực thi.

Kiểm tra các tương tác hiệu ứng

Mô hình tương tác hiệu ứng kiểm tra làm giảm nguy cơ các hợp đồng độc hại cố gắng tiếp quản luồng kiểm soát sau một cuộc gọi bên ngoài. Hợp đồng có khả năng chuyển luồng kiểm soát cho một thực thể bên ngoài trong quá trình chuyển giao Ether. Nếu hợp đồng bên ngoài là độc hại, nó có khả năng làm gián đoạn luồng kiểm soát và khiến người gửi quay trở lại trạng thái không mong muốn.

Để sử dụng mô hình này, chúng ta phải biết phần nào trong chức năng của chúng ta dễ bị tổn thương để chúng ta có thể ứng phó khi chúng ta tìm thấy nguồn có thể có lỗ hổng.

Sau đây là một ví dụ về cách sử dụng mẫu này:

contract CheckedTransactions {
    mapping(address => uint) balances;
    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        msg.sender.transfer(amount);
    }
}

Trong đoạn mã trên, require()phương thức được sử dụng ném một ngoại lệ nếu điều kiện balances[msg.sender] >= amountkhông thành công. Điều này có nghĩa là, người dùng không thể rút amountsố dư lớn hơn của msg.sender.

Chuyển Ether an toàn

Mặc dù chuyển tiền điện tử không phải là chức năng chính của Solidity, nhưng chúng xảy ra thường xuyên. Như chúng ta đã thảo luận trước đó, TransferCallSendba kỹ thuật cơ bản để chuyển Ether trong Solidity. Không thể quyết định sử dụng phương pháp nào trừ khi một người nhận thức được sự khác biệt của chúng.

Ngoài hai phương pháp ( TransferCall) đã thảo luận trước đó trong bài viết này, việc truyền Ether trong Solidity có thể được thực hiện bằng Sendphương pháp này.

Sendtương tự như Transfernó tiêu tốn cùng một lượng khí đốt như mặc định (2300). TransferTuy nhiên, không giống như, nó trả về một kết quả boolean cho biết kết quả có thành Sendcông hay không. Hầu hết các dự án Solidity không còn sử dụng Sendphương pháp này nữa.

Dưới đây là một triển khai của Sendphương pháp:

function send(address payable _to) external payable{
    bool sent = _to.send(123);
    require(sent, "send failed");
}

Hàm sendtrên, sử dụng require()hàm để ném một ngoại lệ nếu Booleangiá trị được gửi trả về từ _to.send(123)đó là false.

Kéo-qua-đẩy

Mẫu thiết kế này làm thay đổi nguy cơ chuyển Ether từ hợp đồng sang người dùng. Trong quá trình chuyển Ether, một số sự cố có thể xảy ra, khiến giao dịch không thành công. Trong mô hình pull-over-push, có ba bên tham gia: thực thể bắt đầu chuyển giao (tác giả của hợp đồng), hợp đồng thông minh và người nhận.

Mô hình này bao gồm ánh xạ, hỗ trợ theo dõi số dư chưa thanh toán của người dùng. Thay vì phân phối Ether từ hợp đồng cho người nhận, người dùng gọi một hàm để rút Ether được phân bổ của họ. Bất kỳ sự không chính xác nào trong một trong các giao dịch chuyển tiền sẽ không ảnh hưởng đến các giao dịch khác.

Sau đây là một ví dụ về pull-over-pull:

contract ProfitsWithdrawal {
    mapping(address => uint) profits;
    function allowPull(address owner, uint amount) private {
        profits[owner] += amount;
    }
    function withdrawProfits() public {
        uint amount = profits[msg.sender];
        require(amount != 0);
        require(address(this).balance >= amount);
        profits[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

Trong ProfitsWithdrawalhợp đồng trên, cho phép người dùng rút lợi nhuận được ánh xạ đến của họ addressnếu số dư của người dùng lớn hơn hoặc bằng lợi nhuận được phân bổ cho người dùng.

Dừng khẩn cấp

Các hợp đồng thông minh đã được kiểm toán có thể chứa các lỗi không được phát hiện cho đến khi chúng tham gia vào một sự cố mạng. Các lỗi được phát hiện sau khi hợp đồng ra mắt sẽ rất khó sửa chữa. Với sự trợ giúp của thiết kế này, chúng tôi có thể tạm dừng hợp đồng bằng cách chặn các cuộc gọi đến các chức năng quan trọng, ngăn chặn những kẻ tấn công cho đến khi hợp đồng thông minh được điều chỉnh.

Chỉ những người dùng được ủy quyền mới được phép sử dụng chức năng dừng để ngăn người dùng lạm dụng nó. Một biến trạng thái được đặt từ falseđể truexác định việc chấm dứt hợp đồng. Sau khi chấm dứt hợp đồng, bạn có thể sử dụng mẫu hạn chế truy cập để đảm bảo rằng không có bất kỳ chức năng quan trọng nào được thực thi.

Một sửa đổi hàm ném một ngoại lệ nếu biến trạng thái cho biết việc bắt đầu dừng khẩn cấp có thể được sử dụng để thực hiện điều này, như minh họa bên dưới:

contract EmergencyStop {
    bool Running = true;
    address trustedAccount = 0x000...; //Account address
    modifier stillRunning {
        require(Running);
        _;
    }
    modifier NotRunning {
        require(¡Running!);
        _;
    }
    modifier onlyAuthorized(address account) {
        require(msg.sender == account);
        _;
    }
    function stopContract() public onlyAuthorized(trustedAccount) {
        Running = false;
    }
    function resumeContract() public onlyAuthorized(trustedAccount) {
        Running = true;
    }
}

Hợp EmergencyStopđồng ở trên sử dụng các công cụ sửa đổi để kiểm tra các điều kiện và đưa ra các ngoại lệ nếu bất kỳ điều kiện nào trong số này được đáp ứng. Hợp đồng sử dụng các chức năng stopContract()resumeContract()nhiệm vụ để xử lý các tình huống khẩn cấp.

Hợp đồng có thể được tiếp tục bằng cách đặt lại biến trạng thái thành false. Phương pháp này phải được bảo mật trước các cuộc gọi trái phép giống như cách chức năng dừng khẩn cấp.

Các mô hình nâng cấp

Ủy quyền proxy

Mô hình này cho phép nâng cấp các hợp đồng thông minh mà không phá vỡ bất kỳ thành phần nào của chúng. Một thông báo cụ thể được gọi Delegatecallsẽ được sử dụng khi sử dụng phương pháp này. Nó chuyển tiếp lời gọi hàm tới đại biểu mà không để lộ chữ ký hàm.

Hàm dự phòng của hợp đồng ủy quyền sử dụng nó để bắt đầu cơ chế chuyển tiếp cho mỗi lệnh gọi hàm. Điều duy nhất Delegatecalltrả về là một giá trị boolean cho biết việc thực thi có thành công hay không. Chúng tôi quan tâm nhiều hơn đến giá trị trả về của lệnh gọi hàm. Hãy nhớ rằng, khi nâng cấp hợp đồng, trình tự lưu trữ không được thay đổi; chỉ bổ sung được cho phép.

Dưới đây là một ví dụ về việc triển khai mẫu này:

contract UpgradeProxy {
    address delegate;
    address owner = msg.sender;
    function upgradeDelegate(address newDelegateAddress) public {
        require(msg.sender == owner);
        delegate = newDelegateAddress;
    }
    function() external payable {
        assembly {
            let _target := sload(0)
            calldatacopy(0x01, 0x01, calldatasize)
            let result := delegatecall(gas, _target, 0x01, calldatasize, 0x01, 0)
            returndatacopy(0x01, 0x01, returndatasize)
            switch result case 0 {revert(0, 0)} default {return (0, returndatasize)}
        }
    }
}

Trong đoạn mã ở trên, UpgradeProxyxử lý một cơ chế cho phép delegatenâng cấp hợp đồng sau khi ownerthực thi hợp đồng bằng cách gọi hàm dự phòng chuyển bản sao delegatedữ liệu hợp đồng sang phiên bản mới.

Xây dựng mảng bộ nhớ

Phương pháp này tổng hợp và truy xuất dữ liệu từ bộ nhớ hợp đồng một cách nhanh chóng và hiệu quả. Tương tác với ký ức của một hợp đồng là một trong những hành động tốn kém nhất trong EVM. Đảm bảo loại bỏ các phần dư thừa và chỉ lưu trữ dữ liệu cần thiết có thể giúp giảm thiểu chi phí.

Chúng tôi có thể tổng hợp và đọc dữ liệu từ bộ lưu trữ hợp đồng mà không phải chịu thêm chi phí bằng cách sử dụng sửa đổi chức năng xem. Thay vì lưu trữ một mảng trong bộ nhớ, nó được tạo lại trong bộ nhớ mỗi khi yêu cầu tìm kiếm.

Cấu trúc dữ liệu có thể lặp lại dễ dàng, chẳng hạn như một mảng, được sử dụng để giúp truy xuất dữ liệu dễ dàng hơn. Khi xử lý dữ liệu có một số thuộc tính, chúng tôi tổng hợp dữ liệu đó bằng cách sử dụng kiểu dữ liệu tùy chỉnh như struct.

Ánh xạ cũng được yêu cầu để theo dõi số lượng dữ liệu đầu vào dự kiến ​​cho mỗi trường hợp tổng hợp.

Đoạn mã dưới đây minh họa mẫu này:

contract Store {
    struct Item {
        string name;
        uint32 price;
        address owner;
    }
    Item[] public items;
    mapping(address => uint) public itemsOwned;
    function getItems(address _owner) public view returns (uint[] memory) {
        uint\[] memory result = new uint[\](itemsOwned[_owner]);
        uint counter = 0;
        for (uint i = 0; i < items.length; i++) {
            if (items[i].owner == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
}

Trong Storehợp đồng ở trên, chúng tôi sử dụng structđể thiết kế cấu trúc dữ liệu của các mục trong danh sách, sau đó chúng tôi ánh xạ các mục đó tới chủ sở hữu của chúng address. Để lấy các mục thuộc sở hữu của một địa chỉ, chúng tôi sử dụng getItemshàm để tổng hợp một bộ nhớ được gọi result.

Lưu trữ vĩnh cửu

Mẫu này duy trì bộ nhớ của một hợp đồng thông minh được nâng cấp. Vì hợp đồng cũ và hợp đồng mới được triển khai riêng biệt trên blockchain, bộ nhớ tích lũy vẫn ở vị trí cũ, nơi lưu trữ thông tin người dùng, số dư tài khoản và các tham chiếu đến thông tin có giá trị khác.

Lưu trữ vĩnh cửu phải độc lập nhất có thể để ngăn chặn các sửa đổi đối với lưu trữ dữ liệu bằng cách triển khai nhiều ánh xạ lưu trữ dữ liệu, một ánh xạ cho mỗi loại dữ liệu. Việc chuyển đổi giá trị được trừu tượng hóa thành một bản đồ của băm sha3 đóng vai trò như một kho lưu trữ khóa-giá trị.

Vì giải pháp được đề xuất phức tạp hơn so với lưu trữ giá trị thông thường, trình bao bọc có thể giảm độ phức tạp và làm cho mã dễ đọc. Trong một hợp đồng có thể nâng cấp sử dụng bộ nhớ vĩnh cửu, trình bao bọc giúp xử lý các cú pháp và khóa không quen thuộc với các hàm băm dễ dàng hơn.

Đoạn mã dưới đây cho thấy cách sử dụng trình bao bọc để triển khai lưu trữ vĩnh cửu:

function getBalance(address account) public view returns(uint) {
    return eternalStorageAdr.getUint(keccak256("balances", account));
}
function setBalance(address account, uint amount) internal {
    eternalStorageAdr.setUint(keccak256("balances", account), amount);
}
function addBalance(address account, uint amount) internal {
    setBalance(account, getBalance(account) + amount);
}

Trong đoạn mã ở trên, chúng tôi nhận được số dư của một accounttừ bộ nhớ vĩnh cửu bằng cách sử dụng keccak256hàm băm trong enternalStorageAdr.getUint()và tương tự như vậy để thiết lập số dư của tài khoản.

Bộ nhớ so với bộ nhớ

Storage, memoryhoặc calldatalà các phương thức được sử dụng khi khai báo vị trí của kiểu dữ liệu động dưới dạng một biến, nhưng chúng ta sẽ tập trung vào memorystoragebây giờ. Thuật ngữ này storageđề cập đến một biến trạng thái được chia sẻ trên tất cả các trường hợp của hợp đồng thông minh, trong khi memoryđề cập đến vị trí lưu trữ tạm thời cho dữ liệu trong mỗi trường hợp thực thi hợp đồng thông minh. Hãy xem một ví dụ về mã dưới đây để xem cách này hoạt động như thế nào:

Ví dụ sử dụng storage:

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense storage cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

Trong BudgetPlanhợp đồng ở trên, chúng tôi đã thiết kế cấu trúc dữ liệu cho các chi phí của tài khoản trong đó mỗi chi phí ( Expense) là một cấu trúc chứa priceitem. Sau đó, chúng tôi đã khai báo purchasehàm để thêm mới Expensevào storage.

Ví dụ sử dụng memory:

contract BudgetPlan {
        struct Expense {
                uint price;
                string item;
        } 
        mapping(address => Expense) public Expenses;
        function purchase() external {
                Expense memory cart = Expenses[msg.sender]
                cart.string = "Strawberry" 
                cart.price = 12
        }
}

Gần giống như ví dụ đang sử dụng storage, mọi thứ đều giống nhau, nhưng trong đoạn mã, chúng ta thêm một đoạn mã mới Expensevào bộ nhớ khi purchasehàm được thực thi.

Bớt tư tưởng

Các nhà phát triển nên bám sát các mẫu thiết kế vì có nhiều phương pháp khác nhau để đạt được các mục tiêu cụ thể hoặc thực hiện các khái niệm nhất định.

Bạn sẽ nhận thấy một sự thay đổi đáng kể trong các ứng dụng của mình nếu bạn thực hành các mẫu thiết kế Solidity này. Ứng dụng của bạn sẽ dễ đóng góp hơn, sạch hơn và an toàn hơn.

Tôi khuyên bạn nên sử dụng ít nhất một trong những mẫu này trong dự án Solidity tiếp theo để kiểm tra sự hiểu biết của bạn về chủ đề này.

Hãy thoải mái đặt bất kỳ câu hỏi nào liên quan đến chủ đề này hoặc để lại ý kiến ​​trong phần bình luận bên dưới. 

Nguồn: https://blog.logrocket.com/developers-guide-solidity-design-patterns/ 

#solidity #design-pattern 

Cách Sử Dụng Các Mẫu Thiết Kế Solidity

How to Use Solidity Design Patterns

Due to the continued increasing popularity of blockchain and DApps (decentralized applications), open source DApps are seeing growth in contributions from a wide variety of developers. The heart of most DApps and blockchain applications are smart contracts developed using Solidity.

Contribution to open source projects raises concerns within the Solidity community because these projects have real-world consequences for people’s money, and when developers from different backgrounds collaborate on a project, it is almost certain that there will be errors and code conflicts in the applications. This is why practicing proper standards for DApps is so critical.

To maintain excellent standards, eliminate risks, mitigate conflicts, and construct scalable and secure smart contracts, it is necessary to study and use the correct implementation of design patterns and styles in Solidity.

This article will discuss the Solidity design pattern; you must be familiar with Solidity to follow along.

See more at: https://blog.logrocket.com/developers-guide-solidity-design-patterns/

#solidity #design-pattern 

How to Use Solidity Design Patterns