从零开始的 RPG 游戏制作教程 #11 排泄、运行效率

作者:DreamerWQ
2021-04-02
2 0 0

往期回顾

第十一期:排泄、运行效率

魔兽争霸官方对战平台一直在招募游戏开发团队,如果对开发 RPG 地图有兴趣的,可以直接跟他们联系,邮箱:warcraft3worldedit#service.netease.com# 记得换成 @

视频游戏的本质是应用程序,是软件,这是不可否认的。

魔兽地图是魔兽争霸 3 的衍生品,魔兽争霸 3 是应用程序,是软件。

也正因此,我们所制作的地图必然地会受到其作为应用程序的限制。

魔兽争霸 3 会读取魔兽地图的数据,并执行地图内包含的脚本。

当游戏按照脚本演算后,所得到的就是我们的游戏世界。

(上图为通过 w3x2lni 工具将一个魔兽地图解包后,所呈现出的、一个魔兽地图中包含的数据文件)

理论上,一张魔兽地图在游玩的时候,并不会带来非常庞大的效率问题,因为标准的、由暴雪提供的魔兽地图往往会在玩上一小段时间后保存、退出游戏,下次游玩的时候才会再载入地图。

这样就不至于一大堆数据堆积在内存里,使得游戏越来越卡顿。

不过,随着玩家社区对魔兽地图编辑器的挖掘,越来越多时长为 60 分钟、120 分钟,并且体积逐渐庞大(从开始的 2、3MB,到后来突破 8MB,到现在平台上随处可见的 100-200MB)的地图浮出水面。

同时在时长范围内,一些追求华丽效果或者丰富系统的制作者还会竭尽所能地往里面添加大量特效、动画、文本渲染、单位创建与删除等等逻辑。

而魔兽争霸 3 的内存回收机制,并不会判断一个【对象】是否长期没有被使用。而在魔兽地图编辑器的触发器中,又有许多函数能够【创建对象】。

在所有这些对象中,最典型的即是【点】对象。


创建一个单位在区域中心,这时,【区域中心】会生成一个点,但这个点会一直在内存里保留。

指定单位移动到区域中心,这时也会生成一个点。

创建物品在单位所在位置,这时也会生成一个点。

……


换言之,在所有涉及到【位置】的操作中,每次执行这么一次操作,魔兽争霸就会创建一个【点】在内存中。

点是一个包含了 X、Y 坐标的数据对象,它是一个可以被储存为变量的数据,并且只要这个点没有被删除,它就会一直待在内存里,等待被再次读取。

显而易见的,我们的地图制作者们大多一开始并不会意识到这个问题,因为魔兽地图编辑器中并没有说明书指出点堆积过多会导致什么问题。


在触发器动作中找到【点】分组,可以看到这里有【移动点】和【清除点】。


以创建一个单位为例,让我们看看应该如何防止点堆积在内存中。

在创建单位动作中,我们需要填写【指定点】参数。

点击 这个参数,找到常用的【矩形区域中心】函数。

可以看到,在这条函数下方有一个注释:会创建点。

这个含义即是说明,如果使用了这条函数,魔兽争霸 3 游戏就会创建一个【点】对象在内存中,供接下来的脚本使用。


如果像上图一样填写,就意味着每次执行这条动作的时候,我们的内存中也就“永久”地增加了一个【点】——直到游戏崩溃或者退出当前地图。

要避免这个问题,我们就需要获取每一个【被创建的点】,把它保存到某个变量中,并且在使用后删除它。

创建新动作,找到逆天局部变量。

设置类型参数为【点】,在这里我们使用 p 作为变量名。

然后我们要将创建单位动作中的点参数修改为逆天变量 p

如图所示,便和先前的功能相同了。

然后在使用完了这个点后,我们要清除它。

如此,我们便得到一个完整的没有点泄漏的创建单位动作。


除了点泄漏以外,常见的泄漏还有单位组泄漏、计时器泄漏、特效泄漏。

它们的清理方式分别如下:


单位组泄漏:

逆天计时器泄漏:

特效泄漏:

其中单位组泄漏往往在群体类效果的逻辑中出现。

当使用选取区域内单位的时候,这些单位都会被获取并存放到一个新创建的单位组中,从而我们可以通过该单位组来索引所有符合我们需要的单位,并为这些单位执行一段特定的逻辑。

不过和点一样,单位组也是一种【对象】,会被创建到内存中。

只是点储存的数据是两个坐标,而单位组储存的数据是数量不固定的单位。

逆天计时器则是 YDWE 提供的一种拓展计时器,能够轻易地传递自定义数据。

(在 JASS2,即魔兽争霸 3 所使用的游戏脚本语言中,并没有【跨域局部变量】的概念,我们在后续会讲解魔兽争霸 3 的脚本语言的概念)

如果创建了一个逆天计时器,而我们不去手动清除它,那它也会作为一个对象被放置到内存中持续存在,从而占用我们的资源。

同理,特效也一样。


如果我们翻阅由魔兽争霸 3 提供的基础脚本定义文件(common.j),我们会发现几乎绝大多数 jass 内的变量类型均由一个 handle 变量类型拓展而来。

只有 integerrealstringboolean 这类基础类型的变量,并不是由 handle 拓展而来的。


而如果我们搜索 handle,则会出现这样的定义。

不过本期并不打算就魔兽争霸 3 的脚本原理进行介绍,因此我们先暂时打住。

回到我们的正题,即所有继承自 handle 的对象,我们如果不再使用它了,都应当考虑【删除/清除】它。


我们 点击 触发器的菜单 TESH,并打开 Function List,搜索英文:Remove,我们将会得到一大堆用于删除特定对象的函数。

而如果我们搜索英文:Destroy,也会出现许多摧毁特定对象的函数。

换言之,只要是继承自 handle 类型的对象,理论上都会有对应的清除函数。


让我们用术语来小结本期所讲的内容,即是【内存管理】。

关于内存管理的更详细介绍,在网络上有大量文章可以查找到。

由于技术的局限性,魔兽争霸 3 即便运行在高配置电脑上,当其内存泄露达到一定量级时,游戏本身仍然会变得极其卡顿。

而魔兽争霸的脚本又并未实现现代编程语言中常见的【垃圾回收】机制,这也正是我们不得不手动去管理游戏对象的清除的原因。


不过,即便我们已经提了这么多关于数据泄露的问题,我们仍然要客观地看待这个事情。

对于技术工作者而言,注重编写的代码的质量、完整性、效率是非常重要的。

但对于游戏制作者而言,尤其是项目负责人而言,有时候必须根据项目的实际制作周期、能够投入的精力、资源,去取舍地选择某些问题是否要解决或不解决,即有时候我们必须在程序运行效率和项目制作效率之间做取舍。

因此本期提出了关于内存管理的概念,但具体在实践中如何把握这种【度】,仍然是需要制作者通过大量自身的实践经验来权衡的。


在魔兽地图编辑器社区,人们往往把【排泄】作为关键词进行讨论。

关于【排泄】的详细介绍,在社区内有许多人已经撰写了更多更详细的实践教程,如果你感兴趣,可以自行搜索这些资料。


另,内存管理的内容也并不局限于排泄,根据游戏的体量不同,一个游戏对于内存管理的要求程度也不同。

不过,内存管理属于更专业的技术研究话题,本教程仅作基本介绍,也在此打住了。