天天飞车游戏引擎选型
游戏引擎已经成为目前游戏开发必不可少的工具,它所提供的便利性和稳定性在大大降低了游戏开发难度的同时,也大大提高了开发的速度和质量。我们可以把游戏的引擎比作赛车的引擎,大家知道,引擎是赛车的心脏,决定着赛车的性能和稳定性,赛车的速度、操纵感这些直接与车手相关的指标都是建立在引擎的基础上的。游戏也是如此,玩家所体验到的剧情、关卡、美工、音乐、操作等内容都是由游戏的引擎直接控制的,它扮演着中场发动机的角色,把游戏中的所有元素捆绑在一起,在后台指挥它们同时、有序地工作。简单地说,引擎就是“用于控制所有游戏功能的主程序,从计算碰撞、物理系统和物体的相对位置,到接受玩家的输入,以及按照正确的音量输出声音等等,它为游戏的运行提供各种各样的底层支持,创造出一个又一个奇妙的游戏世界。
经历了20多年的发展,游戏引擎已经日臻成熟。最早期的游戏开发者只关心如何提高游戏性,和如何将游戏卖给更多的人,此时开发游戏耗时长,并且有大量的重复劳动。渐渐的有一些聪明的“懒人”发现,他们可以借用上一个类似题材的游戏代码,缩短开发时间和成本,于是通过代码的复用,就产生了游戏引擎的雏形。从软件工程的角度说,将一些代码抽象成模块,可以大大提高复用性和稳定性,正是在机器化以及自动化目的的驱使下,游戏引擎诞生了。
从1992年诞生于《德军司令部》的Wolfenstein 3D引擎,到大名鼎鼎的Doom引擎,再到取材于《雷神之锤》的Quake引擎。这些前辈的大名对于如今的开发者来说依然如雷贯耳。新千年(2000年)之后,随着3D游戏的一步步发展壮大,更是出现了Unreal,CryEngine,寒霜引擎等引擎界的巨头。再到最近智能移动平台占据了人们的生活,又出现了活跃于移动平台的Unity3D和Cocos2D,游戏引擎已经发展成为一个大家族,其种类纷繁,不一而足。
开始选择引擎
“工欲善其事,必先利其器”,想要做出一个AAA级的游戏,选择一个好的引擎是关键(很多项目组都有自己的技术积累,会使用自研引擎,本文所说的引擎,都以商业引擎为准。下文提到的“引擎”,也特指商业引擎)。然而面对如果丰富的选项,当我们要进行新项目开发时,应该选择哪一款引擎才是最合适的呢?此时应该有同学说,那就选择最高级,功能最全面的引擎不就好了么?引擎的选择也不是越强大,功能越多越好,强大的功能是以更多的性能消耗(以及更高的授权费)为代价的。就像一台家用轿车装上了跑车的引擎,不仅发挥不出引擎的真正能力,还有可能算坏车子的其他部件;同样的,一个大型游戏选择了一个轻量型引擎,就像跑车装了普通的引擎,完全无法驱动车辆。
引擎的选择,关键还是要看游戏的类型和需求是否和引擎的功能特性相符合,在选择之前,让我门先了解一下游戏引擎的各个功能模块。
游戏引擎的功能模块
l 图形模块。
是游戏引擎最重要的模块之一,其主要功能就是封装底层渲染接口(D3D,OpenGL),并且在屏幕上绘制各种物体。图形(图像)将各种游戏逻辑可视化,并辅以各种特效、动画,形成丰富多彩的交互体验,这也是游戏之所以吸引人的原因之一。图形模块又可以细分为以下子系统:
1) 2D渲染,主要是对游戏界面(Menu,HUD)以及一些2D游戏元素的绘制、编辑功能,包括对不同分辨率的解决方案。
2) 3D渲染,主要是场景,地形,人物,物件等收到光源的影响并产生阴影的渲染过程,以及编辑功能。一般来说引擎还会封装对3D渲染的优化方案(如LOD,各种不同的rendering path)来供开发者选择。
高级的引擎还会预封装一些视觉效果(如动态模糊),以便游戏制作者在可以快速开发的同时,还可以方便地提高游戏的质量。
3) 特效的编辑和渲染。主要是指使用粒子系统来模拟各种视觉效果。粒子系统为提高游戏的逼真度助力良多。
4) 动画的渲染和编辑,包括各种不同种类的动画(如KeyFrame、Morph等),使得游戏中的物体能够按照预先编辑的轨迹进行运动。
5) 摄像机的控制。
6) 图形相关的编辑器及图形数据导入。
需要特别注意的是,有没有编辑器是衡量一个引擎优秀与否的重要指标。包含编辑器意味着引擎可以将程序,美术,策划的工作结合起来,即可以读取美术的输出产品(如3DMax,Maya或者PS的到处文件)和策划的输出产品(各种配置文件)作为程序运行的源数据。这条data pipeline的打通将会很大程度地减轻开发工作量,并且使得游戏开发的各方(程序,美术,策划)能够在同一个平台上进行工作,降低磨合成本。
编辑器同时还意味着将抽象的数据编辑过程可视化,使得美术与策划在一种“所见即所得”的环境中工作(例如Unreal的kismet和animation tree),这样他们可以不用关心具体的实现方式,只需要将注意力集中在他们所关心的游戏玩法,或者美术表现上,大大提高了开发效率。
l 物理模块。
主要负责游戏世界中各个物体之间的交互,令虚拟世界中的物体运动符合真实世界的物理定律,以使游戏更加富有真实感。它是一个逻辑模块,但是封装了复杂的碰撞算法,和图形一样,是游戏引擎的基础模块,也是最重要的模块之一。目前主流的商业引擎都会封装自己的物理模块,但是早起物理模块都是作为独立的物理引擎被集成到游戏中的(其实目前也有一些出色的物理引擎独立存在,如Bullet,PhysX等)。
物理模块的评判标准在于其效率和精度,以及所能得到的碰撞信息。效率无需多言,越快越好;碰撞精度指的是和发生碰撞的物体的实际表面越贴近越好(越贴近越真实);所能得到的碰撞信息越多,意味着可以针对碰撞结果做出更加真实的碰撞反馈的模拟。总而言之,在最短的时间内能够得到最精确的碰撞结果的为最优。但是这两个需求总是矛盾的,更精确的碰撞意味着更大的计算量,因此还是需要根据实际情况来决定使用何种物理引擎。
l 声音模块。
主要负责播放游戏中的各种背景音乐,音效等,和游戏图像完美契合的声效可以进一步提高游戏的逼真度。
看似简单的音乐播放模块,其实也暗藏许多玄机。随着游戏技术的发展,游戏玩家对于游戏中声效的要求也越来越高,更是有以音乐为主题的游戏(QQ炫舞,吉他英雄)。但是在一些封闭的硬件平台(如PlayStation,Xbox),或者是一些低端的Pc平台,手机平台上,能够分配给声音模块的硬件资源比较少。比如在内存比较稀缺的情况下,就需要对声音资源进行流读取(streaming)。一些平台对于声效的通道数(channel)也有限制,否则CPU负担过大会影响游戏帧率。
为了能够更加逼真的模拟现实世界中的声音表现,我们还需要对声波做出一些物理模拟。如为了模拟生源由远及近,我们需要添加声音的多普勒效应。为了模拟赛车引擎转速逐渐增高,我们需要逐步提升声音的音调(pitch)。
优秀的声音引擎能够将对声音的操作,封装出明确、清晰的接口(大部分效果都是通过操作音轨的参数来做出模拟),并且能够尽量少地占用硬件资源。
l 人工智能(AI)模块。
由于上述3个模块已经非常复杂,需要话费很多的时间精力才能完善,因此一般只有比较大型的,成熟的引擎才会专门封装自己的人工智能系统。
人工智能模块主要包括AI逻辑架构,AI逻辑编辑器,AI寻路等。
AI逻辑比较复杂,需要使用比较好的代码结构来管理,以免在逻辑膨胀之后无法维护和拓展。比较简单的逻辑通常使用有限状态机,更加架构化的做法是使用行为树(比如Unreal引擎)或者其他架构。
一些引擎也会为AI逻辑添加逻辑编辑器(比如Unreal的kismet),其目的是将游戏逻辑相关的工作独立出来交给策划去维护。但是这种做法的有效性也是因人而异,需要策划对游戏开发技术有比较深入的了解才能维护得好;而且这种做法提高了Debug的难度,因此如果引擎中包含着AI编辑器的话,也需要根据实际情况决定是否使用。
AI寻路是AI的重头戏之一(对于有自动寻路的游戏来说),场景中的NPC是否能够智能地找到最合适的道路前进,直接决定着游戏的拟真度。比较简单的寻路方式是通过路点(way point),场景中有许多组预先布好的路点,AI会根据优先级选择一条路线沿着一个个路点前进。这种方式不够灵活,在庞大的场景中,需要数量巨大的路点,同时NPC的行进线路也不平滑,因此适用于一些简单的AI。现下比较流行的,也比较有效的方式是使用导航网格(navigation mesh)。编辑器能够根据场景预先烘焙出和场景贴合的导航网格,游戏运行时AI通过一定的算法(例如A*算法)寻找到最短的路径平滑地到达目的地。如果场景中有一些会移动的障碍物,还需要实时跟新导航网格上的动态阻挡信息。
判断引擎的AI模块优秀与否,除了考察是否良好封装了上述功能点之外,也需要关注起性能开销是否适合当前平台,以及AI在游戏中的比重是否需要使用高级的算法,比如一个RPG可能就不需要使用自动寻路了。
l 网络模块。
由于当下流行的商业引擎都是来源于单机游戏,并且不同类型游戏的同步方式也不尽相同,因此不一定所有引擎都会封装网络模块。但是随着游戏网络化,以及玩家的游戏社交需求,网络模块中的重要性越来越大。
网络模块在封装socket的基础之上,负责客户端和服务器,或者是多个客户端之间的消息通信。对于多人在线游戏来说,除去基本的消息通信之外,如何保证各个客户端的表现一致(同步)是一个比较困难,并且非常重要的问题。
由于国内的网络状况良莠不齐,对于现下流行的移动平台来说,在非Wi-Fi环境下,移动网络的覆盖率以及移动网络的流量限制,如何在尽可能少的信息交互之下做出尽可能一致的同步行为,即如何降低玩家的流量消耗,也是很重要的课题之一。
网络同步相关的开发难点之一在于,其debug的难度较高。因为其错误表现不像在某处崩溃(crash)这么直接,其表现往往是多个客户端之间不同步,并且在观察到不同步现象之前(几十帧,甚至几秒钟)错误已经发生了,比较难抓到问题发生的现场;并且一个看不出明显表现的小错误也有可能积累成大错误(比如浮点数精度问题)。如果你正准备开发一个复杂同步的网络游戏(比如RTS类型的网游),寻找一套成熟的网络方案,或者是使用封装好类似同步方式的网络模块可以很大地降低开发成本。
l 跨平台性
在如今游戏平台百花齐放、百家争鸣的情况下,多支持一个平台,就意味着多占有一份市场,对于游戏开发者来说十分重要。
如果引擎本身没有跨平台的支持,想要将游戏从一个平台移植到另一个平台是十分复杂的事情。对于不同的平台,会有不同的硬件支持,特别是一些封闭的平台,比如IOS,甚至有自己的数据格式,以及开发语言。开发人员需要去重新将游戏的逻辑层和硬件底层连接起来。比较极端的情况下,可能会出现在平台A上使用了某技术,在平台B却不能复用的情况。随着硬件技术的成熟与发展,越来越多的智能硬件平台也如雨后春笋般涌现(如Google Glass),因此,跨平台性对于游戏开发团队来说非常重要。
l 脚本系统
一些引擎为了把底层部分和游戏的逻辑部分分开,并且给游戏开发着提供一个游戏逻辑的编写框架,对游戏的逻辑部分引入脚本引擎来支持。引擎只是提供了一个骨架,具体的内容还是要由游戏资源来决定。美术资源决定了游戏世界的外观,脚本资源决定了游戏世界的内容,而引擎部分决定了游戏世界的规则。和真实世界类比,游戏引擎定义了自然界的规则,美术资源和脚本资源定义了这个世界外观和内容,比如美国和中国拥有不同的资源,但是所处的自然规律是一样的。
游戏脚本最初来源于游戏的配置文件,后来渐渐发展到基于命令的脚本,结构化的脚本,面向对象的脚本等等,都是随着游戏内容的多样化发展而来的。它的一个普遍目的是把游戏引擎和游戏内容分开,这样就可以在不需要重复编译整个工程的情况下调整游戏内容,游戏引擎一般都是由c或者c++语言来编写,随着工程量的增加,编译速度是很慢的,而脚本的引入可以从某种程度上解决这个问题。
一些引擎会开发一种全新的脚本语言,使用全新的语法来定义逻辑,如Unreal的Unreal noxss,除此之外,也提供了可视化脚本kismet,虽然其使用上还不非常方便,但是在完成快速原型的过程中,策划可以在无程序的情况下独立实验,将会大大方便原型的制作。也有一些引擎引入了其他高层脚本语言,如lua,python等,其目的也是解耦逻辑和功能。
使用脚本语言开发,而不是对应某平台的开发语言,还有一个很重要的意义在于可以利用脚本语言来实现跨平台,而不需要将逻辑和底层实现耦合在一起。也呼应了解耦逻辑和功能这个主题。
脚本系统并不是一个必须的模块,如果引擎没有为脚本系统准备debuger的话,脚本内容的查错也会是比较困难的一个问题。
l Profiler
Profiler是指在开发过程中,针对内存以及性能的瓶颈进行针对性处理的工具。在一些封闭平台(如PlayStation,Xbox)上,对应的平台商都提供了针对自己平台的Profiler。PC平台更是有不同的CPU、GPU厂商针对自己硬件的优化工具。
但是引擎中如果封装了脚本系统,那么这些脚本的性能、内存消耗在平台的profiler上是没有办法很好的查看的。因此引擎往往会针对自己的脚本系统设计出对应的profiler,以方便开发者进行优化。
l 场景管理、输入接收(input)等模块
游戏引擎中往往会包括一些比较中上层的控制模块,比如场景管理系统,其意义在于给开发着提供一套成熟的,优化的结构来帮助开发者更好地控制游戏,降低开发的复杂度。同时其优化的解决方案也能够帮助提高游戏性能。
而输入接收系统,则是封装了平台的硬件输入响应(如果有跨平台特性的话,这里也会将不同的平台封装成统一的接口,使得游戏逻辑无需根据平台做出区别,得以统一)。
这些模块是一个成熟的引擎必须要包括的部分。
l 插件扩展模块
游戏引擎虽然发展了20年,但是毕竟没有成熟到可以掌控所有的状况,比如Unreal就擅长做3D的第一人称设计,RPG等,却不一定能够对格斗游戏有完美的支持。此时就需要引擎能够留出足够的接口以方便开发者能够根据自身情况制作相关插件来扩展引擎。包括但不仅限于拓展编辑器行为,丰富编辑器的元素,修改导出文件格式等。
支持扩展的另一个意义在于可以使用第三方开发者开发的插件来丰富引擎,例如Unity的在线商店,第三方开发者可以将自己的扩展插件挂在商店里,由其他开发者选购。NGUI就是Unity商店很成功的例子,它在提供成熟解决方案的同时,也是在很好的践行着网络众包的模式,使得游戏技术得以沉淀积累。
选择引擎的基本原则
在了解了引擎的各个模块之后,我们就可以开发分析如何根据实际情况选择引擎了。按照软件工程的开发流程,第一步应该是明确需求,游戏开发也不例外。
首先我们需要了解我们要做的是什么样的游戏,是RPG,还是FPS,还是RTS;我们是需要做2D游戏还是3D游戏,如果是2D游戏的话,其动画方式是序列帧动画还是2D骨骼动画,是否有3D的视觉需求(参考Scaleform);我们想要发布的平台是什么平台,是PC,还是手机移动平台,是否需要支持多个平台;游戏的主要玩法是什么,是以真实的物理模拟为基础(如重力滚球),还是以绚丽的画面为卖点,或是以需要深思熟虑的玩法为主导;
这些总结出来的基础特性将成为引擎选择的依据,由此来缩小选择范围。选择的原则总体而言,就是要考察引擎的特性是否和游戏的特性相符合。选择过程可以分为3步:
1) 考察必须满足的特性,如果引擎不能支持则不能选用该引擎:
l 引擎是否支持想要发布的平台,如果要跨平台,引擎是否支持。
l 引擎的内核消耗是否在平台硬件的承受范围之内,包括内存和性能消耗。即低性能平台需采用轻量型引擎。
l 引擎是否支持想要的渲染技术。
2) 考察针对不同游戏类型的开发需求,如果引擎不能支持,则可以考虑接入第三方库,或通过自主开发解决:
l 如果需要高级的AI,或者智能的寻路表现,引擎是否能否高效地支持。
l 如果需要精确的物理碰撞(如格斗游戏),引擎的物理系统是否能高效率地支持。
l 如果是以音乐为主题的游戏,引擎是否具有优秀的声音系统。
l 如果是多人在线游戏,引擎是否已经封装了成熟的同步机制。
l 如果是逻辑复杂度较高的游戏,其脚本系统是否支持OO编程。
3)还有一些指标,可以作为辅助参考依据:
n 引擎是否自带编辑器(包括在上述引擎模块介绍中提及到的各种编辑器)。
n 引擎是否支持拓展,包括编辑器拓展,与是否能方便地接入第三方库。
n 引擎是否有良好的文档,是否有开发商的技术支持。
n 引擎是否有辅助开发工具(如profiler或其他第三方工具等)。
n 引擎是否可以免费试用,其授权费是否在项目预算内。
天天飞车与U3D
以天天飞车项目为例,来说明之所以选择Unity 3D的原因。
总结一些天天飞车的项目特点,是移动端的极速闪避类游戏,平台覆盖IOS和Android,即需要跨平台。其渲染类型为3D并且没有特殊的渲染效果,动画类型为3D序列帧动画。其物理碰撞需求为3D的实时碰撞。声音需求为一首BGM加上少数的SFX。网络需求简单,只有简单的信息通信,没有实时网络同步的需求。AI行为简单。
同时考察手机平台上的各个引擎。首先从游戏渲染类型上就淘汰了2D类引擎(如Cocos2D)。3D引擎来说,有大名鼎鼎的UE3移动版(Unreal Engine 3),但是引擎本身非常庞大,使用门槛高(需要花费较长时间来学习),并且性能消耗过大,价格不菲,因此并不合适;还有一些其他国外引擎,国人使用研究的不多,如shiva3D、Marmalade等,也因不是很全面或者其使用经验不足没有被采纳。而近年主流的Unity,满足上述天飞项目的所有特点,并且拥有丰富的编辑器,良好的扩展支持,免费试用且授权费及其低廉,比较适合公司内面向普通用户的3D手游的开发。同时U3D已经经过多款手游大作的验证,如TempelRun系列,亡灵杀手:夏侯惇,并且有赛车类游戏,如疯狂漂移等的开发经验,因此也是天天飞车的选择。(这也是选择的重要原因,如果一款引擎被成功应用到同类型项目中,则我们可以很大程度相信引擎的能力),
U3D的优缺点以及使用经验
Unity引擎从2001年开始研发,到今天已经经过了14年的时间考验,发展成为了一个很成熟的引擎,在各个模块都有良好的封装。经过一段时间的使用,笔者也总结了一些U3D的优点:
l U3D最让人耳目一新的是,采用组件模式(Component)管理游戏逻辑脚本,提倡OO原则“多组合,少继承”, 使得开发者得以有一套很好的管理逻辑的机制。U3D采用高层OO语言C#作为脚本语言,再充分复用C#丰富类库的同时,也简化了开发者对于内存的管理。
l U3D具有功能丰富的编辑器,包括但不限于:粒子编辑器,动画编辑器,场景编辑器等,编辑器功能全面,并且开发者可以自定义编辑器项或者编辑器内容,扩展性良好。
l U3D数据导入选项丰富,在支持常用的美术数据格式的基础下,对于贴图能够根据平台进行多种格式的压缩,格式转换对于开发者来说完全透明化。
l 将渲染,声音,物理等模块也封装成组件模式,通过不同的参数多态化,使得开发者得以使用同一的接口进行操作,开发流程得到统一。这样做同时也使得这些主要模块对于开发者来说完全透明,开发者要渲染一个物体,只需要实例化一个GameObj并且附上渲染用的脚本(MeshRenderer)即可。
l U3D的功能完善,封装了引擎模块介绍章节中所述的几乎所有模块,功能全面。
l U3D具有良好的跨平台性,支持目前主流的平台(包括网页),并且覆盖部分家用机平台,未来也将覆盖更多的平台。
l U3D封装了优秀的资源包动态下载机制,这对于目前大热的移动端开发者来说不啻于福音,开发者可以方便地压缩,下载,解压缩资源包,有效地降低游戏安装包大小,并且实现动态更新资源。
l U3D封装了多条rendering path,并且包含目前流行的延迟渲染,开发者可以根据平台性能快速选择。U3D统合了DX和OpenGL,能够根据平台自主选择使用哪一个渲染中间层。同时U3D的shader使用shader lab封装CG语言,能够根据一套shader代码适应不同的硬件平台。
l U3D能够自动,静默,快速编译修改,开发效率高。
l U3D能够很好地捕捉到脚本层的异常,帮助开发者寻找问题。
但是对于一个引擎的特性来说,优点与缺点总是共存的,U3D为了适应更多的平台,添加更多的功能,就势必要产生一些负面的影响:
l U3D的内核消耗大。U3D虽然使用的是C#,确实Mono的运行环境,内核运行的起步价就比较高,启动时间长。对于中高端机没有明显的影响,但是在手机等相对低端的平台上,就比较尴尬了。同时Mono的调试环境比较差,开发效率较低。针对Mono调试环境的问题,天飞项目组适时引入了UnityVS插件,得以让Unity在Visual Studio的环境下进行开发,提高了不少开发效率。
l U3D的稳定性低,编辑器比较容易崩溃,Mono架构也导致有些值在Debug时不可见,增加了开发难度。
l U3D将脚本代码页当做数据来看待,因此要求脚本代码也需要和其他数据一起放在Assets目录下,相对较难使用不同的版本控制软件对数据和代码分别管理。
l U3D没有成熟的代码动态跟新机制,只能将代码封装成dll作为数据动态加载,但是这也会很大地提升开发的复杂度。
l U3D的物理世界的更新和逻辑世界的更新走的是两个时钟(可以在Time Manager里设置),导致有可能会出现两个世界出现不同步的情况。并且U3D的碰撞体的移动消耗比较大。针对这个问题,天飞项目引入了轻量的第三方物理库来代替U3D原有的物理体系,这样不仅能实时更新物理世界,同时也降低了更新产生的性能消耗。
l U3D的粒子系统内存消耗非常大,在手机平台上内存比较吃紧,会产生比较大的影响。针对这种情况,天飞项目在和美术沟通之后,采取尽量减少粒子系统的方式,而使用形变动画,贴图动画等方式进行特效的制作。
l U3D的骨骼动画的skinning计算是放在CPU上做的,在手机上支撑不起大量的骨骼动画运算。针对这种情况,天飞项目主要采取尽量避免添加骨骼动画,而是采用普通的位移,缩放,来制作动画。
l U3D的文档比较简单,对于一些API没有很好地介绍,天飞项目在开发过程中也是通过寻找他人的解决方案,或者不断试验总结来更多地了解引擎。
l U3D由于采用C#托管内存,开发者不容易控制内存的申请与释放,导致产生顿卡。针对这种情况,天飞项目主要通过减少更新时(update函数中)的动态内存分配,以及在游戏关键点(如切关时)及时GC来缓解。同时在托管内存的前提下,内存的profiler几乎不能提供有效的优化信息,于是我们通过游戏中调用U3D提供的资源查找函数统计出使用的资源,结合Android Debug Bridge(对于Android平台)从另一个角度了解内存的使用情况。
l U3D在加载资源时,并不会加载所有的资源,而是需要等到实际使用(渲染)时才正式加载如动画,模型等资源进内存,导致一些物体第一次出现时出现顿卡,针对这种情况,天飞项目在单局加载资源时先实际渲染该物体一帧的方式,将资源都预加载进来。
选择合适的引擎来开发新游戏
通过上面的描述,可以看出,选择一款引擎,首先需要了解它,考察它和需求的吻合度。然而总会有一些情况是在选择阶段考虑不到,而在之后的开发过程中导致踩坑,这时就需要引擎有良好的扩展性。其他成功项目的经验将会给引擎的使用带来很大地帮助,因此选择引擎时也许要考虑这款引擎的“业绩”。从天飞的项目经验来看,即使是像Unity这样经过10几年发展的成熟的商业引擎,也同样会有很多缺陷,一些问题(比如替换物理引擎)也给开发带来了许多不便,导致了许多重构。因此,选择一款适合平台和游戏的引擎来开发游戏,才能使得开发过程尽量平稳,顺畅,做出的产品也将更加能够赢得用户的欢迎。