程序化生成ta_游戏开发算法[通俗易懂]

程序化生成ta_游戏开发算法[通俗易懂]用一些算法/工具(特别是Houdini)生成美术内容(特别是大世界场景)所遇到的算法之外的问题(Quesion)与问题(Problem)_程序化生成

本篇讨论的是什么

从概念上讲,PCG(程序化生成)的含义很广:任何通过规则计算得到的内容,都可算作是PCG。但在很多游戏项目的资料,包括本篇,讨论PCG时特指是:用一些算法/工具(特别是Houdini)生成美术内容(特别是大世界场景)。

PCG的核心是算法,即到底通过什么样的逻辑来生成内容。算法可以直接通过代码编写,或者使用Houdini节点表达,或者借助GPU快速的并行计算能力。但本篇并不会讨论算法,而是算法之外的问题,往往被称为“流程”或“管线”。

在给本篇写标题时,我又觉得我所讨论的问题并非都能归为“流程”或“管线”,毕竟“流程管线”也是个模糊的概念。而我所讨论的,就只是纯粹的“问题”。另外,这个“问题”也包含两层含义,这里用英文表示或许更准确:

  • Quesion:要问自己项目的问题,是一种“选择”。
  • Problem:会遇到的困难。

所以,标题是:算法之外的问题(Quesion)与问题(Problem)。之后的小节也以Q与P为单位讨论(需要说明:本篇主要讨论了问题是什么,并不讨论如何完全解决,因为不同项目背景不一样,而且很多问题我也不知道怎么解决🤷‍):

本篇名词

生成:程序化生成
生成计算:执行的一次生成过程
生成器:执行生成计算的实体
用户:执行生成计算,监督其生成结果,调整生成器的参数
流水线:运行在非用户的机器(构建机)上,自动执行的逻辑。

Q1. 生成哪些类型的内容?

首先要考虑的问题是:
场景中会有哪些类型的内容,而其中的哪些涉及到程序化生成?
(下截图来源UOD2021重生边缘分享)
在这里插入图片描述一种类型的内容是否由程序化生成,有多个考虑因素:

  • 如果它量多、对效果不太敏感、手动难以完成,则会交给程序化生成。
  • 如果它量少,对效果非常敏感,难以用程序化技术生成,则会交给手动处理。

而比较头疼的就是处于上面二者中间的内容,它数量说多不多说少不少、效果不能太差但也不会要求太高、程序化生成虽然难但是也能开发,手动也不是不能做。这些内容就要凭经验去决定到底要不要程序化生成了。
目前比较常见的生成内容如:地形高度和材质分布、植被等实体的分布、河流面片。

但不同的项目类型会区别很大。如果场景只是地牢迷宫,那就不用考虑复杂的河流,而专心于生成墙面等地牢的元素。如果场景是现代都市,那么地形会相对平坦,就不用考虑生成山体的各种自然效果,而更关注高楼大厦的生成。如果游戏是赛车游戏,那么最重要的应该就是赛道和周边景物的生成了。
而不同艺术风格也会影响需要生成的内容,比如一些细腻的自然模拟效果,可能就无法在卡通风格的场景中很好的复现。

不同生成内容,都会有最适合的数据管线与交互方式。因此在最开始,搞清楚要生成什么东西是最重要的。

但实际上,很难在一开始就真的搞清楚所有要生成的东西。做到最后总会发现和一开始的计划有差别,总会有“想少了”和“想多了”的情况。
在我看来,“想多了” 的危害更大,因为它意味着白白耗费了工作量做了不需要的东西。而如果“想少了”只要后续添加上就行——但前提是管线流程的设计可以支持添加新的东西。

Q2. 各个内容的控制度

对于同一种类型的内容,其控制度也需要考虑。例如,对于 “路” 这个内容:
FarCry5分享中可以看到其路的走向是由用户画曲线控制的:
在这里插入图片描述
而GhostRecon分享中提到他们是根据算法自动计算道路的,因为他们的路太多了。这样,GhostRecon的路其控制度就会相对更低,但换句话说是自动化度更高。
在这里插入图片描述

不仅是“手动”和“自动”的区别,就算是手动控制,也有控制度高低的区别。
比如对于河流,使用一条简单的曲线就可以调控走向。但如果河流的宽度也要控制呢?那简单的曲线就无法满足了,需要更复杂的一些方式。

需要何种程度的控制,取决于项目自身的美术与Gameplay需求。

黑客帝国是一个很酷的程序化生成项目,但是如果作为游戏项目,就需要注意其控制度是否能满足需求。

Q3. 生成与手工的职责划分?

【Q1】考虑了哪些类型的内容要程序化生成。理想情况下,当然是这种类型的内容完全由程序化生成。

但有时会有不得不让手动去做的情况。例如:虽然我的植被分布由程序化生成,但对于某处区域,其植被的分布真的很敏感,每棵树在哪都要精确的手动控制。

这时候就需要划分职责了,常见的做法是通过地形上指定mask来划分。

Ghost Recon 提到他们使用了 “减法” 的思路。即程序化生成默认生成所有的地方,而美术师可以划分一片区域来表示这里不需要程序化生成。

那与之相反的就可以称为 “加法” 的思路了。对于那些不想太依赖程序化生成,而只想在某些小范围使用程序化技术的项目会比较有用。

Q4. 是否允许手动对生成结果修改?

如果美术师/设计师认为生成的结果不好,想要修改结果怎么办?
这里的问题是:当下次再生成时,手动所作的修改怎么办?

默认情况下肯定是无法保留的,蜘蛛侠分享中也说了:“美术和设计师不应该插手任何程序化生成的内容,他们必须使用程序化系统中可控的参数和输入来影响效果。如果他们不这么做,就有丢失他们的工作(手工劳动)的危险。”

因此,一般是不允许手动对生成结果修改的。如果有修改的需求则首先要从其他方面找问题:

  • 反思【Q1】,这个内容是否就不应该由程序化生成?
  • 反思【Q2】,这个生成所提供的控制度是否没达到要求?如果是的话则应该思考如何增加控制方式。
  • 反思【Q3】,是否应该划分出手动负责的部分?

如果其他方面真的都无法解决,那么就要考虑这个内容是否是最后一次生成,或者说保证手动修改后不需要再次生成,或者说手动修改的丢失是可以接受的(比如虽然还会重新生成,但是频率很低,而手动的修改很小,那么此时每次生成后都再修改一次,也是可以接受的)。

最后,如果以上方面都不能解决,那就真的要建立某种机制来在每次重新生成时保留之前的手动修改了。这样的问题没有通用的解决方式,只能根据“生成内容”和“所要做的手动修改”做专门化的设计。

Q5. 编辑器内交互式生成和自动流水线生成?

生成的执行者有两种:

  • 一种是用户,他们在编辑器内不断修改参数并触发生成,直到看到他们预期的结果,最后提交。
  • 另一种则是自动化流水线,按照周期,或者触发条件(比如有地图上传)来自动化执行,最后自动提交。

例如,FarCry5每晚会执行流水线,完全程序化生成一遍世界:
在这里插入图片描述
GhostRecon分享中提到,当用户提交涉及到地形修改时,也会触发流水线:
在这里插入图片描述

Worldbatch(世界批量化工具)有助于自动完成任务,或允许您随时更新世界使用的任何类型的数据。
Worldbatch 基于 Houdini python (hython) 并使用 “Hqueue” (SideFX公司的)作业分配系统在“构建渲染农场”上启动数据“作业”。一个作业加载 Houdini 资产 (.otl) 并根据需要设置参数并从该 otl 启动数据渲染过程。
它可以在UI模式下单独使用,用户可以直接从世界地图中选择他想要重新计算的内容,并将作业发送到自己的计算机上,或发送到渲染农场。
我之前提到过 Perforce 插件,当用户在引擎中提交涉及地形的更改列表时,Worldbatch 也将自动启动,Worldbatch 将由命令行脚本启用,并将基本作业发送到农场。以确保即使地形发生变化,数据也是最新的。

“用户在编辑器内交互式生成”和“流水线自动生成”有着相反的优缺点:

用户交互式生成 流水线自动生成
优:可不断调整参数 缺:固定的参数
优:可确认生成结果 缺:可能提交错误的结果
缺:消耗人力成本 优:完全自动,可夜间执行
缺:需要用户有一定经验,且可能失误 优:完全按照程序执行

程序化生成参与度较高且人手不足的项目,建立自动化流水线是很有必要的。像FarCry5这样每晚重新生成世界的流程也会让很多问题变得方便。

但对于一些程序化生成并没有大量覆盖的项目,让用户自己去生成内容是更方便的。

因此,用户和流水线各需要承担什么样的职责,也是需要结合项目情况考虑的问题。

Q6. 是否划分“开发阶段”?

在蜘蛛侠的分享中,他们将PCG管线分为了三个阶段,每个阶段所走的PCG流程是不一样的:
在这里插入图片描述
如果根据不同阶段采用不同的流程,则会减少很多问题,比如【Q4】我就可以直接对于上阶段生成的结果进行手动的修改,因为我知道在下一阶段不会再重新生成了。

但唯一的风险就是:阶段的划分真的是有效的吗?换句话说,有没有可能回到之前的阶段?
如果真的出了岔子需要回到之前的阶段,那就意味着现在阶段的有些工作有丢失的风险,毕竟我在做这个阶段的工作时我是认为上阶段的事情都结束了。

GhostRecon和蜘蛛侠有真实场景(玻利维亚和曼哈顿)作为基础,所以其底层的阶段具有一定的稳定性,毕竟真实的东西不会改变。但对于没有真实场景为基础的游戏项目,其底层阶段的变化的可能性,就是一个重要的考虑因素了。

P1. 如何处理引擎与生成器之间的输入输出?

如何将引擎里的一些数据输入给生成器,如何将生成器的结果转换为引擎内容,这个问题取决于你的生成器是什么。

如果你的生成器就是写在引擎中的代码,则就不需要考虑这个问题。如果你的生成器借助了GPU,则可能会需要一些与计算着色器进行交互的开发。

而目前比较流行的生成器是Houdini。那么主要考虑的就是Houdini数据如何转化成引擎内容。
Houdini官方给UE和Unity都提供了各自的HoudiniEngine插件可以直接用。其底层都使用了HAPI来解析后台Houdini节点里的数据,然后再转换为各自引擎的内容。不过,项目中可能会有些非通用的,官方插件里没考虑的引擎内容,比如项目专有的特殊Actor的数据,那么此时就需要一些额外的开发了。

另一种不使用HoudiniEngine插件的方式是将输入输出数据都放在磁盘上,比如FarCry5:
在这里插入图片描述
将数据放在磁盘上有一些好处,假设所有数据都被转换为了引擎内容,那么当有其他环节需要这个数据时,就只能从引擎中拿。而如果数据在磁盘上,则可以不启动引擎而直接访问。但要注意的是,要保证磁盘上的数据和转换为引擎的内容相匹配。(比如Houdini输出的高度场数据,要和UE生成的地形相匹配,正常流程下当然会是匹配的,但要考虑有没有什么情况会不匹配并导致问题?)

所存储在磁盘上的文件可以是Houdini的数据格式(比如bgeo),也可以是通用的格式(比如png)。通用格式的好处是,其他工具想访问这个数据时不需要使用Houdini的接口。而Houidni格式的好处是,更方便Houdini读取且可附加些额外的信息。

P2. 如何调整生成流程?

这里的“生成流程”指的是:生成器如何得到它的输入,做完计算后如何将其结果转化为引擎内容,后续还需要做什么操作?

“生成流程”并不是算法的一部分。因为算法在不同的生成流程中是复用的:比如对于同一个对地形处理的算法,在某个流程中其输入可能是所有地形,而在某个流程中可能是一块局部的地形,比如对于同一个生成植被分布的算法,在不同流程中可能会输出不同的植被,并且后续所需要的处理也不一样。

“生成流程”也不是用户所要考虑的问题,因为其牵扯到很多数据与操作是工程方面的问题,并非艺术或设计上的问题。所以生成流程的调整一般是技术美术的职责。

生成流程的调整并不是个难以解决的问题,但是必须要开发某种形式,而且最好是易于调整的形式。一种常见的想法是用节点的形式来表示。(下截图来源UOD2021重生边缘分享)
在这里插入图片描述
与节点相对应的是“脚本”。脚本和节点之间当然也互有优缺点,这里只是简单列举:

节点 脚本
优:不容易出错 缺:容易失误
优:易于上手 缺:需要知道一定的语法才能上手
缺:屏幕利用率低 优:一页代码能表示更多的东西
缺:逻辑复杂时节点较乱 优:更利于表示复杂的逻辑
缺:二进制文件没法对比版本 优:可以对比版本间的修改

另外,如果有自动化流水线,需要提供接口让流水线可以自动执行。

P3. 用户如何触发生成计算?(界面)

如前所述,用户不需要考虑生成流程,他们只需要能触发生成并看到结果就行了。也就是说,最好能提供给用户一个容易使用的界面来触发。

而“触发生成”只是界面上一个最基础的功能。很多用户需要的功能都可以放在界面上。

P4. 用户如何调整生成器的参数/输入?

数值型的参数比较简单,只需要在界面上有面板即可。

不过,很多数值都有更具体的含义,最好根据这个进行优化。比如表示颜色的数值,就可以有颜色专用的面板来控制。再举例,如果一个参数代表的是一个资源,那么虽然你可以直接让用户填写资源的路径,但更合适的是提供面板让用户指定并且能预览到缩略图,这在UE中是很容易做到的,可以参考HoudiniEngine:
在这里插入图片描述

而另一种“参数”,或者说是“输入”,并非是数值能表示的。
在这里插入图片描述

例如:

  • 生成器生成河流时需要知道河流的走向,那么此时就需要一个曲线来表示了。
  • 生成器需要知道一个立方体的范围,此时虽然可以让用户填写立方体的位置和尺寸,但是很不直观,最好的方式还是要让用户直接在场景中调整那个立方体。
  • 生成器需要知道地形上的一个区域,此时就需要地形mask数据了,比如FarCry5中就做了Biome Painter。

在这里插入图片描述

这些曲线控件,mask工具或许不牵扯到复杂的技术,但是开发他们仍旧需要不小的工作量。

P5. 如何实现更复杂的操作控件?

随着对控制度的要求的不断提高,操作控件可能会变得越来越复杂。例如控制河流的曲线控件:

  • 最开始是一条曲线就够了。
  • 但是之后,可能想要控制宽度,这个宽度并非是整体一致的,而是会变化的,那么控件上就需要有能控制宽度的操作手柄了。
  • 在这之后,可能还会想要有分支。
  • 之后,还可能会想要控制河流对沿岸的影响范围。
  • 之后,还可能有重采样控制点的需求。

这样,河流的曲线控件会变得越来越复杂。其他的操作控件也会有类似的问题。

当然,这些都是可以开发的,而且需求也算是有限的。但就是工作量会较大且繁琐。比如在UE中就需要很多C++代码来实现。

P6. 如何区分生成内容和手工内容?

在【Q3】中,如果一种内容包含了“生成”与“手工”,那么就需要有方式能区分出来。

一方面,数据上需要区分。这意味着能通过某种规则明确地知道一个内容是手工的还是生成的。因为下一次生成时肯定不能影响手工的内容,而且也可能有些操作只针对于生成内容/手工内容。

另一方面,最好也让用户可以区分。这意味着界面上有某种工具能让人分辨出一个内容是手工的还是生成的。因为用户需要知道自己所维护的内容有哪些,而哪些是不需要自己关注且也不能修改的内容。

P7. 依赖关系过多?

一个生成计算依赖于另一个生成计算是一种很常见的现象,特别是在生成一个强调真实性的世界中。
(下图为FarCry5各个工具的依赖关系)
在这里插入图片描述
依赖关系越多,则上游的变动就会需要下游越多的重新生成。
所以依赖关系肯定越少越好,但是如果你生成的世界是强调真实模拟的,那么依赖关系可能是无法避免的。

此外,像FarCry5那样每晚跑一次全局更新的流水线也能缓解这个问题。因为当用户改了上游的内容后,不需要亲自跑一遍所有下游的计算,只需要等晚上全局计算就好了。

P8. 生成计算的时间太长?

生成计算时间过长,这可能是所有问题中最不好解决的问题。我想有很多原因或影响因素:

  • 如前所述,依赖过多。如果想确保生成是最新的结果,需要确保上游的输入都是最新,生成完还要确保下游都重新生成了。
  • 如果生成器是Houdini,那么输入输出的时间是个消耗。也需要调试下代码中是否有导致变慢的问题。
  • 算法本身太慢,这里还是要追踪时间消耗,Houdini中也有工具可以协助追踪。
  • 另外,如果能区分生成质量,则也可以一定程度上缓解问题。比如在预览,或者调节参数时,能以较低质量生成,但是最后输出结果时,以最高质量生成。
  • 但说到底,PCG这个方式本身而言,相比与手动方式也并不总能以更快方式达成目的。比如,对于一个河流面片,如果它99%的位置都没问题,只有一个地方的一个顶点需要稍微调整下。此时手动调一下会很快,但是如果要生成,那肯定要走一遍完整的河流生成流程。

P9. 生成结果存放在什么文件中,如何维护?

比如对于UE而言,生成的一些模型(如河流面片)会是一个单独的StaticMesh文件,但是有些数据是存放于关卡中的,比如地形和一些Actor。

而关卡中不仅有生成的数据,还会有些手工内容(手工摆放的一些模型,手刷的一些地形区域)。这样,关卡文件就同时包含了手工内容与生成内容,这会产生一些问题:

  • 考虑版本控制如P4,如果关卡被别人checkout,则关卡无法进行生成计算。
  • 生成的数据有破坏关卡中手工数据的可能性。特别是考虑流水线时,因为流水线生成结果后会直接提交,并未经过人工验证。

因此,最好将存放生成内容的文件与手工维护的文件分开。UE中也有支持这么做的机制(见One File Per Actor)。

P10. 后续的生成想要之前生成的中间数据?

生成计算的过程中会有很多“中间数据”,但是转化到引擎中的是最终结果,毕竟很多中间数据在运行时是没用的。(这情况类似3dsMax的“塌陷”,即删除过程的数据)

但有时,后续的计算需要之前的中间数据。例如:前一步对地形的处理时计算了一个表示某种含义的mask,而这种mask对于后续的植被生成有用。

大概有两种思路解决:

  • 重新生成一遍,缺点就是可能会比较慢。
  • 建立缓存,缺点就是需要建立机制维护这个缓存,保证其是最新的。

P11. 要提交文件过多用户分不清?

PCG生成可能会产生很多资源的修改需要用户提交,用户不一定能分得清。

解决方式可以有:

  • 保证生成的内容一定会在某个文件夹内。这样用户就明白它需要提交这个文件夹中的任何修改。
  • 通过逻辑自动判断各种情况下要提交什么文件。

P12. “接缝”?

当要生成的世界很大时,分区域进行计算是个常见的作法,但是这样遇到的一个问题就是“接缝”,即数据在与邻居相接部分的突变现象。

虽然“接缝”问题很常见,但是其原因并不相似,最常见的原因是计算这个区域时,有和邻居区域相关的计算,但由于邻居未加载,所以边缘的数据并不正确。

要想解决最好先分析出接缝产生的原因。而通用的思路例如:

  • 计算时加载所有邻居,但是最后并不上传邻居的计算结果。缺点就是比较慢
  • 计算时,用某种方式调整边缘的数据保证和邻居的边缘一致。缺点就是效果上可能不自然。
  • 先保留接缝,在之后有专门的步骤修复接缝。
  • 。。。

P13. 生成内容的性能?

程序化生成可以轻而易举生成性能超标的内容。因此需要格外关注生成的方式。比如:

  • 点云实体,对于树是可以的,但是像草这种密集的个体,显然个体数目会过于多。此时应该考虑其他方式,比如在UE中可以考虑LandscapeGrassType。
  • PCG可以生成很多贴花个体,但是可能会产生过多 Over Draw,此时可以考虑RVT进行优化(GhostRecon分享)
  • StaticMesh,要注意生成的面数密集度是否符合要求。还也要注意大小的拆分,比如河流面片,一般会根据关卡进行拆分,但可能还是较大。较大的StaticMesh对Culling不友好——你只要看到它的一个角落那么整个大StaticMesh都需要绘制,而过零碎的StaticMesh却会让DrawCall变多。

而且生成的内容量大时,人力是无法评估所有性能数据的,还是需要自动化的方式,比如生成性能热力图。

P14. 如何保留所有手工劳动?

依据和程序化生成的关系,关卡中的手工劳动分为几类:

  1. 完全和程序化生成无关的内容(比如手动摆放的建筑物),或者程序化生成的上游数据(比如手动摆放的石头,后续的生成计算需要输入他们)。这些内容无需做任何处理。
  2. 划分出的手动负责的内容。这些内容就按照划分规则处理。(比如植被生成的手动负责区域)
  3. 手动对程序化生成结果的修改。正如【Q4】所说,尽量不允许这种修改,如果真的允许,那就是需要想办法解决的问题。
  4. 依赖于程序化生成的结果。比如程序化生成了湖,美术师在湖底放了珊瑚;程序化生成了河,美术师在河边种了树。这种情况可以思考下美术师所作的工作,是否能由程序化生成。否则当湖改变,河改变,那么珊瑚和树的位置将不匹配湖与河,而程序化生成总能保证匹配。
  5. 但想“完美”保留所有手工劳动应该是不行的。对于一个有程序化生成内容的场景,你很难说美术师有哪一项劳动是完全和程序化生成内容无关的。举例,美术师摆放了一个观景台是因为这里的景色好,但是如果未来程序化让景色变化了,那么观景台的位置可能就不是最好的了,需要调整。这种情况大概没法避免。

P15. 如何更好的调试生成计算中的错误?

开发程序化生成的管线工具自然会有出错的情况,比如编辑器崩溃,或者生成错误的结果。

代码总会有出错的情况。可能是某个数据不符合写代码时的假设,代码没考虑某种特殊情况,规则有调整但是代码却没来得及调整。。。

但相较于其他问题,调试程序化生成里的错误较困难的是:生成的时间可能要很久,这导致复现时间会较慢。如果问题还不是稳定复现的,那么调试将会成为一种折磨。

因此需要考虑如何能更好地调试错误,比如:

  • 首先肯定是要尽量加快计算,但正如【P8】所讨论,这是相对困难的事情。
  • 构造简单的测试场景,比如简单的关卡,简化后的计算,等。
  • 在生成计算时插入一些log来获取更多的信息。
  • 等等。。

P16. 为非美术内容生成数据?

需要考虑,有可能会有非美术内容(比如Gameplay,音频等数据)需要借助PCG的力量或者要利用PCG生成的内容。

一种情况是,这些内容可以作为生成流程的后续操作,在生成原先的美术内容后再生成。

另一种情况是,这些内容的生成想使用PCG生成的数据作为输入。这时候正如【P1】所讨论,如果一些数据是通用格式而非Houdini格式,那么就会比较方便(因为其他内容的生成不需要使用Houdini接口)。

还有情况是,要专门为这些内容生成专用的数据结构。

P17. 流水线生成了错误结果?

正如【Q5】所讨论,流水线的一个缺点是,生成的结果没有人手动验证。那么就还是需要开发自动化的方式来验证。

对于生成错误数据的情况。正如【P9】所讨论,如果能将手动维护的文件和生成内容的文件分开,那么就会方便很多,就算生成了错误的数据只要再生成一遍就行了。但如果没有分开,还要考虑是否损坏了手动劳动。

参考资料

  • GDC2018分享:Procedural World Generation of ‘Far Cry 5’(我结合了PPT版对此做了很详细的翻译:Far Cry 5 的程序化世界生成)
  • GDC2017分享:‘Ghost Recon Wildlands’: Terrain Tools and Technology(我结合PPT版做了翻译:Ghost Recon Wildlands 中的地形工具与技术,但是由于口音问题,并没有听清视频中的所有内容)
  • GDC2019分享:Procedurally Crafting Manhattan for ‘Marvel’s Spider-Man’(也做了翻译:在‘漫威蜘蛛侠’中使用程序化的方式精心制作曼哈顿,不过对一些细节的翻译我并不自信。。)
  • Unreal Open Day 2021 《重生边缘》中的过程化内容生成是国内的一个分享,虽然内容基础,但涉及问题都是项目中很常见的。
  • 黑客帝国程序化生成:The Matrix Awakens: Generating a World

截图都来源于上面几个分享。

今天的文章程序化生成ta_游戏开发算法[通俗易懂]分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/85524.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注