« August 2018 | Main

September 20, 2018

动作游戏中的击打判定

最近在玩怪物猎人世界,断断续续差不多 100 小时了,加上之前花在这个系列上的几百小时,不敢说是个老猎人,忠实粉丝还是算得上的。

因为职业原因,我又琢磨了一下这类游戏的实现方法。在网上搜不到太多直接资料,所以这篇 blog 更多的是对自己的想法的记录。这次主要还是想理解一下游戏中是如何处理武器和怪物之间的击打判定的。

我知道动作游戏和格斗游戏侧重点不同,但也有类似之处。格斗游戏尤其是 2D 格斗游戏资料比较全,制作方法成熟,从网上能找到不少资料介绍原理。浅谈格斗游戏的精髓——方块的战争 讲的非常清楚。为了性能,也为了简化及明确规则,2D 格斗游戏用了若干 AABB 轴对齐的矩形来做击打/碰撞判定。格斗游戏会精确到帧来做规则判定,在特定帧勾勒出 hurtbox (惯例上用红色框)作为攻击范围判定;如果击打判定帧的 hurtbox 覆盖了那一帧对手的受击盒 hitbox (惯例用蓝或绿色框)区域,就认为击打有效。另外,还会设定出碰撞盒 collisionbox 用来避免和对手重叠,或和障碍物做碰撞检测。

即使现在用 3d 技术来渲染,大多数格斗游戏依然可以归到 2d 格斗类型中,还是可以用这种方法来做的。比如最新的 Street Fighter 5 ,完全延续了平面 AABB 攻击盒的规则。在网上甚至可以找到工具 显示出攻击盒,帮助玩家训练,提高技艺。

也不是所有的 2d 格斗游戏使用 AABB 盒做击打判定,例如任天堂大乱斗就是用了圆和胶囊。比如 这个视频 很好的解释了攻击极打判定规则,武器对撞 clanging 规则,弹刀 rebound ,还有出招前摇 startup lag ,受击硬直 hit lag 。武器格斗会比拳脚格斗更丰富一些,例如同样是挥砍,剑尖的伤害会比中段的击打伤害高一些,这里是用多个优先级不同的 hurtbox 来实现的。

fps 游戏不同。其一,它一般是全 3d 逻辑的,其二,它一般是用子弹对敌人造成点伤害。所以,fps 游戏一般直接把一些立体的小盒子 Cuboid 绑在人物骨骼上,作为 hitbox 做受击判定。子弹的轨迹在很短时间(两帧之间)内可以看作是一个线段。所以这就是线段和若干 Cuboid 求交的数学问题。最早期的 fps hitbox 例如 Quake 3 采用的依旧是 AABB 轴对齐的盒子,这可以极大的减少计算量。当然,另一个原因是当时游戏还是采用的是帧动画而非骨骼动画。在 3d 引擎普遍采用骨骼动画后,通常采用把立方体直接绑定在动画骨骼上来描述 hitbox 。CS:GO 在 2015 年之后改用胶囊体和球来描述 hitbox ,胶囊体更接近人体的四肢,且运算复杂度要小于立方体。

怪物猎人这类游戏又不一样。我认为,把 hitbox 绑在怪物骨骼上这点,和 fps 游戏并无不同。但刀剑挥砍则更接近格斗游戏。但是,作为 3d 逻辑的游戏,我们无法再用平面的框框来在攻击判定帧勾勒出 hurtbox ,那么应该用什么呢?

我觉得应该采用空间的四边形来标识 hurtbox 为主。以太刀举例,我们可以在攻击判定帧设定一个攻击平面,平砍,纵砍,还是斜劈,这个平面可以根据攻击帧和前一帧的武器位置来决定,也可以设计人员自己设定。然后在这个平面上勾画出一个或几个矩形定义出 hurtbox 。击打判定其实就是这个四边形和代表 hitbox 的立方体或胶囊体求交。对于大锤这种有体积的武器,则可以用平行的两到三个矩形来模拟出其厚度。

之所以不用专用的物理系统来做几何体碰撞检测。一是没有必要浪费计算力,尤其是连续运动过程的碰撞检测运算量很大;二是我们需要一个规则更简洁明了的击打判定规则。不过,我们需要定制编辑器去让设计人员定义动画中的 hurtbox 区域,编辑器的开发会是工作量较大的部分。

September 12, 2018

给 skynet 增加网络统计

skynet 在这个阶段的工作,主要是增强运行时内部信息的透明性。多提供运行时的统计数据可以为运维工作提供方便,也能为性能调优给出指导方向。

最近,我给 debug console 增加了 netstat 指令,以及提供了配套的 socket.netstat api 来获取这些数据。

这个指令可以获取 skynet 创建的所有 socket 的列表。每个 socket 归属于哪个 service ,每个 socket 上读写的字节数,最后一次读和写发生的时间,未写入系统挂在代写链表上的字节数。当然还有每个 fd 对应的 ip 地址和端口(如果是 socket )。如果是 listen fd ,会纪录 accept 成功的次数,

这些数据通过系统的 netstat 指令也可以查到,但是由 skynet 提供,可以查到更多细节:最关键的内部信息是一个 socket 被绑定在 skynet 的哪个内部服务上,这是系统的 netstat 无法提供的信息。


在设计获取统计信息 api 的时候,我的第一个想法是向 socket 线程发送一个控制信号,然后由 socket 线程做统计,然后通过 socket 数据结构返回。这样可以回避多线程问题;这类查询似乎也不太在乎是同步还是异步的。

但经过几个小时的思考和推敲,我否定了这个做法。

这样做的确可以简化多线程的处理,但是扩展 socket 消息结构却又可能对过去的代码照成潜在的兼容性风险。虽然这个风险不大,最坏情况只是造成查询结果的内存泄漏。

另外,由于查询结果是个复杂数据结构,必须由接收方来释放内存,把结果投递到制定服务同样也有内存泄漏的风险。

更重要的一点是,异步查询的接口使用起来会麻烦一些。

所以,最后我还是设计了一个同步查询 api 。因为这些统计数据只是用来参考,我们不用特别为一致性考虑。我们得到的数据并不是某个时间点的网络状态的快照。这些统计数据是从 socket 线程之外获取的,在获取过程中,可能在变化,但我认为没有什么坏影响。得益于 skynet 使用的是固定大小的 socket 数据结构数组(默认最大支持同时 64K 个 socket ,可以通过编译配置宏增加),我们不必特别考虑多线程下 socket 数据结构的生命期问题,增加这个统计查询还是挺容易,用一个 double check 就可以保证查询到的单个 socket 的统计数据是有效的,不需要额外加锁。


新特性的使用方法是,登录 skynet 的 debug console 输入 netstat ,或者可以通过 http 协议向 debug console 索取:wget –q –O - http://127.0.0.1:8000/netstat 。

也可以调用 socket.netstat() 获得详细的信息(返回在一个 lua table 中)。