
这是一个有关如何为Unity Asset Store编写插件,如何解决游戏中众所周知的等距问题,从中获利不菲的故事,以及如何理解Unity编辑器的可扩展性。 里面的图片,代码,图表和思想。
序言
所以,那天晚上我发现自己几乎无事可做。 在我的职业生涯中,来年并不十分乐观(尽管与个人生活不同,但这是一个完整的故事)。 无论如何,我有了这个主意,想为自己写一些有趣的东西,虽然很个人化,但我自己却有一点商业优势(当您的项目对其他人感兴趣时,我只是喜欢那种热情的感觉,除了为您的雇主)。 而且所有这些都与我一直期待着检查Unity编辑器扩展的可能性并查看其平台上是否有出售引擎自己的扩展的优点有关。
我有一天专门研究资产商店:模型,脚本,与各种服务的集成。 首先,似乎所有内容都已被编写和集成,甚至具有许多不同质量和细节级别的选择,以及价格和支持。 因此,我马上将其范围缩小到:
- 仅代码(毕竟,我是一名程序员)
- 仅2D(因为我只是喜欢2D,而且他们在Unity中对此提供了不错的现成支持)
然后我想起了我们之前制作的等距游戏,我吃了多少仙人掌,有多少只老鼠死了。 您将不会相信我们在寻找可行的解决方案上花了多少时间,并且在尝试整理和绘制等轴测图时我们已经破坏了多少本。 因此,我努力保持双手不动,我用不同的关键词和不太关键的词进行了搜索,除了一大堆等轴测艺术外,找不到任何东西,直到我最终决定从头开始制作一个等轴测插件。
设定目标
我首先需要简短地描述一下该插件应该解决的问题以及等距游戏开发人员将如何使用它。 因此,等距问题如下:
- 通过远程排序对象以正确绘制它们
- 在编辑器中创建,定位和移动等距对象的扩展
因此,在制定第一个版本的主要目标的基础上,我将自己定为第一个草案版本的2-3天期限。 因此,您不能因此而推迟,因为热情是一件脆弱的事情,如果您在开始的几天没有准备好东西,那么很有可能毁掉它。 新年假期并不长,即使在俄罗斯也是如此,我想在十天内发布第一版。
排序
简而言之,等轴测图是2D精灵尝试看起来像3D模型的尝试。 当然,这会导致许多问题。 最主要的是,必须按照绘制顺序对精灵进行排序,以避免相互重叠的麻烦。

在屏幕截图上,您可以看到首先绘制的是绿色精灵(2,1),然后是蓝色精灵(1,1)

屏幕截图显示了首先绘制蓝色精灵时的不正确排序
在这种简单的情况下,排序不会有什么问题,并且会有一些选项,例如:
- 按屏幕上Y的位置排序,即* (isoX + isoY) 0.5 + isoZ **
- 从最左边的等距网格单元从左到右,从上到下绘制[[3,3,(2,3),(3,2),(1,3),(2,2),(3, 1),...]
- 以及其他很多有趣但不是很有趣的方式
它们都非常好,快速且有效,但仅在这种单单元对象或沿isoZ方向延伸的列的情况下:)毕竟,我对更通用的解决方案感兴趣,该解决方案适用于沿一个坐标方向延伸的对象,甚至没有绝对宽度的“栅栏”,但在与所需高度相同的方向上延伸。

屏幕截图显示了对扩展对象3x1和1x3进行排序的正确方法,其中“栅栏”的大小为3x0和0x3
这就是我们的麻烦开始的地方,并将我们置于必须决定前进道路的地方:
我选择了第二个选项,因为它没有特别的愿望去处理每个对象的棘手处理,切割(甚至是自动)和逻辑的特殊方法。 为了记录,他们在《 Fallout 1》和《 Fallout 2》等少数著名游戏中使用了第一种方式。 如果您进入游戏的数据,则实际上可以看到这些带。
因此,第二个选项并不意味着任何排序标准。 这意味着没有可以用来对对象进行排序的预先计算的值。 如果您不相信我(我猜很多人从未使用过Isometry),那就拿一张纸画一个小物体,尺寸为2x8 ,例如2x2 。 如果以某种方式设法找出一个值来计算其深度和排序,则只需添加一个8x2对象,然后尝试将它们相对于彼此放置在不同的位置即可。
因此,没有这样的价值,但是我们仍然可以使用它们之间的依赖关系(大致来说,哪个是重叠的)进行拓扑排序 。 我们可以通过使用等距坐标在等距轴上的投影来计算对象的依赖性。

屏幕截图显示了蓝色立方体依赖于红色立方体

屏幕截图显示了绿色立方体依赖于蓝色立方体
用于确定两个轴依赖性的伪代码(与Z轴相同):
bool IsIsoObjectsDepends(IsoObject obj_a, IsoObject obj_b) { var obj_a_max_size = obj_a.position + obj_a.size; return obj_b.position.x < obj_a_max_size.x && obj_b.position.y < obj_a_max_size.y; }
通过这种方法,我们可以在所有对象之间建立依赖关系,并在它们之间递归传递并标记显示Z坐标。 该方法非常通用,最重要的是,它可行。 您可以在此处或此处阅读有关此算法的详细说明。 他们还在流行的Flash等距库( as3isolib )中使用这种方法。
一切都很棒,只是这种方法的时间复杂度是O(N ^ 2),因为我们必须将每个对象彼此比较以创建依赖关系。 我已经为以后的版本进行了优化,只添加了惰性重新排序,因此在进行某些操作之前不会进行任何排序。 因此,我们稍后将讨论优化。
编辑器扩展
从现在开始,我有以下目标:
- 对象的排序必须在编辑器中进行(不仅在游戏中)
- 必须有另一种Gizmos-Arrow(用于移动对象的箭头)
- 可选地,移动对象时将与图块对齐
- 瓷砖的大小将自动应用并在等距世界检查器中设置
- AABB对象是根据其等距尺寸绘制的
- 通过更改对象检查器中等距坐标的输出,我们可以更改对象在游戏世界中的位置
所有这些目标均已实现。 Unity确实确实允许显着扩展其编辑器。 您可以在对象检查器中添加新的选项卡,窗口,按钮,新字段。 如果需要,您甚至可以为所需类型的组件创建自定义检查器。 您还可以在编辑器窗口中输出其他信息(以我为例,在AABB对象上),也可以替换对象的标准移动小控件。 通过这个神奇的ExecuteInEditMode标记解决了在编辑器内部进行排序的问题,该标记允许在编辑器模式下运行对象的组件,即与游戏中的运行方式相同。
当然,所有这些工作并非没有各种困难和技巧,但是我没有花一个多小时来解决所有问题(Google,论坛和社区肯定可以帮助我解决所有问题出现,但文档中未提及)。

屏幕截图显示了我在等距世界中的运动对象的小控件
放行
因此,我准备好了第一个版本,并截图了。 我什至画了一个图标并写了描述。 时间到了 因此,我将名义价格定为5美元,将插件上载到商店中,然后等待Unity批准。 我并没有考虑太多价格,因为我还真的不想赚大钱。 我的目的是找出是否有一般需求,如果有,我想估计一下。 我也想帮助等距游戏的开发者,他们最终以某种方式最终被剥夺了机会和附加功能。
在5个相当痛苦的日子里(我花了相同的时间编写第一个版本,但是我知道自己在做什么,而没有进一步的思考和思考,与刚开始使用等距技术的人相比,这给了我更高的速度)我收到Unity的回应,说该插件已获批准,我已经可以在商店中看到它,以及零(到目前为止)的销售情况。 它在本地论坛上签到,在商店的插件页面中内置了Google Analytics(分析),并准备好等待草生长。
很快就进行了首次销售,就在论坛和商店上收到了反馈。 在1月12日的剩余日子里,我的插件已售出,我认为这是公众利益的标志,因此决定继续进行。
最佳化
因此,我对两件事感到不满意:
- 排序时间复杂度-O(N ^ 2)
- 有垃圾收集和一般性能的问题
演算法
有100个对象和O(N ^ 2),我需要进行10,000次迭代才能找到依赖关系,而且我还必须传递所有对象并标记显示Z进行排序。 应该有一些解决方案。 因此,我尝试了很多选择,无法不考虑这个问题。 无论如何,我不会告诉您我尝试过的所有方法,但是我将介绍到目前为止找到的最好的方法。
首先,当然,我们仅对可见对象进行排序。 这意味着我们经常需要知道自己的镜头。 如果有任何新对象,我们必须在排序过程中添加它,并且如果其中一个旧对象不见了,请忽略它。 现在,Unity不允许在场景树中确定对象的边界框及其子对象。 越过孩子(顺便说一句,每次都可以添加和删除孩子)是行不通的-太慢了。 我们也不能使用OnBecameVisible和其他事件,因为这些事件仅适用于父对象。 但是我们可以从必要的对象及其子对象中获取所有Renderer组件。 当然,这听起来不像是我们的最佳选择,但是我找不到其他方法,同样通用且性能可以接受。
List<Renderer> _tmpRenderers = new List<Renderer>(); bool IsIsoObjectVisible(IsoObject iso_object) { iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); for ( var i = 0; i < _tmpRenderers.Count; ++i ) { if ( _tmpRenderers[i].isVisible ) { return true; } } return false; }
使用GetComponentsInChildren函数有一个小技巧, 该函数允许在不需要的缓冲区中分配内存的情况下获取组件,这与另一个返回新的数组的组件不同
其次,我仍然必须对O(N ^ 2)做一些事情。 在停放我投影等距对象的显示空间中的简单二维网格之前,我尝试了多种空间拆分技术。 每个此类扇区都包含一个与之交叉的等轴测对象列表。 因此,想法很简单:如果对象的投影没有交叉,那么在对象之间建立依赖关系就毫无意义。 然后,我们跳过所有可见的对象,仅在必要的部分建立依赖关系,从而降低了算法的时间复杂度并提高了性能。 我们将每个扇区的大小计算为所有对象大小之间的平均值。 我发现结果不仅仅令人满意。
当然,我可以为此写一篇单独的文章...好吧,让我们尝试简短一下。 首先,我们要兑现这些组件(我们使用GetComponent来查找它们,这并不很快)。 我建议每个人在处理与Update有关的任何事情时都要当心。 您始终必须牢记每帧都会发生这种情况,因此必须非常小心。另外,请记住所有有趣的功能,例如custom == operator 。 有很多事情要记住,但是最后您会在内置探查器中了解其中的每一个。 它使记忆和使用它们变得更加容易:)
您还可以真正了解垃圾收集器的痛苦。 需要更高的性能? 然后,忘掉任何可以分配内存的东西,在C#中 (尤其是在旧的Mono编译器中),可以用任何方式完成,从foreach (!)到新兴的lambda,更不用说LINQ了 ,即使在最简单的情况下, LINQ也是禁止的。 最后,代替C#的语法糖,您将获得具有可笑功能的C外观。
在这里,我将提供一些可能对您有所帮助的主题的链接:
第一部分 , 第二部分 , 第三 部分 。
结果
我以前从未听说过有人使用这种优化技术,因此看到结果特别高兴。 如果在第一个版本中,游戏实际上需要50个移动对象将其转换为幻灯片显示,那么即使在一个框架中有800个对象时,它现在也可以正常工作:所有内容都以最快的速度旋转并重新排序,仅用于3-6毫秒,对于等轴测中的此数量的对象非常好。 而且,初始化之后,它几乎没有为一个帧分配内存:)
进一步的机会
阅读反馈和建议后,我在过去的版本中添加了一些功能。
2D / 3D混合
在等距游戏中混合2D和3D是一个有趣的机会,它可以最大程度地减少绘制不同的移动和旋转选项(例如,动画角色的3D模型)。 这并不是一件很难的事,但是需要在分拣系统中进行集成。 您需要做的是获得一个带有所有子级的模型的边界框 ,然后沿着显示Z沿框的宽度移动模型。
Bounds IsoObject3DBounds(IsoObject iso_object) { var bounds = new Bounds(); iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); if ( _tmpRenderers.Count > 0 ) { bounds = _tmpRenderers[0].bounds; for ( var i = 1; i < _tmpRenderers.Count; ++i ) { bounds.Encapsulate(_tmpRenderers[i].bounds); } } return bounds; }
这是如何获取带有所有子项的模型的边界框的示例

这就是完成后的样子
自定义等距设置
那是相对简单的。 我被要求设置等角,纵横比,瓷砖高度。 在经历了数学上的痛苦之后,您会得到以下信息:

物理学
在这里,它变得更加有趣。 由于等轴测图可模拟3D世界,因此物理学也应该是三维的,包括高度和所有要素。 我想出了这个有趣的把戏。 我为等距世界复制了物理学的所有组成部分,例如刚体 , 对撞机等。 根据这些描述和设置,我使用引擎本身和内置的PhysX制作了不可见的三维物理世界的副本。 之后,我将计算出的仿真数据用于等距世界的复制组件中。 然后,我执行相同的操作来模拟碰撞和触发事件。

工具集物理演示GIF
结语和结论
在实施了论坛上的所有建议之后,我决定将价格提高到40美元,因此它看起来不像是带有五行代码的另一个便宜的插件:)我将非常高兴回答问题并听您的建议。 因为这是我第一次在Habr上写东西,所以我欢迎各种批评,谢谢! 现在,我最后要保存的是该月的销售统计数据:
月份 | 5 $ | 40 $ |
---|
一月 | 12 | 0 |
二月 | 22 | 0 |
三月 | 17 | 0 |
四月 | 9 | 0 |
五月 | 9 | 0 |
六月 | 9 | 0 |
七月 | 7 | 4 |
八月 | 0 | 4 |
九月 | 0 | 5 |
Unity Asset Store页面链接: 等距2.5D工具集