January 22, 2021

关于 skynet 调度器的一点想法(续)

这篇是继续上次的一些想法

我最近想重新做一个新的基于消息管道的 N:M 多任务调度器。主要用在我们的客户端上。算是解决在使用 skynet 多年来总结出来的一些问题。

首先,我想改变 skynet 服务直接将待发消息投递到对方消息队列的做法。发出消息会让服务挂起,等待投递。

然后,我设计了一个叫做调度器的模块,用于把带发消息投递到对应的接收管道中。调度器是发送消息的消费者,接收管道的唯一生产者;而每个服务是它自己的发送消息的唯一生产者,接收管道的唯一消费者。这样没有竞争,当消息队列是定长的时候,也无需使用锁(但引入了消息队列满的新状态)。

阅读全文 "关于 skynet 调度器的一点想法(续)" »

January 12, 2021

把 skynet 的原子操作换成了 stdatomic

stdatomic 已经是 C11 的标准,并且成为了 C++ 标准的一部分。msvc 也将会支持 stdatomic 。在 skynet 项目开始的时候,还没有这个可以用,所以我采用的是更早一点的 gcc sync 系列的扩展

我想,用 stdatomic 来实现原子操作,会有利于 skynet 未来的发展。上周花了点时间熟悉这套 api 并在 skynet 中实现。同时保留了之前的实现,如果编译器定义了 __STD_NO_ATOMICS__ 就会切换回老版本。

阅读全文 "把 skynet 的原子操作换成了 stdatomic " »

January 05, 2021

预运算地图寻路的一种方法

上次谈到服务器上寻路算法的优化。文末提到,其实,针对具体需求,我们实际上可以预运算所有的路径,把结果持久化在文件中,运行时用 O(1) 的时间就可以查询到任意路径。

对于半径为 N 的地图网格,选取任意两点作为起点和终点,一共有 N^4 数量级的路径,直接按起点和终点来缓存路径显然不现实。

但实际上,除非整张地图是个复杂的迷宫,大部分路径是重复的。这和人实际上行路是一样的。如果你从家中的卧室去到办公室的工位上,整条路径和你从客厅出发是高度重叠的;仅有出家门前的那一小段略有不同。

在服务器上寻路,通常解决的是 NPC 的小范围内移动绕开障碍(从卧室到家门)的问题,远距离移动的路线(从家到公司)应当在另一个层面去解决。

基于网格寻路无论是用 A star 还是基于图的 dijkstra 算法都非常简单,这里不谈算法,只谈结果的持久化问题。

为了解决起点和终点有 N^2 种可能性,这个数量级太大的问题,我们可以适当的将地图划分成若干区域。如果每个区域都是一个内部没有任何障碍物的凸多边形,那么,当我们的起点和终点都落在同一个区域时,连接两点的直线就是最短路径。

阅读全文 "预运算地图寻路的一种方法" »

December 17, 2020

内存的惰性初始化

这两天和同事讨论一个问题,我写了个小玩意。

事情起因是,我们公司上海的工作室的一个 MMO 项目做服务器压力测试。谈及优化,涉及到服务器中使用的 C 模块。他们把同一套 C++ 加上 namespace 编译了很多份,供多个服务使用。我很好奇,一般来说,Lua 的 C 模块是可以供多个 vm 共用的,并不需要实际链接很多份。仔细探究发现,原来这个代码中用到了一些全局对象(singleton 模式)。

我本能的觉得全局对象的设计中透着糟糕的味道,在逐个分析每个全局对象的必要性时,发现了一个有趣的东西:寻路模块。

阅读全文 "内存的惰性初始化" »

December 04, 2020

粒子系统中的材质组织

粒子系统中,势必会引入多种材质。要么按材质分为不同的管理器对象,要么把所有粒子片放在一个管理器下,但增加材质的属性。

如果是前者,即使粒子的其它属性都有共性,也无法一起处理;而后者,则涉及材质分类的问题。

我们不大可能在渲染阶段无视粒子的材质属性,每个粒子片都单独向渲染器提交材质。因为无论是面片粒子,还是模型粒子,都应该批量提交粒子的空间结构数据,然后一次渲染。如果粒子是面片,那么就应该把一组粒子的顶点信息组织在同一个顶点 buffer 中;如果粒子是模型,就应该把每个个体的空间矩阵组织在 Instance Buffer 中。

如果材质属性只是一个 id 或材质对象指针,作为一个属性关联在粒子对象上的话,不同材质的粒子是无序的,怎样的数据结构可以方便管理呢?

阅读全文 "粒子系统中的材质组织" »

November 27, 2020

粒子管理器的 C++ 封装

这篇接着上一篇 粒子系统的设计

TL;DR 在花了一整个晚上用 C++ 完成了这一块的功能后,我陷入了自我怀疑中。到底花这么多精力做这么一小块功能有意义么?强调类型安全无非是为了减少与之关联的代码的缺陷,提高质量;但代码不那么浅显易懂却降低了质量。

我们用 C 实现了一个基于 ECS 结构的粒子系统的管理器,代码 psystem_manager.h 在这里

先来回顾一下设计:在这个粒子系统中,我期望把粒子对象的不同属性分开管理。

即:传统的面向对象的数据结构中,一个对象 particle 可以有很多属性 a,b,c 。通常是用一个结构体(或类)静态定义出来的,这些属性也可以看作是 a b c 组件,它们构成了粒子对象。而在 ECS 结构中,我们在每个时间点,并非去处理一个对象的多个属性,而是处理同一个属性的多个对象。所以,我们最好按属性分类将多个对象的同一属性聚合起来,而不是按对象,把同一对象的不同属性聚合在一起。

这是因为,在处理单个属性时,往往并不关心别的属性。比如,我们在递减生命期,处理生命期结束的对象时,关心的仅仅是生命期这个属性;在处理粒子受到的重力或其它力的影响时,我们只关心当前的加速度和速度;在计算粒子的空间位置时,只关心上一次的位置和瞬间速度;而在渲染时候,无论是生命期、加速度、速度,这些均不关心。

当数据按属性聚合,代码在批量处理数据时,连续内存对 cache 友好,即使属性只有一个字节,也不会因为对齐问题浪费内存。同一属性的数据尺寸完全相同,处理起来更简单。而且粒子对象相互不受影响,我们只是把同一个操作作用在很多组数据上,次序不敏感。非常适合并行处理。

更重要的是,不同类型的粒子需要自由的根据需要组合属性和行为。有的粒子有物理信息参与刚体碰撞运算,有的则只需要显示不需要这个信息;有的粒子有颜色信息,有的不需要有;有的粒子是一个面片,有的却是一个模型,拥有不同的材质。这导致粒子对象包含的信息量是不同的。及时拥有同一属性,作用在上面的行为也可能不同:例如同样是物理形状信息,可能用于刚体碰撞,改变运动轨迹,也可能只是为了触发一下碰撞事件。

在传统的面向对象的方式中,常用多态(C++ 的虚函数)来实现,或者有大量的 if else switch case 。

如果能按组件和行为聚合,那么就能减少大量的分支。每个粒子的功能组合(打开某个特性关闭某个特性)也方便在运行时决定,而不用生成大量的静态类。

阅读全文 "粒子管理器的 C++ 封装" »

November 19, 2020

粒子系统的设计

这几天在重构引擎中的粒子系统。之前用 lua 做了个原型,这次用 C/C++ 重新实现一次。目前还是基于 CPU 的粒子系统,今后有必要再实现基于 GPU 的版本。

去年写过一篇 blog 也是谈粒子系统的 。 思路大致类似,但这次在数据结构的细节上做了一些专门的设计,有觉得还有点意思,值得写写。

首先,粒子对象本身就是一个集合了多种数据的数据块。我限制了同时最多 64K 个粒子片,这些粒子对象可以放在一块连续内存中,并且可以用 16bit 的 id 进行索引。

阅读全文 "粒子系统的设计" »

November 08, 2020

Lua binding 的一些方法

这几天在给 RmlUi 这个库写 Lua binding 。

这个库原本有一个官方的 lua binding ,但是新特性 Data Model 却没有实现。作者坦承自己对 Lua 不是特别熟悉,这个新特性也在开发中,暂时没想好该怎么做,所以只完成了 C++ 接口,Lua 接口留待这方面跟懂的人来做。

我觉得这个新特性有点意思,打算帮助这个项目实现 Lua 接口。在实现的过程中,发现原版的 Lua binding 做的过于复杂,且那些复杂度完全没有必要。所以打算自己全部重新实现一套。

阅读全文 "Lua binding 的一些方法" »

October 20, 2020

skynet 1.4.0

又是一年过去了,skynet 目前保持着一年一个发布版的开发进度。skynet 1.4.0 发布版将于近期冻结。

这次的主要更新是将 Lua 更新到了 5.4.2 (尚未发布,但 github 仓库中的版本号已经到了 5.4.2 )。可能会让 skynet 的许多项目享受到分代 gc 的好处。如果使用大量 agent 服务的模式,将会降低整体的内存峰值开销(GC 更加及时)。lua 5.4 中 table 的内存开销也比之前的版本要小,运行性能也有所提升。

升级到 lua 5.4 基本不需要修改过往的 Lua 代码。C 库需要重新编译,但基本不需要修改。但如果可以改用新版的 lua_newuserdatanv 取代 lua_newuserdata 会更好。

skynet 依然提供了针对多 vm 共享 proto 的补丁。和以前一样,这是一个可选项,可以自行编译官方版本的 lua 。

阅读全文 "skynet 1.4.0" »

October 18, 2020

cache server 问题总结

这周,我们的 cache server 服务面临了很多的挑战。项目资源超过了 30G ,有几十个用户在同时使用。每天都有版本切换工作(导致重新上传下载 30G 的数据)。在这个过程中,我对 cache server 程序修修补补,终于没有太大的问题了。

总结一下,我认为 cache server 的协议设计,以及 Unity 客户端实现,均存在很大的问题。这些问题是无法通过改进服务器的实现彻底解决的,只能做一些缓解工作。真正的完善必须等 Unity 的客户端意识到这些问题并作出改进。

cache server 的协议设计非常简陋。就是顺序的提交请求,然后每个请求会有序的得到一个回应。这些请求要么是获取 GET 文件,要么是上传 PUT 文件。其中 PUT 文件在协议上不必回应。

由于 PUT 文件没有回应,所以客户端无法直接确定文件是否全部上传完毕;如果必须确认,只能在 PUT 文件结束后,再提交一个 GET 请求。如果收到了后续 GET 的回应,可以理解为前一个 PUT 已经结束。实际上,Unity 客户端没想去确认 PUT 是否结束,从 log 分析,它只是简单的在最后一个 PUT 结束后等待了一段时间再断开连接。

PUT 实际上是个小问题,真正的问题是:这种依赖严格次序的协议,在面对两边数据量不对等、网络速度不对等的近况时,很难有一个健壮的实现。

阅读全文 "cache server 问题总结" »

September 21, 2020

skynet 版的 cache server 引出的一点改进

我们自己做的 cache server 已经工作了很长时间了。上次出问题是在 2 月在家工作期间

这个月又出了一起事故,依旧是 OOM 导致的崩溃。一开始,我百思不得其解,感觉上次已经处理完了所有极限情况,按道理,这是个重 IO 而轻内存的业务,不太可能出现 OOM 的。

通过增加一些 log 以及事后的分析,我才理解了问题。并对应做了修改。

阅读全文 "skynet 版的 cache server 引出的一点改进" »

September 03, 2020

为 skynet 增加并行多请求的功能

skynet 在设计时,就拒绝使用 callback 的形式来实现请求回应模式。我认为,callback 会导致业务中回应的流程和请求的流程割裂到两个执行序上,这不利于实现复杂的业务逻辑。尤其是对异常的支持不好。

所以,在 skynet 中发起请求时,当前执行序是阻塞的,一直等到远端回应,再继续在同一个执行序上延续。

如果依次发起请求,会有不该有的高延迟。因为在同一个执行序上,你必须等待前一个请求回应后,才可以提起下一个请求。而原本这个过程完全可以同时进行。

但是,如果我们想同时发起多个不相关的请求就会比较麻烦。为每个请求安排一个执行序的话,最后汇总所有请求回到一个执行序上又是一个问题。目前,只能用 fork/wait/wakeup 去实现,非常繁琐。

这类需求一直都存在。我一直想找到一个合适的方法来实现这样一类功能:同时对外发起 n 个请求,依回应的次序来处理这些请求,直到所有的请求都回应后,再继续向后延续业务。实现这样的功能在目前的 skynet 框架下并不复杂,难点在于提供怎样的 api 形式给用户使用。

阅读全文 "为 skynet 增加并行多请求的功能" »

August 19, 2020

银河竞逐的设计

银河竞逐 RFTG 是我最喜欢的桌游之一。我认为直到今天,它仍旧是最棒的引擎建造类卡牌游戏。最近,我看了银河竞逐的作者 Tom Lehmann 在 GDC2018 上的演讲 非常有收获,所以写这篇 Blog 分享一下。

策略竞争类的游戏一定要设计多种结束条件。RFTG 的基础版设计了建造出 12+ 张卡结束和分完 12n 个 VP 结束。基础策略就可区分为快速打出一堆小分卡,还是构造一个 VP 引擎。我最近玩得比较多的五扩(Xeno Invasion)还增加了击败(或被击溃)Xeno 结束。

这样可以使得创造出 strategic tension 。

阅读全文 "银河竞逐的设计" »

Misc

Categories

Archives

Recent Comments