又一期猴叔的知乎问答精选。

这期聊到了失落方舟这款游戏。有人在知乎问到:Lost ark这款游戏中,怪物在包围玩家时会尽量避开已经存在怪物的位置,最终会以一个圆将玩家包围起来,这种效果在是如何实现的?

以下是猴叔的回答:

这并不是一个刻意为之的事情,而是一个自然性质产生的必然结果(现象)。具体的思路是这样的,一步一步来,可能有看着很简单的东西,但是要说清楚还是得说:

首先,我们来理解问题,拆解这个“玩法”

真正的游戏拆解工作,就是把这个问题所涉及的数据结构和逻辑捋清楚,列出相关的数据结构和逻辑的理解,他们的形式不一定,如果你爱画画就画出来,你爱列表就列个表都可以,核心目的是搞清楚这块究竟发生了什么。所以我们要观察现象,思考背后的原理:

角色的位置相关数据

这是视频中的一段,我们可以大致看出,他应该不是tilebased周围8格各占一个怪然后围着砍(传奇是tilebased),而是围绕着一个圈砍人,在这里的一个细节就是,并不是每一个狼(看着是狼)都能砍到人,所以这中间说明了,角色数据中包含至少这些信息:

移动、攻击和碰撞?

  • 碰撞圆:即坐标和半径组成的一个圆,2个角色之间避免这两个圆重叠,就可以做出“游戏中角色之间有碰撞”的幻术——值得一提的是,游戏中的碰撞,还就是这样的幻术。
  • 攻击圆:和碰撞圆同心的圆,假如敌人的碰撞圆跟“我”的攻击圆有重叠,意味着我可以开始向他发动攻击了。

上面这些非常好理解对吧,简单的总结就是每个角色有:

  • position(v2):这个游戏是标准的2.5d视角,其实所谓的2.5d视角也是媒体发明的说辞,本质上就是2d,所以逻辑一定是一个2d平面,尽管很多游戏渲染是3d的,但最后也可能是逻辑2d的(比如火炬之光系列,大菠萝4等),看破这个很简单。所以角色坐标是一个v2即可,最理想的应该是v2int,但是这个可能“现代与传统标准游戏开发脱节的自学成才的新一代游戏人”无法理解,我也不想在本回答做过多的解释。
  • bodyRadius(float):事实上应该用int,但是跟上面用v2是一样的问题,所以还是用float。这就是角色的“碰撞圆”,position已经提供了圆心了。
  • attackRange(float):同理应该是int的。这是角色的可以攻击的范围,事实上这个攻击范围并不是真的攻击范围,他是一个让ai用来毛估估的范围,毕竟角色应该会有好几个技能,每个技能的范围都不一样是吧,但是我总不能每一帧给你冒泡出来我要用啥技能,范围多少对吧,所以可以给一个毛估估的范围,当然你要做的细致一些,可以让ai在使用技能上不经常改变主意,每次改变想用的技能就改变这个也不是不行。
  • moveSpeed(float/sec):因为要移动,所以有这个数据。

之后,你应该理解:他正确的单位是int/tick,当然这个问题也不在这篇细说了,但是这个问题确实会带来极大程度的性能损耗和开发别扭就是了。

接着我们观察它的ai移动行为,得出一些细节:

在视频5秒左右的时候,请盯着这两个单位看,他们很有代表性,当然眼睛好的话盯着别的单位也没问题。

你会发现ai的移动,有2个特点:

  • 其一是:先跟玩家保持在直线,尽量直线追赶,这没什么可以多说的。
  • 其二是:他看起来是8方向移动的,是这样的,这个也是问题的key所在,但是你要理解一件事情——他并不一定非得8方向移动,产生这个结果,是一个性能优化所致的问题。

AI究竟在“想”什么

当我们捋清了上面这些信息,也就得除了他所需要的数据和简单的逻辑了,接下来就是更进一步去拆ai在“包围”玩家这件事情上怎么做。

图1:当视频到达这一帧的时候,初心者会错误的理解一个问题——会误以为敌人是有智慧地在进行“包围”工作

图2:当视频到这个时候,我们会脑补出有些怪的轨迹是这样的,他们绕了一圈到角色身边空处。注意一个细节:绕的时候还出现过“纠结”和“抽搐”,千万别忽略这个细节。

到这里,很多耍小聪明的人就会冒出一个极端错误的概念——群体ai。

错误的理解图

在他们错误的想当然中,逻辑是这样的——我根据玩家位置,先为每个角色选好了一个坐标,这个坐标环绕在目标身边,但是不互相碰撞,然后每个怪物走过去就行了,这是一个集群行为,所以是“群体ai”的一部分。乍一想,好像没什么毛病,但实际上这是做不了游戏的思维。我们接下来就来看真正的游戏设计师是怎样看这个问题的:

然后看他的本质是什么?

首先,先不论是不是“帮一群怪找到他们的坐标”,也有菜鸟策划会说“在身边找一个空的位置”,这俩方法表面上是不一样的,但是本质是同一个问题。因为初级策划(甚至是玩家)都会这么给游戏提需求——“怪物在角色目标身边找一个空位,然后去打他。怪物之间是有碰撞的,和玩家也有碰撞。一定要找一个在攻击范围内的位置才能开始攻击。”然后呢?然后就没了,“我”已经设计好了是不是?看看,说得多完整,所有的细节都说了不是吗?

群友太空猴的精彩调侃

但事实上正是因为这几句话啥也没说,所以才产生了以上的误会。我们从本质出发,也就是捋平舌头把话说清楚,这个设计会遇到什么问题?

怎么找出这些坐标?

我们可以把这个问题简化为一个数学问题——有一个半径为r的圆(r>=0)o,在其周围找到x个坐标点(x∈(0,∞)),每一个坐标点都会给一个半径为rx(rx>=0)圆,要求所有的x个圆和o互相不重叠,求:这x个坐标,转化为一个pos: v2[]。

这道题怎么解?我相信现在的数值策划肯定有办法,是吧,都是硕士以上学历了,肯定不像我这种大专生,只能问gpt,结果gpt都解不来这题只能两手一摊了。

我每一帧都重新找吗?

但是这个问题最根本的并不是说这一道题的解法,而是我们需要在计算机程序里每个tick执行n次这个(毕竟目标可能有n个),怎样才能高效的做到?所以,有一系列问题就是:

Q:我每帧都要为每个目标算一次这个“群体ai”吗?

A:显然是啊,毕竟你这个目标是会移动的,移动了原来的坐标可不就不能用了?

Q:那我保存完这些坐标,跟着o一起平移不行吗?

A:除非这游戏没有地形阻挡,比如brotato。不然我o下一tick边上都是悬崖怎么办?让怪跳下去?

问题到这里,发现不对劲了吗?这是“不懂程序可以做好游戏策划吗”的标准答案——不懂你还设计个屁。

那正确的做法,怎么做?

因为最终是遍历每一个怪,执行他们的ai的,所以我们先把自己带入到一个怪里面。现在我是一个怪了,轮到我了,我这一帧(一个tick,也就是一次fixedUpdate)要做什么?先不考虑其他的ai或者我不能行动之类的问题,紧扣这个问题——我的目标是:如果能砍人(我的目标)就砍人,不能就继续接近他。“砍人”也不是我们这个问题要解决的,我们直接解决这几个字——“继续接近他”。

一般来说如何接近?

几乎毫无疑问对吧,(敌人的坐标(v2)-我的坐标(v2)).normalized就是我的移动方向,乘以我的移动力(int/tick或者float/sec*deltaTime),就是我这一帧要怎么移动,经过n帧(假如目标速度小于我)最终我总能追到目标对吧,只要保持这个逻辑的话。这毫无疑问是最高效率,也是最好接受的。

加入阻挡之后如何接近?

但,上面说的情况只有我和目标,没有任何阻挡(其他单位和地形都可以构成阻挡)。那么我们加入阻挡以后,就有可能是这样的:

有阻挡喽,别管阻挡是人还是东西,逻辑上他们没有区别

假设我一帧的移动力<=灰色箭头的长度,那么我继续保持这个方向没毛病对吧。这里要注意一点——所有的阻挡和目标,都是不会动的,因为世界在这一帧里是绝对静止的(因为时间暂停在了这1帧)只有我可能会移动。所以游戏中的“避让做法”的本质,就是for循环列表在我之前的单位移动后的坐标,和在我之后单位移动前的坐标,对我来说就是阻挡,由此我们总是能得出一个静态的阻挡图,如上图。

这时候进一步的思考就来了,如果我的移动速度超过那个灰色箭头我该咋办?Brotato里面的做法是我就停在那里了, 随意你会发现有一堆怪会堆在那里:

但是问题视频里的怪绕圈了对吧,要怎么绕?

理论上来说,走红走蓝任何一条线都是最好的方案对吧?毫无疑问,但是请问,我这一帧怎么知道不向着目标方向走,而是要走红色或者蓝色的绕圈呢?

红色是期望移动方向,灰色是现在的方向

是不是我得把移动方向围绕着我坐标(圆心)旋转x度之后得出来了?好像很简单哦?那请问x是多少呢?是不是,这时候脑袋就嗡的一声?如果x是int,我还能穷举一下,for循环直到这个方向不会被阻挡,我就走这个方向,x就是这个值。是的,但是就算360度遍历一下也不得了,所以为什么我们不干脆22.5度甚至45度作为一个单位,来遍历出来呢?由此,他变成了:

我们从8方向找到这一帧不会碰撞的方向(可能存在0-8个),如果没有一个,我这tick就动不了了。那如果有1-8个呢?(有没有可能有9个?你说呢?)那就把他们和目标方向之间的“较小的那个夹角的角度”从小到大做个冒泡,得出最接近目标的那个走,就完事儿了。

演算一下看看,是不是自然的结果就是这样?

现在用我说的方法演算一下,代入到题目的视频中再看看,是不是就是这么简单的一回事儿?其实就是这么简单,别想复杂了,他就是自然产生的一个结果而已。

发表回复

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

目录