Musoucrow' BLOG


  • Home

  • Categories

  • Archives

  • Tags

  • Search

Unity骨骼动画的总结

Posted on 2020-02-01 | In Teach | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  恰逢假期,在家继续推进Demo,骨骼动画相关的调研算是告一段落了,遂以本文记录相关要点。
  首先要明确一点,本文所说的骨骼动画皆是3D模型的骨骼动画,与2D精灵的骨骼动画无关,虽然原理大致相通。

网格、骨骼、绑定

  了解过3D相关知识的都知道,模型(Model)是由一个个三角形组成的,而这种三角形的学名则是网格(Mesh)。当然在DCC软件里为了方便创作,会用2个三角形组成四边形作为网格:
0
  然后便是骨骼(Skeleton)了,它是驱动模型运动的根本,如下图所示,这是一种彼此之间有父子关系连接在一起的长条状玩意:
1
  最后便是让模型跟着骨骼一起运动了,这个骨肉融合的过程称为绑定,具体要做的事便是将某节骨骼与相关的网格建立关系:
2
  如上图所示,模型上有着不同的颜色,这表示该节骨骼所影响到的网格权重值(蓝色为0,红色为1),所以绑定也俗称“刷权重”。权重值越高,该节骨骼对相应网格的影响便越大(存在多节骨骼对相同网格存在影响,此时便要通过权重值来决定优先级了)。
  随着时代的发展,现代DCC软件基本配备自动刷权重的功能了。做好模型部件的划分(每个部件拥有独立的骨骼,通过各骨骼之间建立关系来联系模型),减少每个模型的权重复杂度,如此通过自动刷权重基本可以应对一般情况了。

模型与动画

  生成给Unity使用的模型与动画我选择FBX格式,毕竟这算是最流行的3D格式了。对于动画,我选择一个动画一个FBX文件的形式(业界也有全部做到一个文件里,在Unity内分割的行为),动画FBX文件里只有骨骼与动画信息,不含模型。
  说到这里,便有一个绕不开的点:多个模型复用相同的动画,这里涉及到Unity里的两种骨骼动画模式:Generic与Humanoid。
  Generic如其名般:一般的动画,在这种动画模式下实现复用的思想很朴素:只要模型的骨骼与动画的骨骼要素相同,那么复用便是水到渠成的事了。这也表示必须同类模型与动画的骨骼结构是一致的。这也表示难以使用外界的第三方资源,对于一些以拼凑、同人、大乱斗为特色的民间项目,或是想直接使用某游戏提取出来的动画,那便捉急了。
  Humanoid则不然,这是一种专为人形设计的动画模式。如下图所示,它定义了人体通用的若干个关节点,将模型对应的骨骼填进去即可:
avatar
  事实上Humanoid便是做了一层中间层转换,让各自的人形模型的骨骼信息统一抽象为上图这套体系,并且还做了关节运动幅度的可控,如此便可实现复用了。且Unity实现了自动识别填充,使用起来还算方便。
  但事实上我放弃了这种做法,选择了Generic模式。原因如下:

  • Humanoid模式为了兼容不同体型下的情况,禁用了具有缩放行为的骨骼动画。
  • Humanoid模式只是为人形考虑,但实际上需要动画复用不只是人形。
  • 由于Demo模型选用的是小泥人,并没多少合适的第三方动画选择(已尝试过)。
  • 哪怕是人形,事实上也会有一些Humanoid无法顾及到的部件(如头发)。
  • 既然不用第三方动画,那么骨骼结构的稳定性自然有保证。

  以上原因不是说用Humanoid模式就完全无法解决,But simple is good,Generic就完事了。

Animator: 切换、分层、混合树

  时值2020年,Animator自然是动画组件的不二之选了。如下图所示般,构建动画状态机,实现一个有机的动画播放环境:
animator
  首先要注意的是,不推荐将动画状态机当成单位业务的状态机使用,虽然动画状态机有提供挂载脚本的形式,但事实上单位的状态并不是与某个动画绑死的,应该由状态去播放动画,而非是动画下绑定专门的业务。让动画的归动画,状态的归状态吧!
  在动画切换控制方面,我使用了Animator自带的变量机制(Parameters),结合条件切换、动画状态脚本、代码控制等方式,实现动画的高可控切换。
  如前文提到的动画复用问题,实际上哪怕骨骼主体相同,但仍会有各自的特殊部件,这时候便要用上Animator的分层机制(Layers)了。通过定义多个不同的层次,在层次中使用Avatar Mask确定影响的骨骼部位,在层次中定义各个动画状态下对应的部件动画。可以为层次定义独立的动画状态机体系,也可以在设置中选中Sync启用以主体层为准的体系。个人更推荐后者,除非与本体动画无关。
  最后是业界不少人士喜欢用的混合树(Blend Trees),分为多种类型(不同的维度乃至于机制),原理为定义若干个动画,确定每个动画在变量组合的特定值下权重最大(播放优先级最高),如此通过操纵变量即可灵活混合相关动画(每个动画的元素都有一定的权重,最终混合成独特的动画)。本人暂时还没用到,主要是追求更明确的动画,而非那种融合的感觉。

补间与帧动画

  骨骼动画的本质,便是在不同的时间点为某节骨骼定义了特定的位置、缩放、旋转。动画的运作便是根据两个时间点之间的骨骼数据做数值变化,这种行为称之为补间(Tweens),同理骨骼动画也就是一种补间动画。与补间动画相对应的概念是帧动画,帧动画只会在特定的时间点发生变化,时间点之间的运动途中是不变的,在许多经典的2D游戏动画便是这种做法。
  为何我会提及到这点呢?一个很明显的区别:较之帧动画,补间动画显得实在是太流畅了,毕竟理论上游戏运作的每一帧它都在改变。但是流畅不是很好么?这一直是电子游戏的追求才对啊!在大多数情况下也许没错,但有时太过流畅,反而会失去「力量感」,说的再通俗点,就是没2D游戏内味了。我想这也是不少一般3D动作游戏做的不好的一点。
  这种现象在日本动画业界运用3D时早有发现:由于2D手绘帧的标准是24帧每秒,而如果3D动画按照视频播放帧进行输出,就会显得两者仿佛根本不在一个世界般。故后来都选择了按照2D手绘帧的帧率进行抽帧,以此达到同步。当然哪怕如此,3D动画也是无法还原出2D手绘帧那股味的,一者在于手绘帧的每帧内容都是人为创作的,具有独特的节奏感。另者在于2D手绘帧的运动帧为了表达动感,往往会画成糊成一团的样子:
3
  这种效果在3D动画基本上是难以实现的,所以往往会盖一层特效解决:
4
  当然《塞尔达传说:荒野之息》对此的处理已经算是上乘了,特意选择了与武器本体颜色相近的特效,在形状上也与2D运动帧接近。个人认为可以在此基础上为武器加入短时间内夸张的形变,使之更有张力。这在《守望先锋》里也有相关运用,留待后日实践验证了。
  言归正传,鉴于补间动画过于流畅的特性,为此我也类似日本动画业界的做法一般,按照24帧每秒的形式对动画播放进行了抽帧。实现思想也很简单:平时将Animator暂停,使用一个定时器,在特定时间点让Animator一次性把暂停的时间差更新补上。如下图对照所示(图1抽帧,图2没有):
5
6
  当然这种方式并不完美:真正的2D帧动画每一帧的持续时间都是人为确定的,而这样只是粗暴的抽帧罢了。当然人为确定帧时间的方式势必带来更高的人力成本,具体如何仍需取舍。

后记

  关于「补间与帧动画」一节纯属个人看法,在正统3D派看来也许属于邪道也说不定(笑。骨骼动画涉及的相关种种实际远不止如此,如换装、部位组合动画等,限于篇幅,就不展开了。

Unity光照模式的总结

Posted on 2020-01-12 | In Teach | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  近日在琢磨Demo应该选择怎样的光照模式,遂做了个试验:对比在同一场合下,各种模式的情况。故以此文记录之(版本为2019.2、平台为Standalone、渲染管线为Builtin)。

环境光

  环境光(Ambient)严格来说并不是一种光照,它只是单纯的为所有显示元素上色罢了。可以理解为2D游戏便是有个(255, 255, 255)环境光。可于(Window → Rendering → Lighting Settings)下的Environment Lighting进行设置。
0
  环境光是无论如何都需要的,一般用于决定画面的底色。下图便是用了白色的效果:
1
  看到上图便能理解我说的「环境光严格来说并不是一种光照」,毕竟连影子都没有,整个画面显得很单薄。但事实上在早期、以及现在一些不依赖光影的游戏是有这种做法的。它们一般会采用类似2D游戏的做法,在素材层面解决各种显示效果问题。对于不依赖光影、强调美术的绝对控制的游戏,使用纯环境光是个方案。
  顺带一提,在Environment Lighting设置下的Gradient与Skybox模式有着不一样的效果,属于更高级的环境光实现。

实时光

  实时光(Realtime)顾名思义,就是每时每刻都在进行的光照。在Light组件的Mode属性设置为Realtime即是。实时光的优缺点很明显,如下:

  • 优点
    • 游戏时可随时改变光照的状态,即刻产生反应
    • 随取随用,无需烘焙
    • 光照效果最好
  • 缺点
    • 在正向渲染(Forward Rendering)下,画面同时出现多个光照时,开销较大
    • 为了节能,某些设备、设置下,光照的数量有限

  实时光一般就是开箱即用到的光照,效果如下:
2
  可以看得出,各项消耗指标都比纯环境光要高,而该场景只有三项光照(平行光1个、聚光灯2个)。故一般游戏都不会如此奢侈,会采用各种手段来达到相同的效果。
  而以上却还不是效果的极致,还差个全局光照(Global Illumination)呢。刚才所见的光照只是「直接的光照」罢了,它只会考虑到照到了谁便处理谁,没有从全局的角度去考虑。在开启全局光照后,除了直接光照之外,还会产生物件之间相互反射的间接光照。效果如下:
3
  从画面效果来看变得更为深邃了,墙壁与地板都有了反射后的光渍,而各项指标实际上与局部并无不同(疑似)。使用它的前提是要在上文的Lighting Settings下开启Realtime Global Illumination,并且为静态物件做好Static标记。具体实现细节请自行查阅官方文档,在此不表。
  当然,这并不代表全局光照优于局部光照,就比如有些游戏的画面风格并不喜欢那些全局光照带来的光渍。还是要看想要怎样的美术效果。

烘焙光

  烘焙光(Baked)可谓实时光的反面:根据光照信息预先渲染成贴图,最后盖到场景上。这个「根据光照信息预先渲染成贴图」的过程,是为烘焙。而烘焙的类型、算法、设置有着多样化的选择,直接影响烘焙的时长、效果、贴图大小与数量。也因烘焙的特性,只适用于静态物件(标记为Static的对象)。优缺点如下:

  • 优点
    • 部分渲染元素(取决于烘焙类型)没有实时运行的开销
    • 属于全局光照,拥有间接光
  • 缺点
    • 光照属性不能运行时修改
    • 动态物件不受影响
    • 烘焙耗时

  烘焙类型主要分三种,效果如下:
4Subtractive: 全烘焙
5Shadowmask: 烘焙阴影与间接光
6Backed Indirect: 只烘焙间接光

  可以看到效果是一个比一个好,但性能却是一个比一个耗。并且可以看出,由于烘焙设置的问题,效果是不如实时光的。而通过设置达到最优的话,烘焙时长则又是个问题了,鱼和熊掌不可兼得啊(砸钱便能我全都要)。
  对于Subtractive,只需把Light组件的Mode属性设置为Baked即可。对于其余两种,实际上是一种实时光+烘焙光的混合方案,则需设置为Mixed。由于动态对象不受烘焙光影响的特性,Subtractive下的胶囊仔直接跟纯环境光时一个样了。解决方案有很多种,如采用Mixed方案(静态物件烘焙光、动态物件实时光)、Light Probes等。
  顺带一提,关于Shadowmask,在阴影设置中可调为Distance Shadowmask。如此将取决于阴影距离的设置,在阴影距离内的阴影,将采用实时阴影,距离之外的则是烘焙阴影。也算是一种提升品质的方式吧。
  烘焙光在业界的应用相当广泛,其中Subtractive式烘焙在早期游戏与现代手游可谓家常便饭,妥善使用Light Probes也能达到不俗的效果。

后记

  以上只是本人粗略的实验与记录,实际上光照的内容浩如烟海,远非本篇所能涵盖。在光照方面本人也只能算是初学者,有所不对还请海涵,并欢迎指教。

2019年度总结

Posted on 2019-12-31 | In Talk | | Visitors

  与前两年不同,今年可没有“不知不觉中,一年又过去了”的感觉。也许是上班了之后,对日子有了概念吧(毕竟元旦放假,笑)。同时与前两年相同,特作总结,以为归纳。
  今年的Blog内容只有寥寥6篇,且全是DFQ相关。盖因10月之前都在紧张的开发中,实是无暇整点别的,希望明年有所不同吧。
  若论今年最大的成就,自然是DFQ的完成了。相关事宜也在后记有所总结。算是这些年来DNF同人的一张答卷吧。
  除此之外,自然是利用这些年的积累,成功的找到了一份符合预期的工作。虽称不上惊艳,但也不差。在找工作期间也受到了不少业界同仁朋友的帮助,在此感谢。工作之后的最大好处便是生活作息规律了不少,至少不存在通宵了。其次是袋里也算有俩破钱,吃点买点都还凑合。在工作上也见识到不少业界才有的东西,并且深刻感受到了屎山、祖传代码、各种对接联调需求扯皮的滋味。也逐渐理解业界不少游戏明明肉眼可见的可调优项,却事实上没去改的现实。
  主要原因在于,团队人数越多,意志的贯彻性就会越差。并且由于人多,野心也就愈大,需要做的杂七杂八也就愈多,不类独立游戏会专注于某项去做好。并且由于团队传承的关系,不免会存在换皮、祖传、屎山代码的现象。亦有可能熟悉这套玩意的核心成员已经不在而导致整体水平下滑,且由于规划、时间问题,没有成本去重构、推翻重做,这也是很无奈的事实。故工作后我最大的两点感受是:

  • 自己能白手写出某套东西并不算什么,能基于别人的东西去改出来那才是本事。
  • 团队人愈多,工作流程(工具链、生产线、规章制度)就愈重要,这决定了团队生产力释放的多寡。并且人类之间的信息交接力其实很差,人多就不免要付出对接成本,这也是要通过工作流程去尽量减少的成本之一。

  说完工作的事,介于「生命不息,整活不止」的原则,新的整活计划自然如火如荼的进行中。如去年所言,需要在3D游戏开发方面发力。结果今年都在忙DFQ,有点惭愧。以上便是本人的2019年度总结,且待明年的Blog吧。
  无双草泥马
  2019.12.31

《DFQ》开发随录——后记

Posted on 2019-10-17 | In Development | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  DFQ终究是顺利的开发完成,并在发布之后的短短四日内达到接近十万的下载量。赢得广大玩家的好评,甚者认为比之官方的手游品质更佳。然而这一切恍若黄粱一梦,于昨日晚上收到了来自腾讯的律师函,要求删除相关下载渠道与宣传内容。
  取得这等成绩自是喜人,能被腾讯警惕也证明品质确实出色。只可惜游戏尚有一些待完善之处,无法再为玩家提供更新了。在此期间也有不少请求换皮合作的邀请,但本人志不在此。也不想讨论相关太多,就来为这个项目做个总结吧。

程序

  关于制作DFQ的想法,在《阿拉德英雄传》3.0搁置以后便有了模糊的想法。认为要做一款简洁爽快的中小型DNF同人手游,作为我、以及开元的DNF同人最终章。但随着增长了见识以后,便发现自己的程序架构掌控力不足,于是花了不少时间恶补。且前期对于要做一款怎样的游戏并无清晰的概念,遂直到2018年6月才正式开始制作。
  由于DFQ算是弥补3.0搁置的一个执念,于是依旧选择了LÖVE引擎。事实上这是个重大的决策失误。LÖVE关于移动端方面的功能只是刚刚推出,且是冷门引擎,没人帮忙踩坑。于是在制作时便遇到了很多问题,包括相关库的缺少或不合适以至于要造轮子、引擎本身的BUG之类等等。在发布之后更是遇到了诸多问题(NO GAME、全面屏、DPI等),在此奉劝各位,不要用冷门引擎。
  说回程序本身,这次基本上是达到我的要求了,至少工程不再是一塌糊涂、难以协作的状态了。对于各种方面的写法问题也有过深刻的研究(如活动对象采用ECS架构),对于代码格式与写法也有了规范。不足之处在于状态的业务按理来说应该配置化,而非OOP,因其存在大量相似而不同的业务,而这些部分是难以用OOP解决的。以及在OOP的规范上,我引入了private(以_前缀表示),然而会发现很多时候这只会增加不必要的思考负担以及封装成本,这方面该如何处理其实至今我仍未有答案(在业界也是众说纷纭)。
  为了弥补Lua没有强类型与智能提示的缺点,我引入了EmmyLua。所以看过之前的开源工程的会发现代码会有形如---@class XXX的注释,不过这种外挂的形式终究不如原生来的爽快,且缺乏运行前检查的功能。对于代码健壮性的建设尚嫌不足,由此可见TypeScript才是究极的工程化脚本啊。
  在后续测试的图中遇到过一次严重的内存泄漏事故,基本上刷到第三个地下城时便会内存膨胀到难以接受的地步。通过使用LuaMemorySnapshotDump发现,由于我为了减少内存申请,会将一些固定的table类参数写成模块变量,而使用后却没有及时清空,导致游戏对象一直依附其中,无法回收。以及为了优化而做的对象池也是如此,真是汗颜。

美术

  美术方面可以说的不多,首先是由于因需求做了个粒子编辑器,导致对粒子效果的掌控力上升。游戏的粒子特效使用率较之《阿拉德英雄传》有了大幅的提升。
  在人物素材方面,实现了非常强大的纸娃娃,并且为了效率优化,纸娃娃是拼合成一张大图的,缺点在于合成相当耗时,需要在恰当的时机进行(如读图)。并且部分设备支持的图片大小最高为4096,而实际上则出现了超过该大小的图片(此问题至今尚未修复)。
  在优化方面,由于引入了图集与压缩的措施,游戏的加载速度与显存占用是得到了极大的提升。在尚未采取压缩之前,小米9会出现显存带宽爆炸的情况。可以说安卓设备五花八门,令人十分头大。
  UI方面,立绘依旧是小山龙同志的作品,质量较之当年可谓进步明显,原本打算采取live2d的方案,却因工期延宕而搁置了,实是一憾。至于界面本身,可以看出对于手游而言,连及格都称得上是勉强,实是因为一版过兼无经验所致。
  手游UI与端游UI的不同处在于,由于屏幕太小,实际上UI要做的很大才显得正常。并且由于有着触控的需求,按钮也必须大且位置合适。如果为了美术效果而做得小,那也得必须要求附近无其他冲突项,且实际可触控范围要比素材看起来大才行。

游戏性

  DFQ的原初想法实是一款刷刷刷的类暗黑游戏,但本人感觉这年头的游戏都过度强调养成了。遂在砍掉养成的前提下重新审视,定下了三国战纪+Roguelike的主基调。三国战纪的元素在于:角色扮演、街机闯关、拾取道具,Roguelike则是随机性了,可谓独立游戏时下的流行元素,当然大家不约而同选择Roguelike的原因很简单:游戏内容不足,所以要用随机性来增添耐玩性。这么一组合起来,DFQ的样子便很明显了:开局一把刀,一切全靠打,随机地图事件,横版街机闯关。
  但是仅仅如此还是不够的,要是玩家一直用着一成不变的搭配进行游戏,那可就太浪费了。传统游戏对于此的解决方案有:上下级替换、针对性关卡替换,然DFQ没有条件搞上下级替换,针对性替换也未免苛刻。于是抄袭《塞尔达传说:荒野之息》的武器耐久度设定,让武器、道具、技能都具有轮替性。这份轮替性对于玩家而言实是辛辣了,以至于赢得不少吐槽。在后续版本中便为此做出了调整优化,但实是最佳之法。关键在于:玩家需要自己感到可控的设计,而非稀里糊涂的机制。DFQ在不少方面都有类似问题(如最初设想的动态难度),实该检讨。
  在关卡设计上,DFQ是以传统游戏的标准进行设计的,做出了不少较之DNF的突破(如跨房间追击战、迷宫等)。在开发前期犯下一个严重的错误:以端游的标准去做手游,以至于难度过大。手游的操作成本比端游要大,以及更适合轻度的体验。传统的DNF操作方式并不够爽快,遂在此基础上加入了小技能可互相强制、普攻可减少技能冷却的设计,让整个游戏的体验如打了肾上腺素一般。
  除此之外,在做《阿拉德英雄传》多年以来,落下不少错误的思想,如怪物的攻击频率相当鬼畜、没有合适的前后摇等。虽然较之当年,我的ACT设计理解有所提升,引入了反击元素,但仍是不足。在小草泥马的一番军训后,引入了紧张时间与舒适时间的概念,领悟了张弛有度的道理。怪物的设计上会做出提供玩家利用的破绽,让战斗的体验达到了业界平均水平(指国际)。

宣传

  在此感谢轻声低语、瓦伦、夕阳飘羽三位同仁的协助,在DNF百度贴吧、COLG等处得到了极大的曝光,也感谢纷纷去安利、自发宣传的玩家们,以此得到了广泛关注。取得了下载量近十万、B站宣传片十三万播放、贴吧关注量近七千、群人数两千七、COLG帖子47页的成绩。
  另外意想不到的是,以前《阿拉德英雄传》的玩家群体的活跃度也很高,并且开元工作室的名声也比想象中要大(不少人都知道,包括DNF吧的吧务),可见以前做下的成果也是有回报的。
  其实在我看来,比较核心向的游戏,只要你知道受众的聚集地,并且有相关号召力的人士愿意帮忙。在赢得了核心玩家的认可下,宣传效果是非常棒的,哪怕远在韩国的好船同志也收到消息并通关了。这点和业界从广泛人群中捞玩家的买量游戏还是有所不同的。

后记

  其实DFQ的成功,除了本身品质的优秀之外,更在于官方手游迟迟未发,当然最近又有动作了,这也算是撞枪口上了吧。且DFQ属于手游里少有的偏传统的单机类型,在玩腻了抽卡养成流水关卡的玩家看来,自然是十分闪耀的。而我之所以拒绝那些合作换皮的邀请,也在于我并没有想过符合游戏的商业化改造方案,如果按照市面上的那一套进行,结合下一次的天时地利人和,感觉迎来的只会是暴死吧。
  经过这次的验证,可以见到我的ACT理解程度已经进入了新的台阶,然而这还不是我所能做到的极限,若有机会一定会再次整活。当然同人游戏是不会再做了,这次已经是撞倒了天花板,在各方面有其局限性。

《DFQ》开发随录——AI

Posted on 2019-05-31 | In Development | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  DFQ是PVE(Player VS Environment)游戏,那么自然要有AI了。DFQ的AI实现并不算复杂,并没有用到时下流行的行为树(Behavior Tree)。原因在于不需要做到太精细的操作,且AI部分的业务不需要执行策划参与(不需要编辑器化)。本文将一一叙述其中要点,相信对于同类型的游戏也有一定的参考价值。

宗旨

  AI设计的宗旨只有一个:根据游戏的各种情况,决定进行怎样的操作。这样的思路其实挺仿生的,我们玩游戏的时候本质上也是如此。要注意的是,玩家进行操作的方式是通过外设(键盘鼠标手柄触控等)输入操作信息(按键坐标等)。从这点来说,AI也可以这么做,以进行虚拟的输入操作。这么做的好处是很明显的,如此AI与玩家在功能上达到了一致,AI也不需要与某个具体的功能耦合,只需要关注相应的操作指令,无需关注具体的功能实现。
  而这一切只需要做一套操作模块即可,并且做联机时来自其他玩家的输入亦可如此处理,通过这种方式达到了玩家、AI、联机三者的有机统一。可谓「软件开发中遇到的所有问题,都可以通过增加一层抽象而得以解决」的一次实践。
  当然这也会引入新的问题:某些功能只想AI拥有,那该怎么办?DFQ中的非转向移动便属于这类,解决方法很简单:设计一个玩家无法触发的操作指令即可。

对象

  AI的实体存在就是个类对象(下文称AI对象)而已,它会被外部调用的基础函数只有两个:Update和Tick。Update用于处理持续性的业务,而Tick则是一次性的业务,当然两者可能会有所结合(Tick接收参数,以驱动Update的运作)。
  AI对象主要会存在的场合有:常驻(移动和攻击)、技能(判断是否应发动)、状态(某些状态下需要后续的操作),通过配置化的方式,即可灵活的组合需要的AI了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
return {
script = "base",
state = "attack",
ai = {
script = "battleJudge",
collider = "duelist/goblin/skin/3-attack"
},
attackValues = {
{
damageRate = 1,
isPhysical = true
}
}
}

  如上配置所示,这是个技能的配置。在AI部分选择了battleJudge类,并提供了用于判断范围的collider参数。如此便定下了该技能的AI方针:使用collider判断是否存在敌人,存在则按下技能对应的按键,以发动技能。

实现

  说完基本构成后,再来说说一些具体AI业务的实现吧。

  • 移动:移动AI的核心构成有三
    • 获取目标:遍历符合条件的对象,涉及到阵营等因素。
    • 寻路:以目标为终点展开的寻路,由于DFQ使用的是网格地图,所以使用A星之类的寻路算法即可。
    • 输入操作:获取到移动路径后,通过发出输入指令以驱动角色以之移动。要注意的是,这种方式不可能做到完全贴合路径,所以出现了超过了路径点的情况也不会作处理。
  • 攻击:攻击AI要做的事情很单纯,遍历技能以Tick它们的AI对象进行发动而已。要注意的是,技能的使用顺序要建立优先级进行排序。
  • 判定:这个判定,便是上文的battleJudge了,通过collider以判断目标是否存在。这里的collider便是先前打击感所言的立体矩形,如下图所示:
    collider

  以上便是DFQ里值得一提的AI业务。顺带一提的是,AI是典型的不需要立即生效的业务,所以可以考虑每帧只执行一个单位的AI业务,以此减缓性能压力,并且避免敌人一窝蜂展开攻击的现象。

后记

  在本文开篇时,输入AI二字的我其实有点恍惚。短短数年,AI几乎成了深度学习/机器学习的代名词了。在游戏领域一个理所当然的缩写反倒让我踌躇了一瞬,真是唏嘘啊。也许以后的游戏AI真的都成了基于强化学习的实现也说不定呢(笑。

《DFQ》开发随录——打击感

Posted on 2019-04-30 | In Development | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  对于动作游戏(本文所谓的动作游戏具体指由FTG衍生而来的「超人系ACT」,如鬼泣、猎天使魔女等)而言,打击感自然是重中之重,本篇就来讲讲DFQ的打击感实现思路吧。
  首先要明确打击感的定义,本人将之定义为:攻击命中时产生的反馈。就这点而言,打击感并非是动作游戏的专属,凡是涉及到攻击交互的游戏都有。而动作游戏与之不同在于会对在攻击中附加控制效果(击退、击飞等),使得动作游戏成为了围绕打击感展开操作的游戏。
  而DFQ的打击感基本源于DNF,在此基础上加入个人的理解,接下来便一一讲解其中构成。

判定

  在产生攻击命中之前,自然得讨论如何触发了。众所周知,动作游戏讲究「所见即所得」——必须是看上去击中了,才算是命中。所以自然要使用一些方式去近似地模拟素材的边界范围,以此进行碰撞判定。而DFQ的做法则相当粗暴——直接构建一些近似的矩形,当然这矩形却不一般:
collider
  如图所示,人物拥有两种颜色的矩形,其中白色矩形表示人物的X-Z轴矩形,红色矩形表示为X-Y轴矩形。与一般的无纵深横版游戏(冒险岛、胧村正等)不同,DFQ这类可进行上下移动且滞空的横版游戏(DNF、三国战纪等)会去构造一种「逻辑上的三维空间」:
coordinate
  当然实际上可以直接使用3D矩形(立方体)进行构造,只是2D图形下不便表示,于是分解为两个矩形进行。自然地,判定时也是红对红、白对白。至于判定算法,由于DFQ没有太精细的需求,矩形不会参与旋转变换,故直接使用AABB即可。

击退

  话不多说,直接上图:
stun
  击退是打击感中最基本的元素了,当然将之命名为击退只是个人行为,在业界它有着各种各样的称呼,如stun、硬直、僵直、気絶等,在此一提。
  由图可见击退有两种不同的姿势(为了表现的丰富度),敌人会保持被击姿势一段时间,且变速位移一段距离。以函数的形式表示便是stun(time, speed, acceleration),time表示保持姿势的时间,speed为位移的初始速度,acceleration为速度的衰减值(也可以是加速值),通过acceleration来每帧减少speed,以此实现简单的变速运动效果。当然speed与acceleration并非是必须的(不带位移的击退),但time则必须有(无time不stun)。

击飞

  同样,直接上图:
flight1
  与击退同样,击飞也属于动作游戏里最核心的控制效果之一,它的别名也很多,如flight、击倒、倒地、浮空等。
  击飞这种控制效果在最初只是作为一种动画表现手法而已,一般用于敌人死亡、某些想表达击飞的招式等。敌人处于击飞动画时一般无法或难以继续进行互动。将之发展的据说是CAPCOM开发《鬼武者》时触发的一个BUG——敌人处于击飞时被后续攻击而产生了滞空效应。从此一发不可收拾,铸就日后《鬼泣》皇牌空战之名。
  而DFQ身为2019年的游戏,自然不可能落后:
flight2
  以上两张图基本可以窥得击飞之全貌了:

  • 击飞在状态上可分为上升、下落、倒地,上升与下落都会进行类似击退的变速运动,且根据进度改变姿势。
  • 姿势内容为击退的两个为基础外加它们的90度旋转版本及倒地。
  • 在击飞时被攻击会切换姿势且保持滞空一小会,形成了浮空连击的效果。
  • 除此以外便是X轴的位移效果了,这点与击退一致。
  • 倒地会根据浮空高度结算出「再击飞」,画面表现上为落地弹起,若高度不足则直接倒地。

  与击退类似,击飞的基本函数形式则是flight(speed_z, speed_x, acceleration_z_up, acceleration_z_down, acceleration_x),参数含义与击退类似,不再阐述。一般而言acceleration_z_up与acceleration_z_down会选择默认值,speed_x与acceleration_x则为可选项,但speed_z必须有(无speed_z不flight)。

特效

  继续上图:
effect
  特效其实没什么好说的,如果说动作姿势是描线,那么特效便是上色了。需要注意的是特效出现的位置一般得是矩形碰撞的交点处,这样才有「打中这个位置」的感觉。
  特效的种类一般就是斩、打、突、气四类(利器、钝器、锐器、魔法),外加出血之类等等,多多益善。
  顺带一提的是,由于特效算是一种创建销毁十分频繁的对象,值得为之做对象池以减少创建销毁的开销。

声音

  这下上不了图了,毕竟声音的可视化形式一般人类都看不懂(笑。
  由于打击感是攻击命中时产生的反馈,而反馈的形式自然不局限于视觉上的,听觉也相当的重要。当然这里讨论的声音可不仅仅是攻击瞬间产生的部分,还包括了整个招式过程。
  一般而言,一个招式基本会包含以下元素:

  • voice:如人物发招时的叫声,播放时机不限。
  • swing:如人物挥剑的声音、特效产生的声音,一般于运动帧时播放。
  • hitting:如刀砍到身上的声音,于命中时播放。
  • damage:敌人被攻击的惨叫声,于命中时播放。

  声音这部分在业界不少垃圾游戏可谓是偷工减料的重灾区,实际上万万不可忽视,毕竟有时候效果好不好就靠听个响(代表:拳皇)。

Hitstop、闪烁、抖动

  这仨放一块讨论是因为他们相辅相成:
battle
  Hitstop这玩意我对其没有准确的中文词汇,业界一般称为硬直、僵直、卡肉等(可见多容易与击退混淆)。其定义如其名般:因hit而导致stop。表现形式为人物停止运动一段时间,这里的运动包括位移、动画之类。Hitstop是敌我双方皆有的,我称本体的Hitstop为Selfstop,敌方为Hitstop。由此可见,卡肉这个说法其实很恰当,感受起来就像是一下刀卡到肉里了。一般Hitstop的高低可以用于表示攻击的轻重,以及像内功拳法、一闪刀法等延迟杀伤效果也可以通过高Hitstop达成。
  至于闪烁就更直白了,就是敌人表面有一层纯色遮罩渐变消失。值得注意的是,闪烁的运作也会受Hitstop影响,故在Hitstop期间闪烁是保持初始状态的,看起来敌人就是蒙上了一层白色。这么做可以使得命中的效果更为明显,在业界中《王者荣耀》也采用了这样的表现手法。
  抖动在图中也许看不太出来,主要就是设定个时间值以及抖动范围(xa, xb, ya, yb),人物在时间内就会随机位移,形成抖动的效果。与闪烁同样,在Hitstop时间是不会流动的,所以抖动与闪烁类似,基本上便是用于加强Hitstop的表现力。在业界中CAPCOM的《吞食天地》也采用了这样的表现手法。

后记

  以上只是打击感的一些机械的构成,实际上要做好打击感得充分利用许多元素,如场景震动、运镜、符合节奏的连击等。实际上它是一门导演的学问,要想培养就只能多抄多想多做。限于素材与平台,未能表现更丰富的元素,只能有待日后进军3D再说了。

《DFQ》开发随录——随机掉落

Posted on 2019-03-31 | In Development | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  随机掉落可谓时下RPG的流行设定,DFQ自然也不例外。而掉落业务自然也有其值得细说之处,不然也就不会有本文了(笑)。接下来将一步步引申出随机掉落的实现演进。

粗劣的实现

  在以往的开发生涯中,对于掉落业务,我采取了很粗劣的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
local random = math.random() -- 0-1
local pool -- drop pool
if (random < 0.1) then
pool = pools.normal
elseif (random < 0.3) then
pool = pools.rare
else
pool = pools.other
end
local index = math.random(1, #pool) -- Choice one.
local item = pool[index] -- Get an item.

  这种实现的槽点可谓数不胜数:掉落池的选取可谓暴力代码,而池中的道具也只能通过塞入相同的多份来扩充概率,对于概率的控制度很生硬。哪怕是将掉落池采取与道具相同的做法(将pools做成list)以去除暴力代码,对于概率控制度的问题依旧没有解决。且进行了两次取随机数,从概率而言并不纯粹。实际效果而言也导致了经常重复掉落,并不可取。

Alias Method

  那么如果选择将多个掉落池合而为一,使之只有一个list呢?
  如此确实能让概率纯粹了,但是对于道具概率的控制度依然很差。这个问题可以通过构建道具概率表({a = 0.1, b = 0.5, ...})以生成掉落池({a, b, b, b,...})解决。但这样生成的掉落池未免也太大了(最后可能会达上千个元素),这太不环保了,那怎么办呢?
  长达廿二年的人生经验告诉我:我们做的绝大多数事情都是前人做过的,遇到不会的问题看看前人是怎么做的就对了。果不其然,这就遇上了个合适的算法:Alias Method。
  本文并不打算详解其中的奥妙,这是愚蠢的复读机行为。直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- items: A list of probability of item.
function Alias(items)
local len = #items
local alias = {}
local probs = {}
local small = {}
local large = {}
for n=1, len do
items[n] = items[n] * len
local tab = items[n] < 1 and small or large
table.insert(tab, n)
end
while (#small > 0 and #large > 0) do
local less = table.pop(small) -- Remove the first element of list and return it.
local more = table.pop(large)
probs[less] = items[less]
alias[less] = more
items[more] = items[more] - (1 - items[less])
local tab = items[more] < 1 and small or large
table.insert(tab, more)
end
while (#small > 0) do
probs[table.pop(small)] = 1
end
while (#large > 0) do
probs[table.pop(large)] = 1
end
return alias, probs
end

  算法的代码量并不多,也就三十多行,输入参数items为道具的的概率list({0.1, 0.1, 0.5, ...}),即代表需要配套的paths来表示对应的道具标识({"stone", "potion", "gold", ...})。至于返回值alias, probs,先来看看获取随机掉落的代码:

1
2
3
local index = math.random(1, #paths) --- 1-n
index = math.random() < probs[index] and index or alias[index]
local path = paths[index] --- Item's path.

  以上代码很好理解,首先随机获取一个道具的索引,根据索引获取到probs[index]的值,与随机数(0-1)比较,由此可见probs存放的是一种运算后的概率值。若是随机数大于概率值,索引则改为alias[index],由此可见alias存放的是一种与原索引相对应的新索引,而新的索引自然会有对应的道具。
  如此我们便可理解这套算法的做法了:为每个道具设置一个概率值以及相对应的另一个道具,随机到一个道具后,仍需二次随机进行二选一。这么做很好理解,就是将一些高概率的道具填充到一些低概率的道具里:
example

  如图所示的第二项紫色的占比(概率)为1,表示不需要进行二次随机了,如此即可保证整个掉落池的概率是可以平分干净的(多出的部分就作为1概率项)。不得不说这种做法十分绝妙,完美解决了先前做法中掉落池元素过大的问题,美中不足在于需要进行二次随机,相对破坏了概率的纯粹性,但由于只是二选一,实际上效果是可接受的。

掉落池的维护

  虽说Alias Method方案的掉落池配置变得相当容易,只需如此这般填写概率值即可,再分别生成items与paths:

1
2
3
4
5
6
return {
["equipment/weapon/sword"] = 0.3,
["equipment/weapon/knife"] = 0.3,
["equipment/weapon/katana"] = 0.3,
["skill/flash"] = 0.1
}

  然而实际上掉落项的种类与数量都相当的多,并且会时常更改。所以这般直接的配置是无法满足需求的,于是演进为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
return {
skill = {
prob = 0,
item = {
flash = 1
}
},
["equipment/weapon"] = {
prob = 0.9,
item = {
sword = 0,
knife = 0,
katana = 0
}
}
}

  新配置明显就方便了不少,若是概率填写为0则表示剩余总概率的平均值(sword=0 => 0.9/3 => 0.3),且填写的概率是相对于本层的(skill的总概率为0.1,故flash=1 => 0.1)。算是基于原配置进行了一波封装,可维护性大幅提升,如此便可面对变化频繁的需求了。

后记

  本文所展示的掉落业务只是基础,在业界会有复杂度远超于此的需求(与时间、职业等因素挂钩,掉落池数量等),但DFQ的需求也仅此而已,期待日后能接触到更主流的设计。

在macOS搭建LÖVE for iOS平台

Posted on 2019-03-01 | In Teach | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  继上回在macOS搭建LÖVE for Android平台后,这次买了新的iPhone,对iOS平台的发布自然也要开始了。如上回一般,也发现了不少实操中会遇到的问题,特此记录,以便后人。

实机调试

  LÖVE for iOS的编译可谓相当容易,前提是你必须拥有一台macOS以及iOS设备,并且安装了Xcode。如官方教程所言般进行便是,大致上就是下载LÖVE源码工程,并且用Xcode打开love/platform/xcode/love.xcodeproj,然后选择love-ios项目、连上iOS设备、设置签名、然后Build就完事了。
0
  如上图所示那便是签名的设置了,需要登录Apple账号作为Personal Team,并确保这是iOS设备所使用的账号。
1
  签名设置完成无错误提示后,那便如上图所示般选择、执行即可。请确保iOS设备与macOS设备是处于连接状态的,过会便可见到iOS设备已将LÖVE安装完毕。此时尚无法直接运行,需要执行设置→通用→设备管理→Apple账号→信任love。
  刚装好的LÖVE仍是空空如也,你可以选择打包好一个项目作为test.love,使用Apple的隔空投送(AirDrop)功能进行快速传输,如下图所示:
2
  投送完毕后,iOS会精确的识别到这是LÖVE所需要的文件,于是你可以在LÖVE里见到它了,如下图所示:
3

画面适配

  我所测试的项目的功能很简单:显示一张图片、该图片会拉伸至窗口大小。直接运行的效果如下图所示:
4
  很明显可以得出两个问题:画面并不是水平的,以及顶部的状态栏没去掉。解决它俩的方法很简单,想要画面是水平的,就得在编译设置里进行更改,如下图所示:
5
  如此即可,至于顶部的状态栏的去除,选择编译设置里的Hide status bar是无效的,因为LÖVE在游戏运行时又做了一次设置。而这个设置则是与引擎的love.window.setFullscreen(fullscreen)这个API有关,只需要在游戏运行时设置为全屏即可关闭状态栏。如此便没毛病了,如下图所示:
6

开发调试

  在开发过程中需要不断地进行实机调试时,每次都对项目进行打包那效率未免也太低了。然而鉴于iOS的沙盒机制,又做不到如同Android般使用Git来进行同步工程。好在*.love文件本质上就是*.zip文件,如此开发一个对压缩包进行增量更新的脚本即可。如以下代码所示,用到了zip命令,脚本版本为Python3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import os
import datetime
import zipfile
from os.path import realpath, dirname
def listdir(path):
paths = []
lst = path.split('/')
tree = file_tree
for i in range(len(lst) - 1):
tree = tree[lst[i]]
for k in tree:
v = tree[k]
p = k if v is True else k + '/'
paths.append(p)
return paths
def zip(code):
os.system("zip %s %s" % (file_name, code))
def sync(path):
is_file_a = os.path.isfile(path)
is_dir_a = os.path.isdir(path)
info = path in name_set and zip_file.getinfo(path)
is_file_b = info and not info.is_dir()
is_dir_b = info and not is_file_b
if is_file_a:
if is_dir_b:
zip('-d %s' % path)
if is_file_b:
time_a = os.stat(path).st_mtime
time_b = datetime.datetime(*info.date_time).timestamp()
if abs(time_a - time_b) > 1:
zip(path)
else:
zip(path)
elif is_dir_a:
if is_file_b:
zip('-d %s' % path)
if is_dir_b:
list_a = os.listdir(path)
for i in range(len(list_a)):
if os.path.isdir(path + list_a[i]):
list_a[i] = list_a[i] + '/'
list_b = listdir(path)
list_merger = list(set(list_a + list_b))
for p in list_merger:
sync(path + p)
else:
zip(path)
zip('-r %s*' % path)
else:
if is_dir_b:
zip('-d %s*' % path)
elif is_file_b:
zip('-d %s' % path)
cwd = dirname(realpath(__file__))
os.chdir(cwd)
file_name = 'game.love'
zip_file = zipfile.ZipFile(file_name, 'a')
zip_file.close()
name_set = set(zip_file.namelist())
file_tree = {}
for p in name_set:
ls = p.split('/')
tree = file_tree
length = len(ls)
for i in range(length):
s = ls[i]
if len(s) > 0:
if s not in tree:
tree[s] = True if i == length - 1 else {}
tree = tree[s]
sync('asset/')
sync('source/')
# ...

IPA发布

  以上只能本机运行而已,若是想分享给他人,便要解决新的问题了。iOS的安装包为.ipa文件,你需要提供正式的开发者账号作为签名,方可生成之。开发者账号分为以下三种:

  • 个人账号
    • 只能提供单人使用
    • 其他人若想运行ipa文件,需要注册其UDID
    • 99美元/年
  • 公司账号
    • 允许多个开发者使用
    • 需要填写公司的邓百氏编码(D-U-N-S Number)
    • 其他人若想运行ipa文件,需要注册其UDID
    • 99美元/年
  • 企业账号
    • 允许多个开发者使用
    • 需要填写公司的邓百氏编码(D-U-N-S Number)
    • 该账号下的APP不能发布到App Store
    • 299美元/年

也就是说,除了企业账号以外,想轻松分享给他人是比较麻烦的。这方面可以考虑诸如蒲公英一般的第三方签名平台,会更方便。
  IPA的生成方式为Xcode下Product→Archive,然后根据指示进行即可。顺带一提,在编译设置中将game.love文件加入到APP资源里,变会默认直接运行该项目,以达到发布的效果。如下图所示:
7

LuaJIT

  还有一个需要注意的问题是:Lua代码若是需要转为LuaJIT字节码,所选择的版本得是LuaJIT 2.1.0-beta2 64bit(在LÖVE 0.10.2下)。重点在于这个64位,由于Apple现行规定APP必须得是64位,于是连LuaJIT的字节码也必须同步。而想要生成64位的字节码,则必须编译出64位的LuaJIT,而这需要在编译时填写参数:make CFLAGS=-DLUAJIT_ENABLE_GC64。

后记

  总的来说iOS平台较之Android在编译方面更为简单,毕竟是很稳定的平台与设施嘛。但在开发方面则有更多的繁文缛节,也算是福兮祸兮吧。

《DFQ》开发随录——随机地图

Posted on 2019-01-30 | In Development | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  虽然先前未曾严明,但《DFQ》的全称为《DungeonFighterQuest》,由字面上便可得出,这是一款《DNF》的同人游戏,那么《DFQ》的地图自然向《DNF》看齐了。而《DNF》的地图众所周知,具有一定的复杂度,在以往的作品开发过程中便是采取了手动制作的方式,可谓十分的费时费力。于是在《DFQ》便采用了生成随机地图的方式,与市面上许多独立游戏的做法不谋而合,毕竟手动做地图实在是太辛苦了(汗)。本文便记录其中心得。

地图结构

map

  如上图所示,这便是一张随机生成的地图,它拥有以下组成:

  • 远景层:地图最底的背景,图中表现为山水。
  • 近景层:地图较近的背景,图中表现为树林。
  • 边上层:地图的上边界,拥有若干地图物件。
  • 地表层:地图的地板,图中表现为草地。
  • 边下层:地图的下边界,拥有若干地图物件。
  • 活动层:地图的主体,拥有若干活动的地图物件。

  在配置中以这种形式组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
return {
info = {
width = {1440, 1280, 1024}, -- 宽度随机选择
height = {600, 736}, -- 高度随机选择
theme = "lorien", -- 地图主体
type = "dungeon", -- 地图类型
bgm = "lorien", -- 背景音乐
bgs = "forest1", -- 背景音效
name = {
cn = "洛兰",
kr = "로리엔",
jp = "ロリエン",
en = "Lorien"
} -- 用于显示的地图名称,拥有中日韩英四语
},
floorHorizon = 327, -- 地表层起始Y坐标
scope = {
x = 16,
y = 368
}, -- 可行走区域起始坐标
far = "$A/far", -- 远景层
near = "$A/near", -- 近景层
floor = {
left = "$A/tile/0",
middle = "$A/tile/2",
right = "$A/tile/1",
bottom = "$A/tile/3"
}, -- 地表层
sprite = { -- 图片
up = {
"$A/flower/0"
...
}, -- 边上层
floor = {
"$A/grass/0",
...
} -- 地表层物件
},
actor = { -- 活动对象
down = {
"$A/tree/0",
...
}, -- 边下层
article = {
"$A/tree/1",
...
} -- 活动层
}
}

  接下来将对逐层进行分析。

远/近景层

  远景层与近景层的机制完全一致,所以可以拿来一起说明。当然之所以会分为两个层次而非合并,是因为远景与近景关于摄像机移动时的相对移动速度不一样,以此形成纵深感。但在地图生成这一块,它们的机制是一致的:

1
2
far = "$A/far", -- 远景层
near = "$A/near", -- 近景层

  它们都是加载一张图片,然后根据地图的宽度进行平铺操作即可。在最后阶段会渲染成一张成品长图,这是一种优化方法。

边上层

  边上层为地图的上边界,拥有若干地图物件。这里的地图物件与其他层的并不一样,在配置里它的划分是sprite,仅仅是单纯的图片罢了:

1
2
3
4
5
6
sprite = { -- 图片
up = {
"$A/flower/0"
...
}, -- 边上层
}

  因为这些物件不需要与角色产生什么互动,最后也会如远/近景层一般,渲染成大块的成图。
  关于物件的放置,会采取生成宽高为100的格子铺满整行,并随机在这些格子上放置物件,如下图所示:
up

边下层

  边下层与边上层类似,但是生成的地图物件为活动对象(actor):

1
2
3
4
5
6
actor = { -- 活动对象
down = {
"$A/tree/0",
...
}, -- 边下层
}

  边下层的地图物件需要作为活动对象主要是因为某些物件会遮挡人物,所以需要采取靠近后透明化的措施。于是不方便作为单纯的图片。
  物件放置方面与边上层一致,这里不再复述,如下图所示:
down

地表层

  地表层即地图的地板,远/近景层类似,也是采取平铺的方针。但是在元素上更为多样:

1
2
3
4
5
6
floor = {
left = "$A/tile/0",
middle = "$A/tile/2",
right = "$A/tile/1",
bottom = "$A/tile/3"
}, -- 地表层

  地表层的图片分为左中右下四种,左右两种为于地图边缘进行随机选择(左/右或中),中为默认选择,下为平铺Y方向。
  除此之外,地表层还会拥有一些类似边上层的地图物件:

1
2
3
4
5
6
sprite = { -- 图片
floor = {
"$A/grass/0",
...
} -- 地表层物件
},

  这些物件也是不会与人物有所交互,最终与整个地表层渲染成大图。与边上/边下层类似,地表层物件的放置会XY平铺宽高为64的格子,以此放置:
floor

活动层

  活动层即地图的主体,活动对象的放置层,诸如障碍、宝箱、怪物等皆置于此。放置的规则与地表层物件一致,与地表层物件的不同之处在于,活动层存在一些拥有障碍的物件:
obstacle

  如上图所示,《DFQ》采用的障碍方式为传统的格子流,这种形式便于配合类似A星的寻路算法。但如此存在障碍格子与物件素材的匹配问题,这方面都需要手动设置好。以及需要警惕因障碍范围过大且恰好四周都是障碍物件围住了人物的情况,好在实际上并不存在这样的物件(障碍并不会很大),并不需要为此做特殊措施。

随机问题

  在处理诸如物件放置的问题时,切忌采用遍历+随机数判断的形式。因为这是不符合概率论的(存在放置数量上限),如此便会导致地图左边的元素多于右边(右边存在轮不到的可能)。所以得采取将格子存储在一个list中,以list[math.random(1, #list)]的方式提取要放置的格子,如此即可保证几率均等了。

后记

  对于一些需要个性添加的元素(地图特效、通行门、BOSS),一般会采取编写专门的处理函数进行添加。对于一些需要固定化的地图,也可以采取生成后输出成文件以加载使用。目前这套很明显的缺点在于无法生成崎岖不一的地形,不过目前暂无需求,且日后再看吧。

2018年度总结

Posted on 2018-12-31 | In Talk | | Visitors

  不知不觉中2018年即将过去了,与去年相同,特作总结,以为归纳。
  今年的Blog总共写了14篇,较之去年的13篇算是相差不大。但内容方面则更为单纯了,简要说来便是:DFQ、网络同步、ECS、实践记录。期望明年能有新的花样。
  今年的主要成果为对网络同步的涉猎,在帧锁定同步方面有《Brick & Ball》,在FPS/TPS同步方面有TPSDemo。收获了不少同仁的赞可,但都算不上是正式项目的实践,只能有待机会了。
  项目方面,《DFQ》的开发也算是正式进行了,这将是明年的总旋律。与今年类似,如无特殊情况则保持月更相关Blog。待最终完工开源后(也许)将会是一个很好的学习素材。
  平常时刻也有与ChawDoe进行一些作业方面的研究,涉及到诸如强化学习、哈希表、寻路算法、词法分析、UNIX/POSIX API、OpenGL等方面,也是获益良多,作为巩固基础了。
  总的来说,愈发感觉到计算机图形学是自己的待恶补项,以及3D游戏开发中的不少方面也值得实践,这将是明年的发展方向,期望在这些方面能有所建树,作出一些心得分享。
  以上便是本人的2018年度总结,今年下半年以来的社会经济状况不是很好,祈祷明年能够回暖吧。
  无双草泥马
  2018.12.31

123…5
Musoucrow

Musoucrow

48 posts
7 categories
36 tags
RSS
Coding Github
© 2020 Musoucrow
Powered by Hexo
Theme - NexT.Mist