很久没更新博客了,记录一下最近的干了什么
除了因为生活的事情去医院去了小一周之外。关于怎么做下去我自己的项目,我仔细思考了一下,现在项目这个情况,年底或者明年年初就做完一个版本的可能性不太大。综合考虑了一下,觉得可以找一个线上合作的班先上着,至少缓解一部分经济压力,另外如果能认识独立游戏行业里的更多人也许能帮我找到做东西的思路。
于是约了一个面试,还主动要求了做个测试,现在先记录一下做的测试的内容。
测试的内容大概就是做一款类似《清零计划 2》这样的联机肉鸽 Demo,具体的玩法有点类似血腥大地或者孤胆枪手,然后工具上主要就是用 Unity 的 Mirror 联机框架和 behaviourtree 做 AI。
项目名字随便起了一个,就叫 DungeonSlayer 吧。
这个 Demo 大概制作周期是一周,最初我给自己定的目标是下面这些:
先简单介绍一下 Actor 框架。
Actor 框架算是经过了四个项目(巧的是,都没上线)的迭代。他主要是包含
在第一版的时候是参考知乎上一位大佬的思路做的,最初只是用来做 GameJam 的。那个项目其实也没怎么写 Actor 的部分,主要是这一套伤害和 Buff 系统。基本上是借鉴那个大佬的思路。
Elona 抄袭项目的文档
第二版是在自己写的仿 Elona 的项目上。当时没有做太多的分层,所有的组件基本都是挂到一个 gameobject 上,最后导致的问题是要修改某些具体的组件内容的时候非常费劲。Elona 那个项目还实现了个简单的 Timeline 系统,但是最后那个项目因为一些原因被我放弃了,那套 Timeline 系统也没能留下来。
第三版就是去年秋天开始的那个合作的项目了,基本算是那个 Gamejam 的遗留意志(其实玩法已经改得面目全非了)这一版最大的改动可能就是把 Actor 组件分层了,然后结合 Zenject 做了依赖注入。
第四版就是在我今年 2 月开始的自己的项目上,最主要的改动就是改了寻路的实现,把 NavMeshAgent 改成了 AstarPath 那个库,别的东西似乎也没怎么改。
现在这个新的测试项目想要把整体的代码整合一下,算是对这个套东西一个阶段性的重构。
改动比较大的就是用 KCC 来实现了人物的移动。其实目前来说以我的项目需求里,并没有哪些是 KCC 解决得很好但是 CharacterController 解决得不太好的情况。但是,换到能完全看到代码的 KCC 相对来说还是让我安心一点(尽管 Motor 上 2200 行的代码还是劝退了我)
Mirror 之前有所耳闻,有一个同事当时业余做的一个小项目用了这玩意。然后我看了文档才知道他的前身是一坨的 UNet....在我早些年还没怎么会写代码的时候,这个 UNet 给我留下了很深的印象。我记得很多功能做了但是非常不好用,他的目标应该是一个易用的 P2p 联机框架,但当时的版本来说,他只给一些无关紧要的东西提供了一些封装,关于一些关键性的内容你要自己疯狂手写。
另外之前除了比较不愉快的 Unet 之外,其实没做过这种 Client 和 server 写在一套代码里的工程。之前商业项目也都是常见的 CS 分开的结构,自己手搓过一个 python 做服务器的 Unity 做客户端的纯粹 Socket 的小 Demo。但是我心想这个内容应该对我来说也不是特别困难,所以当时接了的时候还是信心满满的。
BehaviourTree 相比之下算是比较熟悉的东西。因为之前在学 UE 的时候里面也用过 UE 的 behaviourTree 做过一些东西,不得不说 UE 的蓝图和 BehaviourTree 结合起来写还是对设计师非常友好的。很多时候设计师自己独立就能解决很多问题。
这是我最恐惧的部分,我基本上没什么做关卡设计的经验。然后对于关卡设计我也没有系统的学习和实践经验。而且我自己对这个部分总是有一种蜜汁恐惧感。
实际上开始做的时候,我发现具体的难度比我想象中大很多。
带给我最大麻烦的还是 Mirror。因为 Mirror 想要同步 Instantiate Gameobject 基本只能通过 Spawn,但是 Spawn 的 Prefab 基本是固定的。
这最直接带来的问题就是基于 Zenject 这套 Injection 基本上爆炸了,因为 Actor 的 Gameobject 上就是一个子 container。这部分最后通过注入解决了这个问题,但是我觉得不算一个特别好的解决方案,因为这样破坏了原来的调用顺序,整个生命周期的复杂度又上了一层。
另一个问题其实相对来说好解决些,就是 Actor 的初始化问题,我不知道如何让一个 Actor 开始就是敌对状态。当然这部分的问题还是因为我对 Mirror 本身常用的规则和套路不太了解。
事实上习惯了 Mirror 这套编程方案之后,同步上能想到的套路能想到的办法基本就是两种:
所以针对我遇到的另一个问题:rigidbody 位置不同步的问题。因为 KCC 是基于 rigidbody 移动的,因为使用的是各自在客户端计算物理结果的方法,结果就是只要碰撞之后就经常出现角色的位置和服务器对不上了的情况。
我尝试用 Mirror 提供的 Rigidbody 同步的工具,但是我发现这个东西根本就没啥用。说实话到现在我也不知道这玩意到底是用来干嘛的。。
在我摸清楚这两个思路之前,我暂时放弃了用 rigidbody。我纯粹用 Transform 来做移动。当然这样是完全没办法做任何的物理碰撞了。花了时间把移动硬改成了纯 Transform 移动的效果。想着也许先就纯粹 Transform 的移动好像也能接受?
折磨了一天,发现客户端自己模拟这件事真的是完全不靠谱。跟朋友聊了一下这方面的思路,也确定了很多时候很多的内容尽量是只交给服务器处理,然后客户端只是同步结果就好。
所以后来选择只在服务器上计算 KCC 移动的相关内容,然后服务器计算出来的移动速度等参数通过 SyncVar 或者是 SyncTransform 组件来同步到客户端。目前除了服务器压力比客户端大以外,我也没想到其他的劣势。
这条思路通了以后,基本上所有的内容的改造都是基于这一点了。
我把我认为比较重要的过程都交给了服务器来做计算,比如 KCC 移动,伤害过程,Buff 过程,死亡。客户端只是复读机,复读服务器的结果。
但是很多具体的细节上,我的实现有点抽象。。。比如 HP 这种属性值的同步,因为我原来是一个 Dictionary 来存键值对,但是这玩意的同步一方面是有装箱拆箱问题,另一方面是你内部值的修改其实并不一定会触发同步,所以我用了一个邪门的方式来做,那就是把他编码成字符串,然后同步那个字符串。
事实上后来做 Buff 同步的时候发现可以通过自定义 BufferWriter 的方式直接同步某个自定义的数据结构。。不过似乎也解决不了我上面那种改局部值的问题。所以还是本身的设计如果就只有一个 hp 的 float,那么基本上也不用我那么费劲地来回折腾了。。
另外最大的感觉就是,这个调试的过程相比纯单机真的痛苦太多,毕竟每次你必须要单独 Build 一次然后来调试。当然确定了我这种模式之后,大部分情况下也只需要看 Client 端是不是有什么没同步正确的部分。
剩下的基本就是无尽的写 RPC 和 Command 了,就不展开了
行为树可能确实没啥特别想说的。这一块也算是比较成熟的框架工具了。但我自己写下来感觉最难的部分倒不是说实现上,我觉得可能更难的部分是在定义作用域上。
举例来说一个巡逻-> 发现-> 追逐-> 失去踪迹。这么一个简单的模型,难的点是梳理出来每个部分的边间条件逻辑。其实实现上倒不是有多少复杂的成分在,但是在实现上很多内容跟我预想中确实是不太一样的。
关于这点暂时就不展开讲了。我觉得可能也是我对各种行为树组织的方式不太熟悉,导致我最后做出来的东西总感觉更像是某些地方在 Action 里做了硬编码一样。
我在动画管理部分是仿照了 UE 的动画系统的。很多时候比如攻击,防御,翻滚等操作需要播放固定的动画,但是移动之类的动画又需要一个 2D 的 BlendTree 来完成。所以其实这种需要播放固定动画的部分可以算一个独立的部分,这部分在 UE 的动画系统中叫 montage。
我在 Unity 里通过 playable 的接口实现了一个简单的系统。Animator 还是负责固定的混合树的部分:主要是移动。其他所有的动画播放基本都靠 Montage 来实现,比如攻击,受攻击,眩晕等等。
然后我发现之前的动画接口写错了。。。Montage 和 Animator 根本没法混合。。。
为什么会改动这里,是因为我在游戏中尝试实现两种攻击方式,一种是近战攻击,一种是远程攻击。如果说近战攻击的时候角色的脚下停下,在原地做攻击动画的时候尚且合理,那么远程攻击就显得有点奇怪了。事实上,远程攻击(游戏中表现为用法杖发射光波)会更适合边移动边射击的模式。为了实现这种模式我就改到了动画头上。
当然最后还是花时间改成了正确的。之后的方案就很简单了,对行走动画和攻击动画做混合,并且分别给他们赋予不同的 AvartarMask,行走动画要把除了下半身以外的区域都遮罩掉,攻击动画相反。最后就能做出一个偷感很重的攻击动画。。。
当然中间还有个巨坑,就是 IK 的 Blend 也要考虑进去。我开始的时候没注意设置 IKFoot 的 Blend,导致最后脚步上的动作一直不对。
这套动画状态管理系统其实是参考了 UE 的 TCF 的框架的,但是之前没有遇到过需要写连招或者是输入操作 Buffer 这样的东西。
在这个项目中我觉得自己需要做一个简单的近战的连招,所以就实现了一套。实现起来到不是特别复杂。不过主要问题在于,其实攻击输入操作的部分相当于是我自己硬编码的。
如果要像 TCF 那样通过动画通知来实现的话,还需要实现一个新的 Timeline 的 PlayableAsset。不过这个部分我觉得做起来其实也不是特别困难。
另外我没有做输入缓存 Buffer,毕竟很多时候不是那么看重攻击的手感。当然如果我要改成一个动作游戏的话这一点可能就很重要了。
至于这些之外,武器系统扩展成了这样的形式。主要是支持了修改动画时间以及修改受到攻击者的硬直时间。
其实这些部分也算是扩展了我的知识。虽然估计我很快就会忘掉一些具体的细节。不过如果我什么时候打算做一个联机的游戏或者是做一个强调 AI 并且刚好用行为树的游戏的时候,上面这些经历也算是用得上的。虽然我只做了一周的时间,但是整个制作周期里我基本上是用了我所有能用的时间跟努力来做(甚至 ZZZ 公测都没时间玩)
另外想再聊聊这套 Actor 系统,尤其是在我扩展了动画系统之后。我其实能感觉到他在动画的一些内容上确实是拥有巨大的潜力的。之前考虑过要不要干脆把现在这套手搭 Playable 的系统换成更实在,更成熟的系统,比如 Animacer 或者是 Timeline。但是现在看来似乎短期内没这个必要。
我想要做更多,甚至想要尝试用这套系统做 FPS 和 TPS 和 3DAct 来测试这套系统的潜力,然后抽一套通用的代码出来,想想应该算是挺有意义的一个事。另外也是考虑到,自己自从辞职以来,可能过分高看了自己的代码能力。事实上在代码的积累我还是需要花很多时间去更进一步的,只是相比之下我不算特别担心自己的代码解决能力,更担心策划和美术方向。
所以我认为在代码上,刻意练习这样的 Demo 然后去做一些不同方向的,也有利于以后找工作的时候展示自己的技术成果,把这一点当做一个积累和展示自己的资产也是一件好事。不一定非得是纯粹面向产品的博客和游戏风格,技术向的 Demo 也是完全值得花时间研究的。
所参考的游戏是一个固定场景 + 类幸存者 + 肉鸽 Build 的游戏。事实上我觉得我不太想把游戏做成那样。我在想能不能做一些线性关卡来顺便学习一下如何做关卡设计。于是我就得到了这个:
在游戏的关卡上我最先想到的游戏是求生之路。因为时间原因,我没有太多时间设计太多有趣的强力的 Build 体系来支撑整个游戏的玩法,所以我觉得我需要一个用固定资源控制来引导整个游戏的那种设计,自然而言我就想到了求生之路。求生之路的安全屋的设计算是一个核心的引导点,基本上游戏的循环也是围绕一次次的寻找安全屋而来。
另外一个参考的可能是类魂游戏里的一些关卡的思路。我在开局就设置了一个岔路,但是通向的是全游戏最难的精英怪。我想要把这个当做是个最终的谜题,玩家需要寻找能够解开这个谜题的道具或者 Buff。
之后的设计基本就是中规中矩的:挑战奖励循环。然后可能还有一个小设计就是在第二个红色区域挑战完成之后,相邻的白色通道会开启近路。魂系列对于开近路这件事一直有些说不清道不明的痴迷,甚至很多地方已经成了解谜游戏式的设计,比如魂 2 原罪学者里有个地方要把火药桶推下去炸开篝火旁边的近路。
但是我觉得这个设计可能不好的点就是,某些挑战区域和他的奖励区域并不是明显有对应关系的。因为我没时间做升级,获得 Buff 这些内容,所以我只是把奖励放在了那些蓝色的区域。原本这些区域我想设置一些解谜内容的。
最后整个游戏的关卡设计出来了,但是内容并没有填充完毕。我只做了到了最后那个困难 2 的区域的怪物,目的也是为了让大家使用一下新拿到的武器之类的。
另外关于敌人和武器强度的平衡,我目前真的没有什么专业的正确的思路,感觉就是把一些敌人的移速改一改,攻击频率改一改,移动速度加一加,伤害加一加。武器差不多也是这么回事。毕竟后面几乎没什么时间去实现一些具体的技能或者是特性的情况下,这几乎是我能想到的最优解了。
当然可惜这个面试还是因为其他原因失败了。但是花一周时间做一个也算是可玩的 Demo 这件事确实比我想象中来得更有意义一些。事实上我还是挺感谢这次硬逼着自己做下来这个 Demo 的机会的,它其实让我重新审视了我在具体执行上的一些不足之处,甚至是我相对来说较为自信的代码层面。
那么其实针对其他领域下,比如设计和美术方面我其实更需要更多的实践,并且一定得是“限时命题作文”。因为这样我才不会给我自己左右摇摆的借口来把心思移到”这件事合不合理“而是专注于”怎么按时做出来最好的东西“。
我决定先不再做大的,长期的项目来挣钱。事实上目前的情况虽然也不至于穷到揭不开锅,只是一直以来没收入这件事确实某种程度上也让我焦虑。然后这种焦虑更多的反映到了我对自己作品的急功近利上来。我觉得自己仍然需要磨练自己的能力,最好是这种“限时命题作文”的形式。
目前除了一个保密的项目,业余时间我想更多花点时间做一点免费的,可以不考虑直接营收的游戏,限时的,限命题的游戏来锻炼一下我自己的能力。现在能想到的几个: