RMMZ脚本教程-基础篇

RMMZ脚本教程-基础篇

写在前面

时光荏苒,距离上次写 RMMV 教程居然已经过去了三年多…三年间我由一名大一新生变成了大四老油条,期间由于我在 RMMV 中对 JavaScript 脚本的学习而免试加入了校科协的前端组(虽然半年后就跑路到了算法组),此后也或多或少做了些前端项目,还拿 Node.js 完善了项目的后端…总之 RPG Maker 系列软件与 6R(Project1)论坛让我发现了对编程的兴趣,影响了我高中时选择参加信息学竞赛、大学报考计算机专业直至现在。
而三年后的现在,恰逢此次评测活动,感谢 @梦结界工作室 和 @Project1 论坛支持的 RMMZ 软件,我决定再次回归到 RPG Maker 系列软件脚本教程的撰写,为 RPG Maker 系列软件的蓬勃发展做一点微小的贡献。
我在这次的教程选题时做了诸多思考,在翻看自己珍藏多年、初中时手抄的脚本教程(彼时不能随时使用电脑,被迫如此)时,发现了 RMVA 官方脚本教程的身影。教程分为「基础篇」、「解读篇」与「实践篇」三部分,由浅入深介绍了从 Ruby 语法到 RGSS3 系统组成再到对其的修改(即编写脚本)。令人惋惜的是在 RMMV 之后的文档这部分内容便销声匿迹了,因此我希望能够借此机会为 RMMZ 补充这样一份教程,即“RMMZ 味”的 RMVA 脚本教程,以期能够抛砖引玉为 RMMZ 吸引更多插件开发者与教程创作者。

注意事项

  1. 本教程复刻于 RMVA 帮助文档中脚本入门的章节,可以说是该教程直接翻译成 MZ 的版本。教程分为「基础篇」、「解读篇」与「实践篇」三部分,这篇文章为「基础篇」,另外两部分将稍后发布。
  2. 教程中会用到的资料/参考链接/推荐阅读:
  3. 因为本人能力有限且粗枝大叶,故教程中或许一定会有错误,请大家批评指正~
  4. 本教程同步发布于 Project1论坛 和我的博客青い記憶
  5. 感谢 @梦结界工作室 和 @Project1 论坛支持的 RMMZ 软件,感谢 Copilot 和 New Bing 为本教程的撰写提供的帮助。

正文

新的开始

在基础篇,我们将一边完成较短的程序一边介绍 JavaScript 以及 RMMZ 核心脚本的基础知识。
首先要做好学习的准备,请提前准备好 RMMZ 软件本体、一款趁手的代码编辑器(本教程使用的是 VSCode,至少别是记事本)、好学的大脑以及勤奋的双手~

建立项目与脚本

既然是新的开始,那么我们从头建立一个学习脚本入门用的新项目。建立项目后,在主菜单【游戏】中选择【打开文件夹】,在弹出的资源管理器窗口内进入【js\plugins】目录,这里便是 RMMZ 存放插件的地方啦~可以看到已经有四个官方为我们写好的插件:AltMenuScreen.jsAltSaveScreen.jsButtonPicture.jsTextPicture.js,在后续的实践篇中或许有机会向大家讲解这四个插件是如何编写的。
今天,我们先建立一个新文件。脚本文件的名称是什么无所谓,这里为了便于理解,我们暂且命名为 TEST.js(注意扩展名一定要是 js 结尾),然后使用代码编辑器打开这个文件(也可以在代码编辑器里面新建一个 js 文件然后保存到插件目录中)。

打开插件

此时,再次回到 RMMZ 在主菜单【工具】中选择【插件管理器】(也可以直接按 F10),在空白处双击,点击名称,就能在弹出的菜单中看到我们新建的名为 TEST 的插件了,请选择这个插件并保持开启(这时候插件管理器的下方会提示无法加载插件“TEST”,这是正常现象),然后就可以正式开始我们的脚本学习啦!

注释

在 JavaScript 中,// 后的一整行文字或被 /**/ 夹在中间的若干行文字会被认定为注释
作为注释的代码或文字不会被执行,不会影响到程序的运行,只作为编写者标记的备忘录来使用。

/* 我是一句块注释
我也是一句块注释
我还是一句块注释*/
// 我是一句行注释,我并不会被执行

这两种方式和事件指令的“注释”功能相同,是记录程序处理内容的简单说明,使用注释会很容易知道这段脚本是做什么的。即使是自己编写的程序,但时间一长,该段内容是指定做什么处理也会忘记。在脚本的学习和实际操作时,推荐注意多使用注释功能。
还有,作为程序的说明,在后面的学习中脚本会写有很多的注释。但实际操作中没有必要完全仿效其注释的内容。// 以后的文字,可以按实际中最容易理解的说明注释。
除此之外,当在学习脚本的时候遇到想要使程序的某个部分暂时不运行时,也可以使用 /**/ 将代码包裹起来(或在逐行在代码前加上 //),这是调试脚本和排查 Bug 时的常用技巧。

语法和库

等等…在真正开始学习前,先插播一条关于程序语言的小知识:程序语言在结构上大致分为语法两种。
所谓语法,就是用语言表达某种意思时所要遵循的规则。与中文、英文等自然语言一样,程序语言也有语法,例如条件分支的语法、循环的语法等。上面所讲的「// 后面的一整行文字都是注释,不会被执行」则是语法之一。语言的说法,狭义上指的就是语法。
所谓,就是像图片的显示、这样带有应用功能的语言集。与中英文等自然语言中的单词和成语的意思类似。学习了英语的语法在使用英文时,增加词汇量(掌握库的使用方法)则是非常重要的。
本教程的基础篇是以学习 JavaScript 语法为重点。最开始学习时或许会产生「学习语法,到底有什么用」的厌烦感,应用逻辑思维来理解基本语法就能突破最初的屏障,仔细阅读参考程序记住实际的使用方法,其实并不是多么困难的事情。
那么让我们开始吧!

数值计算

这是关于 JavaScript 中数字和计算的基础知识。作为例子使用计算器那样简单的计算来解说。

显示开发者工具

在测试游戏时,你可以按【F8】或【F12】来打开开发者工具,这将是我们调试脚本的有力工具。
开发者工具的作用十分强大,包括元素查看器(Elements)、调试控制台(Console,可以查看输出、执行脚本等)、源代码查看器(Sources)、网络资源调试器(Network)、性能表现(Performance)等众多方便实用的功能。在我们的教程中,经常用到的时调试控制台(Console),用于查看运行过程中的数据、文本打印结果。

数值

在其他编程语言中,你或许会了解到许多不同的术语来描述不同类型的数字,例如整数(例如 10、400、-5)、浮点数(有小数点或小数位,如 12.5、114.514)、双精度(一种特殊类型的浮点数,他们比标准浮点数精度更高,可以精确到更大的小数位数)。在开始担心弄混上面这些概念之前,请直接忘掉他们吧!JavaScript 中只有一个数据类型 Number 来表示数字(内部均为双精度浮点类型),任何类型的数字都可以用完全相同的方式处理它们。
此外,JavaScript 的数字支持四种进制:十进制、二进制(如 0b101)、八进制(如 0377)、十六进制(如 0xff)。但在教程中我们一般使用的是十进制(也许颜色色号会用到十六进制)。

显示

空说这么多挺没意思的,让我们实际将数字显示出来吧:在控制台中打印数值时,可以使用 console.log 命令。
请在脚本中添加以下代码:

console.log(233);

然后测试游戏,按【F8】打开开发者工具,并切换到【Console】选项卡,就可以看到控制台中打印出的文字了!
也许你注意到了,这个语句的结尾是有一个分号 ; 的,这是 JavaScript 中的语法,用于表示语句的结束。JavaScript 中的语句可以不用分号结尾,因为 JavaScript 会自动在每行的结尾加上分号,但为了保证代码的可读性,我们建议在每个语句的结尾都加上分号。
在后面的教程中,我们也会用 console.log 打印各种调试信息,用于排除程序 Bug,是十分有用的一个指令。

计算方法

在 JavaScript 上进行计算吧:

console.log(1 + 1);         // 加法
console.log(114 - 514);     // 加法
console.log(7 * 5);         // 乘法
console.log(24 / 4);        // 除法

像上面这样写上普通算式就会输出计算结果。其中 +- 等符号被称为运算符

优先级

和普通算式一样,在 JavaScript 中,运算符的优先级是有规律的,优先级高的运算符会先被计算,例如先乘除后加减。想要改变这个顺序,可以使用括号来改变运算顺序。

console.log(1 + 2 * 3);     // 7
console.log((1 + 2) * 3);   // 9

括号还可以多层重叠起来使用(不需要跟数学中一样需要变成中括号、大括号)。

其他运算符

除了上面提到的加减乘除运算符,JavaScript 还有其他运算符,例如求余运算符(%,也叫取模运算):

console.log(4 % 3);         // 1
console.log(5 % 3);         // 2
console.log(6 % 3);         // 0

以及指数运算符(**):

console.log(2 ** 3);        // 8
console.log(3 ** 2);        // 9

此外,有时我们也会用到位运算符,例如按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)、右移(>>)、无符号右移(>>>),在此先不做赘述。

变量与常量

所谓变量,就是可以改变的量,它是一种可以存储数据的容器,可以在程序运行时改变其值,是相对于常量的数据类型。在 MZ 中,变量只能用来存储数字,对变量的操作也只是加减乘除取模这些操作,而在 JavaScript 中,变量可以存储任意类型的数据,例如字符串、对象、数组等(后面都会进行介绍),在本节中,我们先用数字来讲解变量的基本使用方法。

变量的命名

不同于 MZ 编辑器中变量只能以四位数字为代号,JavaScript 中的变量可以起符合以下规则的任意名字,并且从此以后和 MZ 中编辑器中的四位数字一样把这个名字作为“代号”使用:
首先,变量名必须以字母、下划线(_)或美元符号($)开头,不能以数字开头。
其次,变量名中可以包含字母、数字、下划线(_)或美元符号($),但不能包含空格或其他符号。
最后,不能使用 JavaScript 中的关键字作为变量名,以下是 JavaScript 中的关键字:

abastract   arguments   boolean     break           byte
case        catch       char        class           const
continue    debugger    default     delete          do
double      else        enum        export          extends
false       final       finally     float           for
function    goto        if          implements      import
in          instanceof  int         interface       let
long        native      new         null            package
private     protected   public      return          short
static      super       switch      synchronized    this
throw       throws      transient   true            try
typeof      var         void        volatile        while
with        yield

此外,变量名是区分大小写的,例如 aA 是两个不同的变量名。
正确的变量名示例:

a
a1
a_b
a$b
a1b2c3

错误的变量名示例:

1a      // 不能以数字开头
a b     // 不能包含空格
a-b     // 不能包含符号
let     // 不能使用关键字
声明变量

在 JavaScript 中,使用 var 命令来声明变量,例如:

var a;

这样就声明了一个名为 a 的变量,但此时变量 a 的值是 undefined,因为我们还没有为它赋值。
除了使用 var 命令声明变量,还可以使用 let 或通过直接赋值声明变量,let 的用法和 var 命令类似,但是它有一些不同的地方,我们会在后面的教程中详细介绍。

var a;      // 使用 var 命令声明变量
let b;      // 使用 let 命令声明变量
c = 1;      // 直接赋值声明变量
赋值和引用

下面是一个赋值和引用变量的例子:

var a = 1, b = 2;
console.log(a + b);

在这个例子中,我们在第一行声明了变量 ab,然后给它们赋值 12。其中,= 号被称为赋值运算符。和数学上表示“左边等于右边”的意思不同,这里表示“将右边的值赋给左边的变量”。
第二行是引用变量 ab 的值。所谓引用,就是使用变量的值。变量的运算和数字的运算是一样的,这里可以解释为 1 + 2,最后输出结果为 3。
另外,引用未定义的变量会使程序报错,例如:

console.log(xxx); // ReferenceError: xxx is not defined
常量

在 JavaScript 中,使用 const 命令来声明常量,例如:

const a = 1;

这样就声明了一个名为 a 的常量,它的值是 1,并且不能被修改。

简写运算符

在 JavaScript 中,除了 = 这个赋值运算符,还有其他的赋值运算符,例如 +=-=*=/=%=等。

var x = 1;
x += 7;
x -= 3;
x *= 5;
console.log(x); // 25

上面的代码中,第二行的 x += 7 等同于 x = x + 7,同样 -=*=/=%=**=<<=>>=>>>=&=^=|= 也是如此。这些运算符是 JavaScript 中的简写运算符,它们的作用是将运算符右侧的值与左侧的值进行运算,然后再赋值给左侧的变量。
这个示例中,第一行是定义变量 x 并赋值为 1,第二行加上 7,第三行减去 3,第四行乘以 5,最后输出计算结果为 25。

自运算符

在 JavaScript 中,还有一种特殊的运算符,叫做自运算符,它可以对变量进行自增(++)或自减(--)操作。

var x = 1;
x++;
console.log(x); // 2

上面的代码中,第二行的 x++ 等同于 x += 1,即将变量 x 的值加 1,然后再赋值给变量 x
自运算符有两种形式,一种是前置形式,即 ++x,另一种是后置形式,即 x++。我们暂时不用区分这两种形式,只需要记住,自运算符会改变变量的值。

变量的作用域

作用域是指变量的作用范围,变量的作用域决定了变量是否可以被引用。如果一个变量在它的作用域外被引用,JavaScript 就会报 ReferenceError 错误。
JavaScript 中有以下三种作用域:

  • 全局作用域:所有代码的默认作用域,具有全局作用域的变量在程序的任何地方都可以被引用。
  • 函数作用域:函数(后面会讲到)内部的作用域,具有函数作用域的变量只能在函数内部被引用。
  • 块级作用域:用一对花括号 {} 包裹的代码块(后面会讲到),例如 if 语句、for 循环等。
    在 JavaScript 中,在函数外声明的变量具有全局作用域,而在函数内部声明的变量具有函数作用域,特别地,只有 letconst 命令在代码块内声明的变量才具有块级作用域。
    虽然具有全局作用域的变量可以在任何地方被引用,但是这样做会使程序变得难以维护,因此,我们应该尽量避免在全局作用域中声明变量。
    undefined 和 null
    在 JavaScript 中,undefinednull 都表示“没有值”,它们都是原始类型的值。
    undefined 表示“未定义”,即此处的值尚未定义,如果一个变量没有被赋值,或者由 var 声明的具有全局作用域的变量尚未被定义,那么它的值就是 undefined
    null 以及两者的异同将在后面的教程中介绍。
    console.log(a); // undefined
    var a;
    console.log(a); // undefined
    a = 1;
    console.log(a); // 1
    

字符串

一般来说,由文字连接起来的数据就称为字符串

字符串的定义

在 JavaScript 中,字符串是用一对双引号 " 或单引号 ' 包裹的一串字符。
和数字一样,字符串也能代入变量为其赋值和通过 console.log() 输出。

var str1 = "Hello World!";
var str2 = 'Hello World!';
console.log(str1); // Hello World!
console.log(str2); // Hello World!

字符串也能进行加法运算,可以得到一个连接起来的字符串,例如:

var str1 = "Hello";
var str2 = "World";
console.log(str1 + " " + str2); // Hello World
转义字符

字符串中 \ 符号及其后面的字符称为转义字符,是作为一段连续文字中换行等操作使用的特殊文字,例如 \n 表示换行。
转义字符有 \t(Tab)和 \s(空格)等,另外 \n 表示换行,重叠起来的 \\ 表示 \ 字符本身。

var a = "Hello";
var b = "\\World";
console.log(a + '\n' + b);  // Hello
                            // \World
模板字符串

在 JavaScript 中,还有一种特殊的字符串,叫做模板字符串,它的作用是可以在字符串中嵌入变量。
模板字符串用反引号(`)包裹,其中的变量用 ${} 包裹。它可以让变量的值在字符串中显示出来。

var name = "World";
var str = `Hello ${name}!`;
console.log(str); // Hello World!

上面的代码中,第一行将变量 name 的值赋为 字符串 "World"。第二行里面,${name} 表示将变量 name 的值嵌入到字符串中,和前后的 Hello ! 合在一起,从而得到了一个新的字符串。RMMV 的事件指令「显示文字」里,在文本中使用 \V[n]\N[n] 可以将变量的值和角色的名字作为信息嵌入到文本中,这个功能与此非常相似。

分支条件语句

满足一定条件时,执行一段代码,不满足时,执行另一段代码,这就是分支条件

语句块

在讲分支条件之前,我们先来讲一下表达式语句语句块的概念。
在 JavaScript 中,表达式是指可以计算出一个值的代码,例如 1 + 1"Hello World" 等。
语句是指可以被执行的代码,例如 var x = 1;console.log(x); 等。
语句块是指用一对花括号 {} 包裹的一段代码,例如:

{
    var x = 1;
    console.log(x);
}

还记得我们在前面讲过的变量的作用域吗?在 JavaScript 中,只有 letconst 命令在代码块内声明的变量才具有块级作用域。这里的代码块就是一个语句块。

比较运算符

在具体学习处理分支条件之前,要先记住条件判断中所使用的运算符。
下面 这些运算符称为比较运算符。使用这些运算符,和给予的条件(数字和字符串)比较,然后返回结果。

运算符 作用
== 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于

这里一定要注意的是,判断相等的运算符不是 =,而是 ==。前面提到过,= 符号表示的并不是比较而是赋值,用于将右边的值赋给左边的变量。请一定注意不要混淆!
除此之外,还有一个运算符 ===,它和 == 的作用是一样的,关于详细的区别,我们将在后面的教程中讲解。

布尔值

比较运算的结果就称为布尔值,它只有两个值:truefalse
比较后是正确的话为 true 值,错误的话为 false 值。请在脚本中试试看吧。

console.log(3 + 1 == 1 + 3); // true  
console.log(3 + 1 != 1 + 3); // false
console.log('你好' == '你好'); // true
console.log('你好' == '再见'); // false
逻辑运算符

在 JavaScript 中,还有一些运算符,它们的作用是将多个布尔值进行逻辑运算,得到一个新的布尔值。

运算符 作用
&& “~ 与 ~”的意思
|| “~ 或 ~”的意思
! “非 ~”的意思

使用这些运算符就能指定更加复杂的条件。

console.log(3 + 1 == 1 + 3 && 114 < 514); // true
console.log(3 + 1 == 2 + 3 || 114 > 514); // false
console.log(!true); // false

上面的例子中,第一行表示“3 + 1 等于 1 + 3 同时 114 小于 514”,因为这两个运算都成立,所以输出结果就为 true(真、正确);第二行表示的是“3 + 1 等于 2 + 3 或者 114 大于 514”,因为比较运算符 \|\| 是只要有一个运算成立就为 true,而很显然两个运算都不成立,所以输出结果就为 false;第三行的 ! 运算符和另外两个运算符稍微有点不同,它表示的内容为后面的条件不成立或取反,可以理解为颠倒 truefalse 的运算符,因此第三行的意思就是“非真”,输出结果就为 false
此外,不只 +- 等用于计算的运算符具有优先级,比较运算符和逻辑运算符也具有优先级。目前只要知道 == 之类的比较运算符的优先级高于 &&\|\| 之类的逻辑运算符就可以了。

if … else 语句

if 语句是 JavaScript 中分支条件的语法,这个和英文中的 if(如果 … 就)的意思一样,表示“如果 if 后面的条件成立的话,就运行下面的代码”的意思。

var x = 1;                          // 定义变量 x 并赋值为 1
if (x >= 0) {                       // 如果 x 大于等于 0 的话
    console.log('x 大于等于 0');    // 显示 x 大于等于 0
}                                   // 结束 if 语句

在不满足条件也要进行处理的情况下,可以使用 else 语句。

var x = 1;                          // 定义变量 x 并赋值为 1
if (x >= 0) {                       // 如果 x 大于等于 0 的话
    console.log('x 大于等于 0');    // 显示 x 大于等于 0
} else {                            // 否则
    console.log('x 小于 0');        // 显示 x 小于 0
}                                   // 结束 if 语句

在满足多个条件的情况下,可以使用 else if 语句。

var x = 1;                          // 定义变量 x 并赋值为 1
if (x > 0) {                        // 如果 x 大于 0 的话
    console.log('x 大于 0');        // 显示 x 大于 0
} else if (x < 0) {                 // 除此之外,如果 x 小于 0 的话
    console.log('x 小于 0');        // 显示 x 小于 0
} else {                            // 其他,
    console.log('x 等于 0');        // 显示 x 等于 0
}                                   // 结束 if 语句
switch 语句

在条件为根据特定变量的值进入不同分支的情况下使用 switch 语句会更方便。

var x = 1;                          // 定义变量 x 并赋值为 1
switch (x) {
    case 0:                         // 如果 x 等于 0 的话
        console.log('x 等于 0');    // 显示 x 等于 0
        break;                      // 结束 switch 语句
    case 1:                         // 如果 x 等于 1 的话
        console.log('x 等于 1');    // 显示 x 等于 1
        break;                      // 结束 switch 语句
    case 2:                         // 如果 x 等于 2 的话
        console.log('x 等于 2');    // 显示 x 等于 2
        break;                      // 结束 switch 语句
    default:                        // 其他情况
        console.log('x 不等于 0、1 或 2');  // 显示 x 不等于 0、1 或 2
        break;                      // 结束 switch 语句
}

把第一行的 var x = 1; 改成 var x = 2;,再运行一次,然后再来看看输出结果是什么。当 x 代入 1 时会输出“x 等于 1”,当 x 代入 2 时会输出“x 等于 2”。
需要注意的是,switch 语句中的 case 语句后面的 : 是必须的,而 break 语句也是必须的,break 前可以写任意多句语句。

条件运算符

使用 ?: 运算符也是运算符形式的分支条件语句。

var x = 1;
console.log(x > 0 ? '大于' : '小于等于'); // 大于

这个例子的意思是,变量 x 的值大于 0 的话,就输出“大于”,否则就输出“小于等于”。条件运算符的语法是“条件?真时的值:假时的值”。上述的条件是 x > 0,真时的值是“大于”,假时的值是“小于等于”,会根据条件是否满足来决定使用 ?: 后面的值。
当然使用 if 语句也可以完成同样的功能:

var x = 1;
if (x > 0) {
    console.log('大于');
} else {
    console.log('小于等于');
}

使用 if 语句的语法就会像上面那样,但是使用条件运算符的语句就会更简洁。

循环语句

重复进行特定的处理就要使用循环语句。

while 语句

在满足特定条件期间进行循环的情况下使用 while 语句。

var x = 0;                          // 定义变量 x 并赋值为 0
var i = 1;                          // 定义变量 i 并赋值为 1
while (i <= 5) {                   // 当 i 小于等于 5 的时候重复以下处理
    x += i;                         // a 的值加上 i 的值
    i++;                            // i 的值增加 1
}                                   // 循环结束
console.log(x);                     // 显示计算结果

这个例子是求从 1 到 5 的和的程序。在这里,变量 i 用来产生从 1 到 5 的数字,当然也可以不用 i,但一般情况下循环处理用的变量会使用 ijk 为名称。
那么这个程序最重要的是 i++; 这行,如果没有这行 i 的值将永远为 1,而 i <= 5 的条件永远为真,就会成为无限循环。因此,这里每次都向 i 加 1 使得 i 的值从 1 依次变为 2、3、4、5、6,从而使 i <= 5 的条件变成假,循环结束。
变量 xi 的数值变化如下表所示:

循环 x i
初始状态 0 1
1 次后 1 2
2 次后 3 3
3 次后 6 4
4 次后 10 5
5 次后 15 6

循环 5 次后,在 a 的数值里实际上进行了 1 + 2 + 3 + 4 + 5 的运算,i 的值也超过了 5 而使得循环结束。

do…while 语句

无论条件是否成立,都先执行一遍循环体的情况下使用 do...while 语句。

var x = 0;                          // 定义变量 x 并赋值为 0
var i = 1;                          // 定义变量 i 并赋值为 1
do {                                // 重复以下处理
    x += i;                         // a 的值加上 i 的值
    i++;                            // i 的值增加 1
} while (i <= 5);                   // 当 i 小于等于 5 的时候重复以上处理
console.log(x);                     // 显示计算结果

这个例子和上面的例子是一样的,只是使用了 do...while 语句。

for 语句

想让变量特定范围内时进行循环的情况下使用 for 语句。

var x = 0;                          // 定义变量 x 并赋值为 1
for (let i = 1; i <= 5; i++) {      // 重复以下处理
    x += i;                         // a 的值加上 i 的值
}                                   // 循环结束
console.log(x);                     // 显示计算结果

这个例子和上面的例子是一样的,也是求从 1 到 5 的和的程序,这里 for 后面有使用 ; 分隔开的三部分,分别是初始条件、循环条件和循环后的处理。初始条件一般是定义循环变量的语句(如这里的 let i = 1),循环条件是判断循环是否继续的条件(如这里的 i <= 5),循环后的处理是每次循环结束后要进行的处理(如这里的 i++)。相比使用 while 语句和 do...while 语句的情况,if 语句更加简洁。
JavaScript 中有许多语法,循环中也存在各种各样不同的方法。在不同语法的使用中,请按照这个示例提示的「让编写更简便」的方式来思考。实际上,如果理解了条件分支语句和循环语句的话,就能用代码实现大部分的逻辑了。

break 语句

想中途中断循环就要使用 break 语句。

var i = 1;                          // 定义变量 i 并赋值为 1
while(true) {                       // 无条件重复以下处理
    i++;                            // i 的值增加 1
    if (i == 5) {                   // 如果 i 的值为 5 的话
        break;                      // 中断循环
    }                               // 条件分支结束
    console.log(i);                 // 显示 i 的值
}                                   // 循环结束

这个示例是“一边把 i 的值加上 1,一边显示 i 的值”的程序,并且加上了“i 等于 5 的就中断循环”的条件。运行该程序,显示的结果是 2、3、4。
break 语句也可以在 for 语句中使用,还记得上节的 switch 语句吗?break 语句必须被写在 case 语句成立后执行的一系列语句的最后。

continue 语句

想在不中断循环的情况下,在中途跳过本次循环而进入下一次循环时就要使用 continue 语句。

for (let i = 1; i <= 5; i++) {      // 重复以下处理
    if (i == 3) {                   // 如果 i 的值为 3 的话
        continue;                   // 跳过本次循环
    }                               // 条件分支结束
    console.log(i);                 // 显示 i 的值
}                                   // 循环结束

运行这段程序,显示的结果是 1、2、4、5。

循环嵌套

条件分支和循环等控制结构可以嵌套使用。比如要用 ij 两种变量进行双重循环,可以按照下面的方法:

var output = '';                        // 定义变量 output 并赋值为空字符串
for (let i = 1; i <= 5; i++) {          // 变量 i 在 1 到 5 之间变化
    for (let j = 1; j <= 5; j++) {      // 变量 j 在 1 到 5 之间变化
        output += `(${i}, ${j})`;       // 将 (i, j) 追加到 output 中
    }                                   // 内层循环结束
    output += `\n`;                     // 将换行符追加到 output 中
}                                       // 外层循环结束
console.log(output);                    // 显示 output

此外,虽然缩进不影响程序的运行,但是为了让程序结构更加清晰可读,建议在 if 语句、for 语句、while 语句、do...while 语句的语句块中使用四个空格或一个 tab 作为缩进。

函数

比如数字的计算和字符串的显示等,能够进行某种处理的被命名的功能被称为函数

函数的使用

函数这一词语来源于数学,但在程序中使用的函数,实际上更加接近“命令”。我们一直在使用的 console.log 就是一个函数。

console.log(Math.randomInt(100));

这个例子是每次运行都会显示一个 0 到 99 之间的任意一个随机数字的程序。Math.randomInt 函数是 MZ 中从 0 到指定整数这个不大的范围中随机选取一个数字并返回的函数。这个“指定的值”就称为参数。参数通常在函数名称后面的括号 () 内指定,就像我们一直在用的 console.log 函数一样,上面的示例里的 100 就是 Math.randInt 函数的参数。参数如果不直接指定数值的话,也可以像下面这样使用表达式(还记得什么是表达式吗?):

let x = 100;
let y = 200;
console.log(x + y);    //等同于 console.log(300)

与参数相反,函数将参数进行处理后返回的结果数值称为返回值。也就是说,Math.randomInt 函数的返回值是在特定范围内的随机数值。函数的返回值可以像普通数字和变量那样被使用。

console.log(Math.randomInt(6) + 1);

上面的例子的意思是 Math.randomInt 函数的返回返回值加上 1,并显示其计算结果。Math.randomInt 函数在给予的参数为 6 的情况下返回值为 0 到 5 范围内的任意一个数,所以再加上 1,就像丢骰子一样随机显示一个 1 到 6 范围内的数值。

函数的定义

一个函数定义(也叫函数声明函数语句)由三部分组成:

  • 函数的名称,命名规则与变量相同。(你可能会好奇,为什么 console.log 中间会有不允许出现在函数名称中的 .,其实它的函数名是 log,至于前面的 console. 有什么含义,这个我们会在下节“对象”的内容中介绍)
  • 函数的参数列表,用括号括起来,参数之间用逗号分隔
  • 函数体,即定义函数的 JavaScript 语句块,用大括号 {} 括起来

具体语法如下所示:

function 函数名(参数1, 参数2, ...) {
    函数体
}

例如,前面的 Math.randomInt(6) + 1 处理就可以定义为名称为 dice 的函数,按照下面这样定义之后,只要写 dice 就会得到一个 1 到 6 之间的随机数:

function dice() {
    return Math.randomInt(6) + 1;
}
console.log(dice()); // 等同于 console.log(Math.randomInt(6) + 1);

这里的 return 就是结束函数处理的命令。函数可以类比为 MZ 中的公共事件,而 return 就相当于“中断事件处理”的意思。return 右侧的表达式的值就是函数的返回值。函数不在中途中断的话还可以省略 return

函数的参数

可以注意到,之前的 dice 函数并没有参数,含有参数的函数可以像下面这样定义:

function abs(x) {
    if (x < 0) {        // 如果 x 小于 0 的话
        return -x;      // 结束函数,返回值为 -x
    } else {            // 除此之外
        return x;       // 结束函数,返回值为 x
    }                   // 条件分支结束
}
console.log(abs(-10));  // 10

这个是返回指定参数数值绝对值的函数。这里“指定参数数值”暂时定义为 x。这个 x 称为形式参数。在函数内部使用 if 语句来处理分支条件,x 小于 0 时(为负数)返回值 -x,除此之外(为 0 以上的数)返回值 x。最后一行是指定参数为 -10 的实际情况下使用函数,其结果用 console.log 函数输出。与 x 为形式参数相比,这儿的 -10 称为实际参数。
顺便讲一下,所谓绝对值是数学上提出的概念,是表示到原点(上述情况可以认为是 0)距离的数值。-3 也好 +3 也好,原点为 0 的情况下,每个到 0 的距离都为 3
含有多个参数的函数也能定义。多个参数要用逗号 , 分开来指定:

function sum(x, y) {
    return x + y;           // 结束函数,返回值为 x + y
}
console.log(sum(10, 20));   // 30

这个函数是返回两个参数的和。当然实际中直接写成 10 + 20 会更快更容易,这里只是说明语法的用法。
定义函数时就已经指定且使用时可以省略的参数称为默认参数。默认参数是在形式参数后面加上赋值符号 = 来指定的。先设置好了默认参数,在使用函数时未指定括号 () 内实际参数数值的情况下,形式参数就会自动代入默认参数数值。

function dice(n = 6) {
    return Math.randomInt(n) + 1;
}
console.log(dice());     // 等同于 console.log(dice(6));

上面的例子中没有指定函数的参数,使用函数时会返回 1 到 6 之间的随机数,然而指定参数的话,就能更改随机数字的最大值。比如,dice(10) 就会返回一个 1 到 10 之间的随机数。
很多内置函数都有这样省略参数的功能。比如,console.log 函数就有默认参数,如果不指定参数的话,就会输出一个空行。

函数的重定义

下面的例子中,hello 函数被重定义了两次。

function hello() {
    return "Hello, JavaScript!";
}
function hello() {
    return "Hello, Project 1!";
}
hello();    // Hello, Project 1!

在 JavaScript 中,函数名称重复的话不会出现错误。这种情况下后面的定义会覆盖前面的定义,系统会认定后面定义的函数有效,而先前的定义就无效了。
我们在写插件的时候,也会利用这个特性来按照自己的需要修改 MZ 中的函数,实现各种各样的效果。

匿名函数

在 JavaScript 中,函数也可以没有名字。这种函数称为匿名函数

function() {
    return "Hello, JavaScript!";
}

这个函数没有名字,因此也不能被调用。那么这么写有什么意义呢?
其实,这种函数的存在是为了方便在其他函数中使用。比如,我们可以把上面的函数赋值给一个变量,然后再调用这个变量。

var hello = function() {
    return "Hello, JavaScript!";
}
hello();    // Hello, JavaScript!

这样就可以调用这个函数了。或者,我们也可以在创建后直接调用这个函数。

(function() {
    return "Hello, JavaScript!";
})();       // Hello, JavaScript!

这种写法也称为立即调用函数

对象

JavaScript 是一种面向对象的语言。这里,所谓对象可以解释为除基本类型外的任何东西。

基本类型

JavaScript 中的基本类型包括数字、字符串等:

3           // 数字
"Project 1" // 字符串
true        // 布尔
null        // 空
undefined   // 未定义

除了上面这些,还有比较新的 JavaScript 版本中出现的 symbolbigint 类型(甚至比同样使用 JavaScript 作为脚本语言的 MV 还要新),这些在 MZ 代码编写中几乎不会用到,因此我们不做介绍。
除了以上这些基本类型之外,JavaScript 中的一切皆对象。

对象的属性与方法

对象是 JavaScript 中的核心概念。对象是一个包含相关数据和方法的集合。这些数据和方法通常由一些变量和函数组成,我们称之为属性方法
还记得我们一直在用的 console.log 函数吗?我们曾介绍过其实只有 log 部分是函数名,而他前面的 console 便是一个 JavaScript 的内置对象,它提供了 MZ 开发者工具中控制台调试的接口,而 log 则是这个对象的一个方法(也就是一个函数,也可以说是用于在控制台中输出调试信息的接口)。
console.log 中,我们可以看出,对象的属性和方法是通过在对象名和方法名之间加 . 来访问的。我们也可以自己创建一个对象,然后为这个对象添加属性和方法:

var person = {};   // 创建一个空对象
person.name = "Project 1"; // 为对象添加属性
person.sayHello = function() { // 为对象添加方法
    console.log("Hello, " + this.name + "!");
}
person.sayHello();  // 调用对象的方法

上面的例子用到的新东西有点多,让我们来挨个解释…
首先,我们创建了一个空对象,这个对象的名字叫做 person,其中 {} 可以表示一个空对象。
然后,我们为这个对象添加了一个属性,这个属性的名字叫做 name,值为 Project 1,是个字符串。
接着,我们为这个对象添加了一个方法,这个方法的名字叫做 sayHello,是通过匿名函数定义的。
最后,我们调用了这个对象的 sayHello 方法。
此时我们注意到,定义方法使用的匿名函数中使用了 this 关键字。this 关键字在 JavaScript 中有着特殊的含义,它指向的是当前对象,我们可以认为是对象名字的别称。在这个例子中,this 指向的就是 person 对象。
你也可以试试在直接创建的函数中使用 this 关键字,看看它指向的是什么:

function a() {
    console.log(this);
}
a();

不出意外的话,你会在控制台中看到 Window,这便是 MZ 中的全局对象。点击 Window 左边的小三角,可以看到它的属性和方法(这便是控制台的功能强大之处,你也可以输出刚刚定义的 person 对象,看看是不是有我们定义的属性和方法),比如 $dataActor 便记录了游戏的角色数据,这便是我们教程的下一部分「解读篇」的内容了。再向下翻,你会看到我们刚刚定义和调用的 a 函数。

对象的创建

扯远了…我们回到正题。我们看到,可以通过 {} 来创建一个空对象,然后为这个对象添加属性和方法。对象的属性和方法也可以在创建对象的时候就定义好,这样的对象称为字面量对象

var person = {
    name: "Project 1",
    sayHello: function() {
        console.log("Hello, " + this.name + "!");
    }
}
person.sayHello();

这样的对象创建方式和上面的方式是等价的。
除此之外,还有一种创建对象的方式,那就是使用 new 关键字。

var person = new Object();
person.name = "Project 1";
person.sayHello = function() {
    console.log("Hello, " + this.name + "!");
}
person.sayHello();

这种方式和上面的方式也是等价的。其中,Object 是 JavaScript 中的一个内置对象,它提供了创建对象的方法。这里的 person 便叫做 Object 的一个实例
此外,ECMAScript5 中还加入了使用 Object.create 方法创建对象的方法,这种方法的优点是可以指定对象的原型,因此我们放到后面再介绍。

点表示法和中括号表示法

上面的例子中,我们使用了点表示法来访问对象的属性和方法。除了点表示法之外,我们还可以使用中括号表示法来访问对象的属性和方法:

var person = {
    name: "Project 1",
    sayHello: function() {
        console.log("Hello, " + this.name + "!");
    }
}
person["name"];         // Project 1
person["sayHello"]();   // Hello, Project 1!

后面的中括号中可以是一个字符串,也可以是一个变量,这样就可以动态地访问对象的属性和方法了。

var person = {
    name: "Project 1",
    sayHello: function() {
        console.log("Hello, " + this.name + "!");
    }
}
var a = "name";
var b = "sayHello";
person[a];          // Project 1
person[b]();        // Hello, Project 1!
对象原型

在 JavaScript 中,函数也是对象,所以可以有属性。每个函数都有一个特殊的属性叫做原型prototype),正如下面所展示的:

function a() {}
console.log(a.prototype);

我们可以在控制台中看到以下内容:

Object
    constructor: ƒ a()
    __proto__: Object

其中,constructor 属性是指构造函数,指向了这个函数本身,而 __proto__ 属性指向了 a.prototype 的原型。
现在,我们可以添加一些属性和到 a 的原型上:

a.prototype.name = "Project 1";
console.log(a.prototype);

控制台中的输出变成了:

Object
    name: "Project 1"
    constructor: ƒ a()
    __proto__: Object

然后,我们可以通过 new 关键字来创建一个 a 的实例:

var b = new a(); 
// 当然你也可以省略括号写作 var b = new a; 两者是等效的
b.url = 'rpg.blue';
console.log(b);

可以看到控制台中的输出:

a
    url: "rpg.blue"
    __proto__:
        name: "Project 1"
        constructor: ƒ a()
        __proto__: Object

就像上面看到的,b__proto__ (原型)属性就是 a.prototype。但是这又有什么用呢?我们可以试试访问 bname 属性:

console.log(b.name); // Project 1

可以发现,bname 属性竟然是 a.prototype 中的 name 属性。当我们访问 b 的属性时,MZ 会首先查找 b 是否有这个属性,如果没有,就会去 b 的原型中查找,如果还没有,就会去原型的原型中查找,直到找到为止。如果最终还是没有找到,就会返回 undefined。这样在原型上链式查找的过程,便产生了原型链的概念(这个例子中原型链便是:b -> a.prototype -> Function.prototype -> Object.prototypea.prototype 后面这些原型分别是函数的原型、对象的原型)。
这样,我们就可以在 a.prototype 中定义一些公共的属性和方法,然后通过 new 关键字来创建对象,这样就可以避免重复的代码了。

类和类的继承

如果你学习过其他语言,继承通常是通过类来实现的。但在 JavaScript 中,类和类的继承都是借助函数对象和对象原型来模拟的。
类在其它语言中可以理解为对象的种类,而对象可以称为这个类的一个实例。在 JavaScript 中,类是一种特殊的函数(或者说由函数模拟),它的原型中包含了一些公共的属性和方法。或许实例和类的概念还是有些抽象,按照 RM 的概念来讲,RM 中的“武器”可以被认为是一个,“短剑”则是“武器”类的一个实例,它应当具有“武器”的所有属性(价格、攻击时附加普通攻击等属性)和方法(被装备、被出售、对战时播放指定动画等方法)。
还记得上节中的 a 函数吗?我们可以把它看作一个类,而 b 就是这个类的一个实例。现在我们通过下面的例子再理解一下前面这句话:

function Person(name) { 
    this.name = name;
}
Person.prototype.hello = function() {
    console.log("Hello, " + this.name + "!");
}
var b = new Person('Project 1');
b.hello(); // Hello, Project 1!
console.log(b instanceof Person); // true

上面的脚本可以看作创建了一个叫 Person 的类,它有一个构造函数 Person 和一个原型方法 hello。构造函数接受一个参数 name并将其赋值给新创建的对象的 name 属性。原型方法 hello 可以被所有 Person 的实例共享。
然后,我们通过 new 关键字来创建一个 Person 的实例 b,并调用 bhello 方法。
最后,我们通过 instanceof 关键字来判断 b 是否是 Person 的实例。
本节的名字叫做“类和类的继承”,那么继承呢?首先以 RM 的概念来理解,“装备”这一父类可以有“武器”、“防具”这两个子类,两个子类相同的属性和方法可以在“装备”中定义,这样当实现“武器”和“防具”时,“价格”、“被出售”等这些相同的属性和方法就不需要再重复定义一次了。
在 JavaScript 中,类的继承是通过原型链来实现的。这时候就该请出我们前面提到的 Object.create 了。Object.create 方法可以创建一个新对象,并同时指定这个对象的原型。当我们指定原型为父类的原型时,就可以实现继承了。例如我们将上面的 Person 作为父类,创建一个 Student 类:

function Student() {}
Student.prototype = Object.create(Person.prototype);
var s = new Student();
s.name = 'rpg.blue'
s.hello(); // Hello, rpg.blue!

这样,Student 的原型就是 Person 的原型,也就是说 Student 类继承了 Person 类的所有属性和方法。其实例 s 依然可以继承到 Person 类的构造函数 Person。MZ 核心脚本的编写时期恰好是 ECMAScript5 时期,所以 MZ 的核心脚本也是通过这种方式来实现类的继承的。
或许你会问,为什么不直接将 Student.prototype = Person.prototype 呢?这样做的话,Student.prototypePerson.prototype 就指向了同一个对象,这样的话,Student.prototype 的任何改动都会影响到 Person.prototype,这显然是不合理的。另一个可能的疑问是为什么不写成 Object.create(Person),读者可以自己试一下会出现什么问题,并思考为什么(读者自证不难bushi)

constructor 属性

每个实例对象都从它的原型上继承了一个 constructor 属性。这个属性指向了用于创建当前实例对象的构造函数。例如:

function a() {}
var b = new a();
console.log(b.constructor === a); // true
console.log(b instanceof a); // true

这里 bconstructor 属性指向了 a,而 instanceof 关键字就是通过判断 bconstructor 属性来判断 b 是否是 a 的实例的。
让我们再来看看上面的 Student 类的实例 s

console.log(s.constructor === Student); // false
console.log(s.constructor === Person); // true

这里 sconstructor 属性指向了 Person,而不是 Student。这是因为 Student 的原型通过 Object.create(Person.prototype) 指向了 Person 的原型,导致 Student 的原型中的 constructor 属性错误地变为了 Person。因此,我们需要在调用 Object.create 后手动将 Studentconstructor 属性指向 Student

function Person() {}
function Student() {}
console.log(Student.prototype.constructor === Student); // true 
Student.prototype = Object.create(Person.prototype);
console.log(Student.prototype.constructor === Student); // false
Student.prototype.constructor = Student;
console.log(Student.prototype.constructor === Student); // true

可以看到经过 Student.prototype.constructor = Student 后,Studentconstructor 属性重新指向了 Student。我们可以打开任意一个 MZ 的核心脚本文件,例如 rmmz_windows.js,可以看到 Window_Base 是这样定义的:

function Window_Base() {
    this.initialize(...arguments);
}
Window_Base.prototype = Object.create(Window.prototype);
Window_Base.prototype.constructor = Window_Base;

呼…终于讲完了,这一节的内容是 JavaScript 的核心,内容比较多也比较难理解,读者可以多看几遍。除此之外,ECMAScript6 中也引入了 class 关键字,可以用来定义类,但是这个关键字只是语法糖,本质上还是通过原型链来实现的,且 MZ 的核心脚本中并没有使用 class 关键字,所以这里就不再赘述了。

显示图片

学完对象之后让我们稍微轻松一下,来调用 MZ 核心脚本在标题显示图片吧。

精灵和位图

在之前建立的插件文件中输入以下代码:

let xjzsq_Scene_Title_create = Scene_Title.prototype.create
Scene_Title.prototype.create = function () {
    xjzsq_Scene_Title_create.call(this);
    this._sprite = new Sprite(new Bitmap.load("img/enemies/SF_Skullmask.png"));
    this.addChild(this._sprite);
}

如果画面左上角显示出骷髅兵的图片就表示成功了。
首先,因为要向标题这个场景(关于场景的内容会在解读篇中讲到)中添加图片,所以我们需要对 Scene_Title 这个场景类中添加内容。而原来的内容还是要保留,所以我们在第一行代码中对 Scene_title.prototype.create 这个方法做了备份,让 xjzsq_Scene_Title_create 这个变量指向了原来方法中的内容,而后我们重新定义了这个方法。这里的 xjzsq_Scene_Title_create 的名字是任意的,只要不跟现有的变量重名即可。
在重新定义的方法第一行中,我们使用 xjzsq_Scene_Title_create.call(this); 来先执行了原先这个方法中的内容。请注意这种方式在以后的插件编写中会经常用到,类似于我们对一件画作不满意的地方修修补补,而不影响到画作上原来的内容。
接下来就是插入图片的代码了,这里出现了 SpriteBitmap 这两个类。Sprite 是在游戏画面上显示某些图片的容器即精灵的类,Bitmap 是精灵中显示的位图(或者说是图片)本身内容的类。
新创建的函数第二行,是新生成一个 Sprite 类的实例,并把实例代入类成员 this._sprite 中。这里 new Sprite(...) 是上一节所介绍过的生成实例对象的第二种方法。
而新建对象使用的参数 Bitmap.load("img/enemies/SF_Skullmask.png") 则是读取 RMMZ 自带的图片素材("img/enemies/SF_Skullmask.png")并生成 Bitmap 类的一个新实例,然后将其作为生成 Sprite 实例的参数,也就是设置这个位图为精灵要显示的图片的意思。
这里要注意,指定 Bitmap 的路径名,分隔符是使用的 / 而不是 \ 这点特别注意。我们在字符串“转义字符”这个小节中学习了 \ 在字符串中具有特殊的意义,而在这里使用 / 就避免了这种可能的错误。
第三行,是将这个精灵实例加入到 Scene_Title 类实例的子内容中,加入到子内容后,Scene_Title 类的其他方法将自动把子内容显示在画面上,我们也就能在标题画面看到图片了。

Graphics 静态类

Graphics 静态类是 MZ 中用于处理游戏中图像的类,其中包含了画面的宽度和高度等信息。
所谓静态类,可以认为是收集了相同范围方法的集合,和类的区别在于,静态类中的方法不需要生成实例对象就可以直接调用。
图片显示在左上角,这显然不是我们想要的效果,我们需要把它放到画面中间。请继续加入以下代码:

let xjzsq_Scene_Title_update = Scene_Title.prototype.update
Scene_Title.prototype.update = function () {
    this._sprite.x = Graphics.width / 2 - this._sprite.width / 2;
    this._sprite.y = Graphics.height / 2 - this._sprite.height / 2;
    xjzsq_Scene_Title_update.call(this);
}

这里我们又对 Scene_Title.prototype.update 这个方法做了备份,然后重新定义了这个方法。