(文章略长,请先有心理准备)
# 前言
当 RPG Maker 改以 JavaScript 为游戏语言之后,自认对 JS 有些许了解,我每隔一段时间便会兴起加入 RM 圈,制作游戏、撰写插件的想法。
想来要做点小插件应该不是难事吧。
但现实残酷,我总是没办法坚持下去(正因这一点,我相当㐽服巴哈、Discord 上面各个正爆肝制作中或甚至已经完成过游戏的人)。
JavaScript 大概是世界上最易于学习的语言之一了,然而要读官方的程序逻辑与 JS 能力其实关联不大——里面用的写法都很简单,只是粗糙且不必要地复杂。只要有基础,理应慢慢能懂。
那为什么我还是会失败放弃呢?要怎么样才能用简单的程序,做出插件,修改缺省功能?
我打算以我从全然不懂的 RM 新手,直到成功开始制作插件的这段经验,写点介绍与方法。需要注意的是,我在这以前已经学过了 JS ,我比较没有「看不懂一段程序在做什么」的困难。
如果你为了 RPG Maker 而开始学习 JavaScript ,正想知道如何在这里中活用你的编程技能,那也许我可以给你一个参考起点。
如果你没有 JavaScript 或其他语言的编程知识,那接下来可能会有相当多领域术语,如果遇到困难,无论如何都难以理解,那我建议可以先从语言本身开始学起(可以参考 [MDN] 网站),这篇文章并不是语言教学。
如果你已经对编程语言有相当了解,但对 RPG Maker 的代码并不清楚,那从第二章开始应该可以作为参考。
如果你已经是插件界的老手……这篇文章是写给新手看的喔,对你来说可能会有很多无趣、乏味的内容。
任何人发现了文章中的错误或想提供补充参考,小至文本误键,大至观点偏误,我随时欢迎指教。
# 1. 为什么会失败
当我反思自己在 RM 上屡屡放弃的主因,大概便是那长到吓人的代码。一上来就是强大的心理压力,实在吃不消。
让我们计算一下每个文件的行数,这些数字经过四舍五入,因为官方目前仍持续更新版本,它们随时可能变化。
“`txt
main 150
plugins x
rmmz_core 6400
rmmz_manager 3100
rmmz_object 11300
rmmz_scenes 3600
rmmz_sprites 3700
rmmz_windows 6600
“`
加总后近三万五千行。就算我读了,也记不住,怎么办呢?
如果你想透过官方文档来学习代码,那可能要失望了。文档中仅包含了 `rmmz_core.js` 6400 行程序,其余部份一字未提,但那才是形塑 RM 系统的主要部份啊。
这就好像电脑的维修手册上,居然是高中电学一样。不,我想知道它的主控板、内存,乃至操作系统的规格。
而解方很简单,就是别读。
研究目录页,对这本书的基本架构做概括性的理解。这就像是在出发旅行之前,要先看一下地图一样。
——《如何阅读一本书》P.42
在面对他人所写的代码时,也同样可以从目录来大略了解它写了什么,不需要立刻就读完上万行代码,只要等到需要的时候,再以查找百科全书的心态来探索就好。
# 2. 了解如何取值
当然,我们终究要读一些程序。既然没有看,知道自己要查什么东西就变得很重要。
### 变量、开关与角色
在 `rmmz_object.js` 里,可以看到像是:
– `Game_Variables`:控制游戏变量。
– `Game_Switches`:控制游戏开关。
– `Game_Actor`:控制游戏角色。
所有这样的单例,在运行期会被放入以 `$game` 开头的变量中。你可以开启一个项目,在测试运行的控制台中察看。
至于我们在编辑器数据库接口规划的道具、角色等等设置,则是存放在以 `$data` 开头的参数。
因此,我们能用如下的片段,在 RM 中玩 FizzBuzz :
“`js
(() => {
// 取得1号变量。
const value = $gameVariables.value(1)
// 依据规则,返回数字对应的字符串。
function getValueSign(rules, value) {
const res = Object
.keys(rules)
.reduce((res, key) => value % Number(key) === 0
? res + rules[key]
: res
, “”)
return res.length === 0 ? value.toString() : res
}
// 以上限值取得一段范围的 FizzBuzz 字符串。
function getFizzBuzzByRange(limit) {
const rules = {
3: “Fizz”,
5: “Buzz”,
}
const getFizzBuzz = value => getValueSign(rules, value)
return […Array(limit)]
.map((_, idx) => getFizzBuzz(idx + 1))
.join(“\n”)
}
// 依据1号变量产生对应 FizzBuzz 字符串。
const result = getFizzBuzzByRange(value)
// 设置2号变量。
$gameVariables.setValue(2, result)
})()
“`
### 如何找到自己需要的值
在 `rmmz_object.js` 中,我们可以找到大部份所需的东西,甚至是事件页功能的对应函数也有。
而 `rmmz_scenes.js` 、 `rmmz_sprites.js` 、 `rmmz_windows.js` 三个文件则是负责处理画面、接口等等功能。这部份比较复杂些,不在本文的介绍范围内。
你可以透过浏览大纲,以了解那些功能或事物在 RM 代码中的具体用词,以便在遇到相关问题时知道以什么关键字搜索。
值得一提的是,那些事件功能对应的函数有部份可能并非那么开箱即用,因为它们是以「在事件对象环境下运行」为前提而撰写。
例如:若我想在运行 FizzBuzz 函数时立刻看到结果,又懒得再加显示消息的事件指令。但是直接使用显示消息的 `command101` 函数却会报错,因为它想提前看到下一条事件指令,以准备像是选择项之类的情况。
于是我需要检查消息系统,并写成这样:
“`js
(() => {
// …
// 设置为滚动字幕
$gameMessage.setScroll(5, false)
// 加入至待处理的文本串行中。
$gameMessage.add(result)
})()
“`
# 范例1:清除画面上的所有图片
### 目标与猜想
假设我们制作的游戏使用了大量图片来组合成独特的接口,这些图片还可能因为条件不同,于是编号也不同。那当要关闭接口的时候怎么办?
仅管当然可以老老实实写好几条指令把图片遂一关掉,但这样既浪费性能、容易遗漏,如果要改接口,也不方便。
由于图片的运作方式我们可以猜测,它可能是某种数组。因为在编辑器中,我们只能透过「编号」来指定图片,它没有名称标签,而对于这类数据最简单的方法就是数组——它不会无缘无故用对象来装以编号为键的东西,在范例2中我们可以看到另一个例子。
### 查找自己需要的程序段
既然是数组,那一定有一个地方写着 `变量名称 = []` 。而程序中提到图片的有两个地方:
– `rmmz_manager.js` 的 `ImageManager`
– `rmmz_object.js` 的 `Game_Picture`
但整段浏览下来,`ImageManager` 其实是用来加载图片文件;`Game_Picture` 则是包装图片应该如何显示的数据。
那就搜索 `picture` 这个关键字吧。看样子应该有个变量,会被用来装一大堆 `Game_Picture` 才对。但图片属于游戏运行过程中才逐一加入的对象,可能没办法用 `Game_Picture` 直接找到目标。
等等,`picture` 在 `rmmz_object.js` 居然使用了超过一百次?
我们可以发现除了 `Game_Picture` 之外,用最凶的是 `Game_Screen` ,而正好就在第一个搜索结果,我们有 `this.clearPictures();` 。
“`js
Game_Screen.prototype.clearPictures = function () {
this._pictures = [];
};
“`
### 确认效果与保险起见
就是这个了,我们只需要调用 `$gameScreen.clearPictures()` ,游戏在下一帧就会把图片清除掉。
保险起见,让我们再深入一点,万一它在删去图片以前,还需要做什么事情呢。
就在这段下方,写着 `Game_Screen.prototype.eraseBattlePictures` ,这是用来清除战斗时的图片,它十分简单地用了 `Array.slice` 。
看来我们不用担心太多,直接跳到第二个带有 `erase` 的 `Game_Screen.prototype.erasePicture` ,这里更是直接把对应元素设置为 `null` 而已。
所以没问题,我们可以直接使用 `$gameScreen.clearPictures()` 来删除所有图片。安全、干净、无污染。
# 小结:视为香料调味
就像上面的范例都可以使用事件的脚本选项运行,在许多功能中,编辑器都留下了可以写「脚本(或者叫做 Snippet)」的空间,也许是在技能伤害公式、道具效果运行公共事件(又译做一般事件)、条件判断使用脚本等等。
如果编辑器里面没有我们需要的数据字段——也许我们想让主角的名字,依据武器装备而不同——那也可以到武器的「注释」字段中定义,如 `<name:小明>` (如果冒号后面有空格,也会一并包含进去),并用 `$dataWeapons[武器编号].meta` 得到 `{name: “小明”}`。
只要能够活用这些功能,就能简化许多原本需要复杂过程的做法,提供更多可能。