查看: 48927|回复: 27

[版务] 【新手向】RMVA - RGSS3脚本入门教程(施工中)

[复制链接]
梦石
0
星屑
6891
在线时间
7027 小时
注册时间
2013-11-2
回帖
1263

开拓者剧作品鉴家

发表于 2016-12-31 22:30:53 | 显示全部楼层 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有账号?注册会员

×
本帖最后由 RaidenInfinity 于 2017-5-5 12:44 编辑

RGSS3脚本入门教程




本教程献给所有想学习RMVA脚本的游戏制作者,祝你们可以靠所学到的知识成功编制梦想世界。


目录


展开折叠
    第一章 - Ruby基本编程手法
    1.1 - 变量赋值
    1.2 - 基本数据类型
    1.3 - 基本运算法
    1.4 - 方法定义与调用
    1.5 - 流程控制
    1.6 - 数组与哈希表
    1.7 - 系统方法

    第二章 - Ruby面向对象编程
    2.1 - 面向对象
    2.2 - 类与实例
    2.3 - 实例方法与变量
    2.4 - 封装、继承与多态
    2.5 - 猴子补丁
    2.6 - 类方法与模块
    2.7 - 垃圾回收机制
    2.8 - 对象之根与高阶操作
    2.9 - 方法对象与代码块
  
    第三章 - 游戏主循环与呈现
    3.1 - 游戏工程与启动
    3.2 - 游戏主循环
    3.3 - 声乐呈现
    3.4 - 输入处理与图像缓存
    3.5 - 数据库与档案存取



前言·预习


前言

RGSS3,全名Ruby Game Scripting System 3(Ruby游戏脚本系统3)。它基于Ruby 1.9.2 编程语言,是RPGMaker VX Ace所采用的游戏系统。
至于为什么叫3嘛…RGSS的第一代是XP的,第二代是VX的,那么VA的就是第三代了。(MV用的语言是JavaScript,那就是另一回事了)

既然是第三代,那么它的功能当然比前二代强得多,默认脚本的结构也成熟得多。
想当年我参加XP区R考场的时候,我的天啊写XP脚本的我好佩服你们可以忍受那么紊乱的结构和那超级多余让强迫症无所适从的写法啊!(碎碎念中)

那么说到脚本(Script)。啥?不就是一堆代码吗?为啥叫脚本呢?
百度百科中对“脚本”这个词的解释是:脚本是使用一种特定的描述性语言,依据一定的格式编写的可执行文件,又称作宏或批处理文件。
没错,脚本就像戏剧的剧本(小知识:剧本的英文也叫Script)那样,是给“解释器”(可以理解为演员)看的,按照里面所描述的行为来运行而达成演出效果。
这个解释器包含在RGSS300.dll里面,而它的“舞台”是Game.exe。脚本数据则存在Data文件夹里的Scripts.rvdata2,可由脚本编辑器修改。

由此可见,脚本构成了RMVA游戏的血肉,是非常重要的事物。通过深入地研究脚本,你甚至可以抛弃数据库,直接在脚本里面定义数据。(说实话,这个我干过也很推荐这么做)
就连事件指令,也是由“事件解释系统”进一步转换成脚本调用而实现的。通过事件指令第三页的脚本指令(按了会看到一个白框),很多原本做不到的事情都可以像施展魔法那样弄出来。
当然,事件脚本指令这个部分,我们在接下来有机会会说到。

脚本技术是非常重要的。本教程的目标不止是要让读者能改默认脚本,而且是要能自制系统,也能自行检查和排除插件脚本冲突的问题。
我认为,每个游戏都是一个个案,应该是独一无二的。滥用插件脚本(尤其是成配套的战斗系统)会造成玩家因为审美疲劳而产生排斥感,这是很明显的。
对脚本技术的知识缺乏甚至会扼杀很多天马行空的想法,导致制作者屈服于现有的插件脚本,与自己理想中的效果有巨大的落差。
因此,我希望可以透过这个教程,以图文并茂的方式,为读者们讲解RGSS3的基础和实践运用。



预习

在开始教学之前,我建议先阅读一些资源,预习一下。这可以垫下一下关于编程的基础,让学习变得更有效率。
当然,在阅读本教程时遇到的一些问题或不理解的深奥词汇(希望是没有啦…),可以参考这些资源,或直接去百度/谷歌搜索。
毕竟,读这个教程就是为了不求助于他人,对吧?

首先是taroxd所发布的,由6R一群大触所翻译的,本区一直以来都有置顶但大概是被无视的,F1 帮助文档汉化版。

链接:https://rpg.blue/forum.php?mod=viewthread&tid=369922
(注:对这个资源有什么疑问请在该帖回复,毕竟解铃还须系铃人)

这么说好了。你买了个洗衣机,你总得看说明书才知道怎么组装怎么用,对吧?那么F1帮助文档,就是RMVA的说明书。
无奈的是,汉化版的RMVA并未附上汉化版的帮助文档,导致很多制作者只能自己摸索,遇到诸多不便。
因此,这个资源可说是必读的。本区的版规(A-10-b项)就有规定,帮助文档内可以查询到的提问将视为违规,这是有明显原因的。
所以不管你是新手还是老手,在继续阅读本教程之前,还请从头到尾过目这个帮助文档。可能有你所遗漏的知识也说不定。
本教程默认你已经看过这个帮助文档了,所以如果你没看过的话,也许会遇上一些问题(所以去看啊啊啊啊!)。

接下来是 @QQ蚊子湯 同学所建议的 Ruby菜鸟教程(中文)。

链接:http://www.runoob.com/ruby/ruby-tutorial.html

既然是中文版的教程,那就没有理由不看了啊。见多识广啊,你说是吧?
这个网站里面简略地说明了Ruby的默认系统提供了什么样的功能,和基本语法。
当然在本教程也会粗略地再提及一次,所以就算没看…喂!去看啊!不会损你一管血的吧!

如果你看得懂英文,我推荐阅读Tutorials Point的Ruby教程(英文)。

链接:https://www.tutorialspoint.com/ruby/
(注:我不知道需不需要“魔法”,需要的话就用咯)

比起中文版的,我个人是觉得英文版的比较齐全和详细,使用的例子也比较好。
看得懂英文可以阅读,就算不会…嘛也可以尝试一下啦。百度机翻不知道可不可靠呢。

最后是Ruby 1.9.2的官方帮助文档 RubyDoc(英文)。

链接:https://ruby-doc.org/core-1.9.2/
(注:我不知道需不需要“魔法”,需要的话就用咯)

RMVA有官方帮助文档,Ruby当然也有官方帮助文档。
这个网站里面列出了Ruby所提供的核心方法和它的调用参数,也有很多范例。
我个人呢,如果我忘记了一个核心方法怎么用,我常翻阅的是这个资源。



线上解释器

当今的科技非常发达,程序员们想要测试一些灵机一闪时想到的算法的时候,直接开浏览器找个线上解释器就行了。
在阅读本教程前段(关于Ruby基础)的时候,没有关系到RM本身的一些代码可以直接开个解释器测试运行。这可以省去很多功夫,也比较容易能调试。
这里会介绍几个Ruby语言的线上解释器。要注意的是,这些解释器的Ruby版本可能比较高(有些方法RMVA自带的没有),不过应该没有太大的问题。

1. 书村网提供的Ruby在线解释器(中文)
链接:http://www.shucunwang.com/RunCode/ruby/

2. Repl.it提供的Ruby在线解释器(英文,可能需要“魔法”)
链接:https://repl.it/languages/ruby

3. Tutorials Point提供的Ruby在线解释器(英文,可能需要“魔法”)
链接:http://www.tutorialspoint.com/execute_ruby_online.php




Hello World!


展开折叠
Hello World!,中文翻译是“你好,世界!”。这一般是尝试编程的新手所踏出的第一步,也就是让程序显示一个讯息。

首先,我们打开RMVA,创建一个新工程。啥,你不会创建新工程?F1帮助文档再看一遍啊!
第一件事,就是点主菜单的“测试”部分,然后把“显示主控制台”打钩。为什么呢?待会儿我们会知道的。

01.png

接着,就是认清楚“脚本编辑器”在哪儿。打开主菜单的“工具”部分,看到了没?脚本编辑,快捷键是F11。其实图标栏也有个图标可以按进去。

02.png

脚本编辑器长这个样子的。左边是个列表,右边是个文本编辑框。大部分默认脚本的定义都在这儿,然后拉下去你会看到放入插件脚本的地方。
至于插件脚本怎么安装,可以参考我写的教程:https://rpg.blue/forum.php?mod=viewthread&tid=396828

03.png

现在拉下去,看到“(请在此添加)”了没?鼠标指下去按右键,然后选择“插入”。

04.png

好,现在你增添了一个新的空白脚本,在左下角给它一个名字吧。然后在右边的文本编辑框里面输入:

[pre lang="ruby"]
p "Hello World! 你好,世界!" #其实加什么字都行的啦
[/pre]

05.png

哎哟,代码居然有颜色的啊。没错。为了便于阅读,RMVA的脚本编辑窗是有为代码上色的。
目前我们看到的是,p 是黑色,"Hello World! 你好,世界!"是紫色,然后#和后面的字都是绿色。
这…到底是为什么呢?

p 是一个系统自带的方法。它的用途是在控制台显示内容(在RMXP和RMVX则是很蛋疼的弹窗),便于检查和除错。
实际上还有一个相似的方法叫做print。print和p的分别就是p在显示内容后会自动换行,而print不会。
紫色的是字符串,包含在单引号(' ')或双引号(" ")里面的是字符串,在这儿是作为即将被显示的文字。
绿色的是注释。在#符号后面的文字都不会作为有效代码处理,算是写给人看的笔记。
(这段如果看不懂的话,我还是建议…再把F1帮助文档看一遍好了)

启动测试游戏后,我们会看到控制台内显示出来的文字。这代表踏出的第一步已经成功了!来鼓掌吧!

06.PNG

你应该有发现到,我们现在所做的,和游戏一点关系都没有嘛!我想学的是怎么改游戏内的事物啊?
这些都是基础,是很重要的事物。当然,你可以自由选择阅读的内容,跳过你认为已经会了的部分。




评分

参与人数 15星屑 +442 梦石 +6 赞 +7 收起 理由
哇哇哇啊叭叭 + 1 精品文章
夢中人 + 1 感谢大佬给俺带来新世界
whitedewxiao + 1 精品文章
KB.Driver + 1 补课
玲珑菲菲 + 1 精品文章
chd114 + 1 韦天魔术棒
匿名RM爱好者 + 1 赞一个!
鑫晴 + 66 精品文章
百里_飞柳 + 66 精品文章
zaiy2863 + 220 实测 都不需要翻墙

查看全部评分

梦石
0
星屑
6891
在线时间
7027 小时
注册时间
2013-11-2
回帖
1263

开拓者剧作品鉴家

 楼主| 发表于 2016-12-31 22:53:56 | 显示全部楼层
本帖最后由 RaidenInfinity 于 2018-4-21 18:15 编辑

注意:相关知识已充足的读者们可以直接跳过第一和/或第二章。这楼的内容主要是给没接触过编程(也懒得看/看不懂预习部分提供的教程资源)的读者拓荒用。

第一章 - Ruby基本编程手法


1.1 变量赋值

展开折叠
相信大家在鼓捣RMVA的事件的时候,有见过“变量操作”这个指令。变量(variable),可以视为装数据的一个容器。
Ruby编程语言的其中一个优势,就是变量没有固定的类型。也就是说,我们在写Ruby脚本的时候,不需要特别指定这个容器要装的是数字,文字,还是其他类型的数据。
这是否常方便的,但是也代表着出现错误时,较难找出问题所在。不过,基础打稳之后,我们能尽量避免出错也能自行排除错误。

创建一个变量的时候,我们要给它取一个名字,并使用 = (赋值号)给它赋值。
[pre lang="ruby"]
a = 5
b = 6.5
c = "一二三"
p a
p b
p c
[/pre]

在运行上列代码后,在控制台内会得到下列结果:
[pre lang="ruby"]
5
6.5
"一二三"
[/pre]

变量有很多种(比如前缀@或$的),但是因为牵扯到比较深入的内容,所以第二章才会提到这个部分。
在为变量取名的时候,必须由字母或下划线(_)开头,而且里面不可以含有字母,数字和下划线以外,包括空格的符号。
[pre lang="ruby"]
a3 # -> 可以
ab123cd # -> 可以
abc_123_Def # -> 可以
1abc # -> 出错
ab%^23 # -> 出错
[/pre]

大写字母开头的变量在首次赋值之后无法再次改动(不允许用 = 来赋值),所以叫做“常量”(constant)。更进一步的说明会在第二章内提到。
   




1.2 基本数据类型

展开折叠
Ruby的变量主要可以用于存放几种基本数据:
1. 整数 (Fixnum)
2. 浮点数/小数 (Float)
3. 字符串 (String)
4. 符号 (Symbol)
5. 范围 (Range)
6. 布尔逻辑值 (true/false) (注:不知道布尔逻辑的话,可以去百度搜索一下)
7. 空值 (nil)

在Ruby语言,创建整数的方式有四种:十进制,二进制,十六进制,八进制。不知道什么是进制?百度搜索去呗。
一般我们写的数字都是十进制。前缀0b,0x和0分别是二进制,十六进制和八进制。某些特殊情况之下,用十六进制的方式来写数字会较容易阅读。
[pre lang="ruby"]
a = 12345 #十进制
a = 0b11000000111001 #二进制
a = 0x3039 #十六进制
a = 030071 #八进制
#以上四句的效果都是一样的
[/pre]

浮点数(小数)和整数的分别就是有无小数点。例如 a = 1 和 a = 1.0 是不一样的。这个会在基本运算的部分说到。
[pre lang="ruby"]
a = 1.0
a = 45.678
#只要有小数点,就算后面是0也都算是创建浮点数类型
[/pre]

字符串(文字)的创建方式有两种:双引号或者单引号,如"ABC"和'DEF'。两者有何不同呢?
如果使用双引号,Ruby会对字符串中的内嵌表达式(#{变量})和转义字符(如换行符\n)进行替换操作,而使用单引号的话则不会这么做。
至于用哪种比较好,是得看情况的。我个人因为其他语言的影响所以偏向双引号,但推荐的做法是除了特殊状况外尽量用单引号。
[pre lang="ruby"]
a = 66
b = "#{a}RPG"
c = '#{a}RPG'
p b #输出 -> "66RPG"
p c # 输出 -> "\#{a}RPG"
[/pre]

符号(Symbol),是Ruby的一个比较独特的数据类型。它的创建方式,就是冒号+文字(如:abc)。在RMVA脚本编辑窗里面,它会显示为橘黄色。
符号这种东西,我将它描述为“可以作为方法名字的字符串”。因为它创建的规则,和方法的名字相同(必须以字母或下划线开始等等)。
方法是啥?我们之后会提到的。方法名和变量名的区别是,问号(?)和感叹号(!)是可以使用的,但只能放在最后面。
[pre lang="ruby"]
a = :abc123 # -> 可以
a = :abc123? # -> 可以
a = :abc!123 # -> 出错,因为?或者!必须在最后面
b = :123abc # -> 出错,因为不可以用数字开头
[/pre]

范围(Range),就如其名。提供一个开始值和一个结束值(以..或者...相隔),就是它的创建方法。
[pre lang="ruby"]
a = (1..9) #1到9,包括9(1,2,3,4,5,6,7,8,9)
a = (1...9) #1到9之间,不包括9(1,2,3,4,5,6,7,8)
[/pre]

布尔逻辑值(可以理解为开关,然后布尔是人名)实际上由两种数据组成:真值(true)和假值(false)。
我知道很多“大神”又会来说什么真值假值是不同的啦什么的。但实际上,我们不需要知道这么多。
我们只需要知道true是真(理解为开启),false是假(理解为关闭)。它的用途在于条件分歧和循环的逻辑判断,我们接下来会提到。
[pre lang="ruby"]
a = true #真值 / 开启
b = false #假值 / 关闭
[/pre]

最后是空值(nil)。可以说数值不存在,也可以说没有数值。如果你时常遇到脚本出错,报错讯息里面有Nil这个字,那就是读取变量时因为某种缘故,遇到了空值。
[pre lang="ruby"]
a = nil #空值
[/pre]
   




1.3 基本运算法

展开折叠
在熟悉变量的赋值和数据类型后,我们来看看Ruby中的运算法,也就是加减乘除这一类的。
Ruby的运算符有很多,可以到本教程末尾的“附录”查看。在这里只会提到最基本的括号和加(+),减(-),乘(*),除(/),取余(%),乘方(**)。
我相信各位都有学过数学,知道运算的优先率(括号提升优先度,先乘方,后乘除取余,最后加减,左到右运算)。在这儿就不再提及了。

变量在赋值的时候,赋值的内容实际上是一条表达式(一组运算符,变量,或常量,最后会被运算而得到一个值),如:5, 1 + 2, a + b * 5
在第一次对一个变量赋值时是“定义”,接下来的就是“操作”了。
[pre lang="ruby"]
a = 1
b = 3 + 4
c = a + b
p a #输出:1
p b #输出:7
p c  #输出:8(因为a是1,b是7,所以a + b = 1 + 7 = 8)
[/pre]

当然,有时我们会遇到需要为单个变量更改数值的情况。这时,我们可以用“自运算符”来简化。比如+=,-=,*=,/=等等。
[pre lang="ruby"]
a = a + 2
a += 2 #这是上面那句的简化版
[/pre]

上面提到的都是整数的运算法。浮点数的运算法和整数一样,但是要注意的是,整数在和浮点数一起运算时,会被转变为浮点数。
这是什么意思呢?举个例子:3 + 5.5。3是整数,5.5是浮点数。得到的答案是8.5,是浮点数。
你会提问,这有什么重要的吗?浮点数就浮点数呗。但是当你遇到除法(/)操作的时候…问题就来了。
[pre lang="ruby"]
p 5 / 2  #输出:2
p 5 / 2.0 #输出:2.5
[/pre]
从上述的例子可以看到,5除于2,两个都是整数,所以得到的答案是整数。小数的部分被省略了。
5除于2.0的情况下,因为其中一个是浮点数,所以得到的答案是浮点数,保留了小数点。
那如果你其中一个是浮点数,但是你想要整数答案怎么办?当然是把浮点数转换为整数。转换的方法我们之后会提到。

在Ruby里面,字符串能用加法以添加文本。例子:"ABC" + "DEF" = "ABCDEF"。
[pre lang="ruby"]
a = "ABC"
b = "DEF"
b += a
p a #输出:"ABC"
p b #输出:"DEFABC"
[/pre]

如果想要把整数,浮点数之类的数据作为文本加入字符串里面,怎么办?都说“作为文本“了嘛,当然有转换的方法啊。(转换法之后会提到)
   




1.4 方法定义与调用

展开折叠
我们知道,在控制台上显示变量存着的数据,使用的是…
[pre lang="ruby"]
p 变量名字
[/pre]
(小知识:中文字也可以当变量的名字,为啥之前不提及是因为我不推荐)

在上面列出的这行代码内,我们调用(也可以说是呼叫/使用) p 这个系统方法,而要显示的变量要作为它的参数。
方法,可以理解为一个“功能”。方法可以由系统提供,或者自己定义。我们调用方法的时候,有时候需要附上参数,也可以获得方法返回的数据。
啥?参数?返回数据?不懂啊!啊…这么说好了。假设有一个炼金术师,你给他一红一蓝两瓶药水,他帮你把这两瓶药水合起来,然后给你一瓶紫色药水…
红蓝药水是递给炼金术师(方法)的材料(参数),紫色药水就是获得的成品(返回数据)。那么,我们就直接看范例代码吧。

[pre lang="ruby"]
#方法定义
def add(a, b)
  return a + b
end

#方法调用
c = add(8, 7)
p c #输出:15
[/pre]

从上面的范例代码我们可以看到,定义一个方法的格式是:
[pre lang="ruby"]
def 名字(参数)
    内容
end
[/pre]

def 是 define(定义)的缩写。它是一个关键词(keyword),在RMVA脚本编辑器内会显示为蓝色。
end 也是一个关键词,带结束的意思。一个def要配一个end,不然运行游戏时会报错(语法错误SyntaxError)。
那么 def 的后面,就是方法的名字了。方法的命名规则与变量相似,然后建议用有意义的名字,最好不要用乱七八糟的东西来取名(变量也是啊!),以免阅读困难。
问号(?)和叹号(!)可以用在方法名字的最后面(比如abc?和cde!)。这两个符号一般用来描述某种性质的方法(比较容易认得)。
一般问号(?)用于描述判断方法(返回true/false),而叹号(!)用于描述毁灭性方法。
什么是毁灭性方法?电脑爆炸?不。我们会在之后的“系统方法”部分认识到一些后缀叹号(!)的方法,了解它与没有后缀的版本的分别。
在名字后面,括号里面的就是参数了。在Ruby语言内,参数是选择性的。就连包着它的括号也是选择性的!例如:
[pre lang="ruby"]
#没有括号包着参数的方法定义
def add a, b
  return a + b
end

#没有括号包住参数的方法调用
c = add 8, 7
p c #输出:15
[/pre]
不过为了美观和可读性…还是加吧。由此可见,p c 和 p(c)是一样的。但是因为我个人的习惯,我选择不用括号包着p的参数。   
这个要看个人品味和习惯。当然,如果这是要写给别人看的…我感觉还是正式一点比较好,尽量写得易懂,一目了然。

参数代入方法内,就成了局部变量(local variable,可以理解为暂时的变量)。
所以范例里面,定义了参数a和b(注:多个参数请用逗号(,)隔开)后,在方法里面可以自由使用a和b来做一系列的运算。

return 这个关键词的意思是返回。不管放在哪儿,只要运行到这一行,就会强制结束方法的运行,并把return后面的表达式所计算出来的值递回去。
如果return的后面没有表达式(单独的一个字),那么系统会默认返回空值(nil)。一些系统提供的方法在操作失败时也会返回空值,是要注意的地方。
RMVA默认脚本的一些方法没有return。这是为什么呢?那是因为,在Ruby语言,方法会自动将end前面所执行的那一句表达式得出来的值作为返回数据。
意思就是说,我们这么写是可以的:
[pre lang="ruby"]
#没有return的方法定义
def add (a, b)
  a + b
end
[/pre]
不过这也要看个人风格。为啥RMVA默认脚本有些方法最后面有return,有些则没有,明明都是差不多的东西呢…这你得自己去问开发人员,他想的是什么。

另外这里也要提到的就是分号(;)在Ruby语言里面的作用。有用过C/C++/C#/Java的就会知道,分号否常重要,是语句的终止符。(语句的意思:交给程序运行的一行指令)
在Ruby里面,仅是换行就能结束语句了。分号在Ruby语言的用途是作为分隔符,用它可以将数行代码浓缩成一行。
例如我们可以基于上面的例子改一改:
[pre lang="ruby"]
#没有return的方法定义,用分号浓缩成一行
def add (a, b); a + b end
[/pre]
一些地方不需要分号(比如end前面)就能并入上一行了。这个就留给各位自己摸索吧。
一般是不建议用分号来减少行数的。不过如果真的有需要的话(RMVA默认脚本中,可以在Game_BattlerBase里见到这种情况),那就用吧。

说完方法的定义,现在就要来说方法的调用了。
方法的调用格式是:名字(参数) (括号可以省略掉,多个参数要用逗号(,)分隔)如果该方法不需要参数,直接写名字就行了。
递入的参数数量必须和方法所需要的参数数量相符,否则会报错(参数错误ArgumentError)。
递入参数的时候,是写一个表达式,然后传给方法的是表达式所计算出来的值。
如果你已经忘掉了什么是表达式…再复习一下吧。表达式是一组运算符,变量,或常量,最后会被运算而得到一个值。
表达式里面可是包括调用的方法所返回的值的哟!下面是各种调用方法的例子(方法还是之前那个):
[pre lang="ruby"]
#定义变量
x = 5
y = 6
z = 7

#定义方法
def add (a, b); a + b end

#方法调用
p add(1,2) #输出:3
p add(1 + 2,3) #输出:6
p add(x,y) #输出:11,因为x = 5,y = 6,所以x + y = 11
p add(add(x,y + 4) + 2,z) #输出:24,因为add(x,y + 4)得出15,然后add(15 + 2,z)得出24
[/pre]

不过,RMVA默认脚本里面的一些方法(尤其是Window_Base的)调用的时候可以递入不同数量的参数。有时3个,有时4个,如此的。
在Ruby里面,我们可以在方法内定义默认参数。来个例子:
[pre lang="ruby"]
#可以接受2到4个参数的加法运算方法
def add(a, b, c = 0, d = 0)
    a + b + c + d
end
[/pre]

从上面的例子我们可以看到,c和d这两个参数,默认数值为0。如果调用这个方法时没有附上第三或第四个参数,那么默认值就会生效。
[pre lang="ruby"]
#方法定义
def add(a, b, c = 0, d = 0); a + b + c + d end

#方法调用
p add(1,2) #输出:3
p add(1,2,3) #输出:6
p add(1,2,3,4) #输出:10
[/pre]
如果递入的参数少于2或大于4的话,就会报错。还有就是,默认参数只能放在参数列表的后面,不管有几个。

人呢,总会有天马行空的各种想法。如果我不知道我最多会放入多少个参数,怎么办?可能是5个,可能是10个,也可能无上限…
这是有办法的。但是因为牵扯到“数组”这玩意儿,所以这个我们之后有机会提到。

那么我们已经知道了如何自定义一个方法,也知道了如何去调用它。但是这只是冰山一角。
Ruby的系统提供了很多核心方法。每种基本数据有它自己的一套默认方法。详细理论本教程第二章才会深入,所以此时此刻,当做一个现象就好。
比如之前所提到的数据类型转换,是数据类型本身附带的方法。除此之外,整数有绝对值,浮点数有取整,字符串有文本替换…
调用这种方法的方式,是在数据(不管是变量,常量还是直接创建)的后面加上句号(.),然后写上方法的名字和代入参数。
[pre lang="ruby"]
a = 5
b = 2.0
c = "ABC"

p "123".to_i #输出:123(没有引号),因为使用to_i转换为整数
p 5.to_s #输出:"5"(有引号),因为使用to_s转换为字符串
p (-6).abs #输出:6 ,因为使用abs取绝对值
p (45.789).ceil #输出:46 ,因为使用ceil向上取整
p a / b.to_i #输出:2 ,不是2.5,因为使用to_i将浮点数转换成整数类型了
p c + a.to_s #输出:"ABC5" ,如果没有用to_s会因为运算时数据类型不相配而报错
[/pre]

某些系统方法可以直接调用,比如p是在控制台显示数据,rand是获取随机数。
但是一些方法则是被包在“模块”里面的(模块在第二章会解说),比如系统提供的三角函数方法,被包在命名为Math(意思是数学)的模块里面了。
[pre lang="ruby"]
#注:下列的三角函数方法参数单位是弧度而不是角度
p Math.sin(0.5) #输出:0.479425538604203
p Math.cos(1.55) #输出: 0.020794827803092428
p Math.tan(0.87) #输出:1.1853248603008053
[/pre]

Ruby所提供的核心方法为我们带来了很多便利。这些方法将在本章的末尾简略地介绍。
   




1.5 流程控制

展开折叠

我们使用RMVA的事件系统时,有时会用到“条件分歧”和“循环”这两个指令。
“条件分歧”就是判断一个情况是否成立,是的话做些什么,否的话做些什么。
“循环”则是重复地运行特定的一些指令,直到使用“跳出循环”
这些控制指令运行的方式,叫做“流程控制”。

不过上述的这些只是皮毛。如果只用事件指令,遇到一些比较复杂的流程时,会导致事件页略显紊乱而且也难以追溯。
Ruby脚本的流程控制语法可说是比较“人性化”的,也较接近人类的语言(主要是英文,不过也没差)。
本教程会将流程控制分成逻辑判断,条件分歧,抓错机制,循环,迭代这五个主要部分来讲解。

A - 逻辑判断

在制作游戏流程的时候,我们常常会需要用到比较运算。判断金钱是否超过5000啊,背包有没有带着通行证啊,密码锁输入的数字是否等于8010啊,之类的情况。
我们用六种主要的比较运算符:等于(==),不等于(!=),小于(<),大于(>),小于或等于(<=),大于或等于(>=)来判断左右两个值之间的关系是否成立。
(注意:值指的是表达式运算完毕后得到的值,也就是 a + b == c + d 这样的语句是可以用的)
这种运算最后会得出一个值,也就是真值(true)或者假值(false)。这两个值统称布尔逻辑值(boolean value),是一种基本数据类型。

假设我们有一个变量a,这个变量里面存着整数4。(a = 4)
[pre lang="ruby"]
#变量赋值
a = 4
#比较运算(注意:true是真值/是,false是假值/否)
p (a == 4) #输出: true #判断a存着的值是否等于4
p (a != 4) #输出:false #判断a存着的值是否不等于4
p (a < 4) #输出:false #判断a存着的值是否小于4
p (a > 4) #输出:false #判断a存着的值是否大于4
p (a <= 4) #输出:true #判断a存着的值是否等于或小于4
p (a >= 4) #输出:true #判断a存着的值是否等于或大于4
[/pre]
当然因为最后计算处理的是一个值,我们可以把它存在一个变量里面待用。

上述的例子是关于整数和浮点数的比较运算。不同类型的基本数据的默认比较运算法可能会不同。
不过,大部分基本数据(如字符串,符号)的等于(==)和不等于(!=)操作的用途就如其名,没有其他特定条件。

很多时候,我们不只是需要一个比较运算。我们可能需要判断角色A在队伍内的同时,金钱超过一定的数量。我们也可能要判定玩家是否拥有两种物品之中的其中一种。
在这种情况下,我们有逻辑运算符(也称逻辑联结符),就像加法或者乘法那样把数个布尔逻辑值通过运算整合成一个值。
我们有三种逻辑运算符:否(!),或(||),和(&&)。!等于反转,||等于加法,&&等于乘法。

否(!),简单来说就是反转逻辑值。true变成false,false变成true。很简单,对吧?就像不等于的符号是!=那样。
[pre lang="ruby"]
a = true
b = false
p !a #输出:false(因为 !true = false)
p !b #输出:true (因为 !false = true)
[/pre]

或(||)是逻辑运算的加法。如果把true(真值)比喻作1,而false(假值)比喻作0,那么:
[pre lang="ruby"]
#注:这不是可运行的代码
0 + 0 = 0 (所以false || false 将得到 false)
0 + 1 = 1 (所以false || true 将得到 true)
1 + 0 = 1 (所以true || false 将得到 true)
1 + 1 = 1 (所以true || true 将得到 true)
[/pre]
注意:1 + 1 = 1 是因为布尔逻辑值只有1(true)和0(false),所以只要不是0(false)的都是1(true)。
因此,只要其中一个布尔逻辑值为真值(true),那么得到的结果必定是真值(true)。毕竟是“或者”嘛。
在Ruby语言里面,||的运算进行的时候,只要左边的值为真,那么右边的表达式就不会再执行了。这个现象被称为“短路”。

和(&&)是逻辑运算的乘法。只有两边都是真值(true)时,结果才能是真值(true)。
[pre lang="ruby"]
#注:这不是可运行的代码
0 * 0 = 0 (所以false && false 将得到 false)
0 * 1 = 0 (所以false && true 将得到 false)
1 * 0 = 0 (所以true && false 将得到 false)
1 * 1 = 1 (所以true && true 将得到 true)
[/pre]
&&的运算进行的时候,只要左边的值为假,那么右边的表达式就不会再执行了,毕竟已经没必要了。

以上提到的三种逻辑运算符就和基本运算法那样,可以混合成表达式。
不过要注意的一件事就是,和(&&)算是乘法,所以执行的优先度会凌驾于算加法的或(||)。否(!)当然是排最前面了。
在写逻辑运算的表达式时,请务必善用括号,以免出现优先度的问题,而且要顾虑到可读性。
[pre lang="ruby"]
#变量赋值
a = 5
b = 6
c = 7
d = 8

#逻辑表达式
p (a + 2 < 6 || c == 7) #输出:true ,因为左边运算后得出false,右边运算后得出true(false || true = true)
p (c != 8 && !(a == 6)) #输出:true ,因为左边运算后得出true,右边运算后得出false但是被反转成true (true && true = true)
p (a > 7 || c < d && d == b - 2) #输出:false ,原因是表达式计算完毕后是false || true && false
#而因为&&凌驾于||所以: false || (true && false) = false || false = false
[/pre]
嘛,为了自己和他人的眼睛,能加括号的地方尽量加括号吧。实际上最外层那层括号是不需要加的,但是为了美观所以我加上去了。
(注意:运算符||和&&在对非布尔逻辑值使用的时候有别的效果,这个在第二章才会提及)

Ruby是很人性化的语言。实际上,它准备了三个关键词:not, or 和 and,与!,||和&&符号相对应。
不过,我否常不建议使用关键词来代替符号,因为它们的执行优先度会比符号版本的还低很多。

最后一个要介绍的符号,“联合比较运算符”(<=>),英文叫spaceship operator,直译是宇宙飞船运算符(可能是因为样子…很像?)。
你看到这符号的第一个反应会是:这啥啊?小于等于大于?你没猜错,它是三合一的,非常特别的运算符。请直接看例子吧。
[pre lang="ruby"]
#变量赋值
a = 6
b = 6
c = 5
d = 7

#联合比较运算
p a <=> b #输出:0 ,因为左边和右边一样
p a <=> c #输出:1 ,因为左边大于右边
p a <=> d #输出:-1    ,因为左边小于右边
[/pre]
如上面的例子所示,<=> 这个符号会根据左右两边的值之间的关系,得出1,0或者-1这三种结果。(0 等于,1 大于,-1 小于)
不过,如果两边的值无法被比较(比如你拿字符串来和整数比),就会得到nil(空值),表示运算失败。

B - 条件分歧

假设我们需要执行如此情况的流程:如果玩家的平均队伍等级是10以上,就允许通过,否则就不允许通过。
以上的例子要用脚本实现的话,我们会用到[b]if(如果),else(不然/否则)和end(结束)这三个关键词。
[pre lang="ruby"]
if 玩家的平均队伍等级是10以上
    允许通过
else
    不允许通过
end   
[/pre]
玩过RMVA事件的各位应该挺熟悉这个架构。不过,脚本多了数个条件分歧的功能,是事件所缺少的。

在Ruby语言里,if的判断通过条件是:不是false(假值)或者nil(空值)。
也就是说,不管是什么数据作为if的判断条件,只要不是false或者nil,就会通过。
因此,if除了拿来检测真假(true/false)之外,还用来检测是否空值(nil)。
这个特性会在第二章内,提到空值时再进一步说明用法。

现在我们来说说elsif(不然如果)这个关键词。elsif,很明显是else(不然)和if(如果)的混合体。
(小知识:在C++和Java等编程语言里面,else if这两个字是分开写的。)
假设我们有如此的代码:
[pre lang="ruby"]
if a == 5
    p "五"
else
    if a == 6
        p "六"
    else
        if a == 7
            p "七"
        else
            p "???"
        end
    end
end   
[/pre]
这是什么鬼?为啥那么乱?很好,这不就是用事件条件分歧指令时常见的事吗?

那么我们把上面的这段代码,善用用elsif来整理一下。
[pre lang="ruby"]
if a == 5
    p "五"
elsif a == 6
    p "六"
elsif a == 7
    p "七"
else
    p "???"
end   
[/pre]
嘿,是不是整洁多了呢?效果当然还是一样的啊。除此之外也要记得一件事情。
在一个if到end的代码段里面(不管里面有没有elsif或者/和else),只要有其中一个判断通过了,就会在执行结果之后跳到end处,结束流程。

Ruby语言提供了另一个关键词,叫做unless(除非)。它是if的相反,也就是说:
[pre lang="ruby"]
if !(a == 1)
    p "测试文字"
end
#和
unless a == 1
    p "测试文字"
end
#的效果是一样的。
[/pre]

unless不能配上elsif用,虽然可以配着else。可读性…则是一个巨大的问题。
因此,我个人不推荐使用这个关键词,因为if已经非常非常足够了。加个!来执行判定反转也不是什么难事。
当然你坚持一定要用的话,我是建议别配着else。

Ruby异于其他编程语言的一个特征,就是提供了条件分歧的倒装句。什么意思呢?
我们可以把上面的例子再简化成:
[pre lang="ruby"]
p "测试文字" if !(a == 1)
p "测试文字" unless a == 1
[/pre]
执行内容在左边,条件在右边。这叫做“倒装”。我个人认为这是一种救赎啊…会上瘾的!
这种形式的条件分歧的只能容纳一句执行内容,所以如果需要超过一句的,还是用正规的吧。
实际上倒装句与英文文法比较相似,而我们博大精深的中文的话,是与正规的条件分歧相似也比较易于理解。

代码嘛,当然是越简单越好。但是呢,Ruby毕竟是人性化的语言,所以它准备了很多特殊的关键词来表现这一点。
then(那就)是其中一个“比较多余”的关键词,因为有没有都没差。

如果我们不要用倒装句的话,要怎么把一个简短的条件分歧缩成一行啊?来看例子。
[pre lang="ruby"]
#用分号的版本
if !(a == 1); p "测试文字" end
unless a == 1; p "测试文字" end

#用then的版本
if !(a == 1) then p "测试文字" end
unless a == 1 then p "测试文字" end
[/pre]

好,then(那就)的用处非常明显了吧?如果a不等于1,那就显示“测试文字”这个字符串。真是直白啊。
要不要用then,要不要用倒装句,那全是个人的习惯和风格。这个,谁也管不着(拿着带刺铁棍的制作组员那是另一回事)。

我们介绍过许多种比较运算符。连奇怪的<=>(联合比较运算)都跑出来了。
现在我们要介绍一个也是挺奇葩(和很少用到),但是不得不知道的是“特殊等于测试符”(===,下称三等号)。
我去,=是赋值,==是等于,===是什么鬼玩意儿?在Ruby里面,在不同的情况下它有不同的用处。
为什么要提这个?这是因为Ruby有一个条件分歧操作是基于这个符号的。

三等号(===)一般来说和等于符(==)一样。不过,它的实际功能随着数据类型而改变。
在这章里我们会提到三等号判断范围是否包含整数的功能。(如果左边的类型是范围,判断左边是否包含右边)

假设我们有一个范围,1..15(1到15之间,包含15):
[pre lang="ruby"]
#使用等于符
p (1..15) == 7 #输出: false,因为范围和整数无法对等
p 13 == (1..15) #输出:false,因为整数和范围无法对等

#使用三等号
p (1..15) === 7 #输出:true,因为1到15之间包含7
p 13 === (1..15) #输出:false,因为范围必须在左边才行
[/pre]

那么基于这个功能,我们来写一段代码,判定下面的这个情况。
我们要判定a(里面存着的值)是否在(1..15)里,等于17,等于20,或是在(22...33)之间,和除此之外的时候,然后显示一段文字。
[pre lang="ruby"]
#变量赋值
a = 24
#条件分歧
if (1..15) === a
    p "a在1..15的范围里"
elsif 17 === a
    p "a等于17"
elsif 20 === a   
    p "a等于20"
elsif (22...33) === a
    p "a在22和33之间"
else
    p "a不符合以上的所有条件"
end   
#运行后输出:"a在22和33之间"
[/pre]

上面的这个例子,在Ruby语言里可以再简化,用case(事例)和when(等于时)关键词。
[pre lang="ruby"]
#变量赋值
a = 24
#条件分歧
case a
    when (1..15)
        p "a在1..15的范围里"
    when 17
        p "a等于17"
    when 20
        p "a等于20"
    when (22...33)
        p "a在22和33之间"
    else
        p "a不符合以上的所有条件"
end   
#运行后输出:"a在22和33之间"
[/pre]
注意:Ruby的case在条件判定成功后会自动退出(break),不像C/C++那样如果不加break就会继续运行来判定下一个条件。

当然,在遇到下一个when或者else或者end之前,是可以放复数的执行内容的。
如果执行内容只有一行的话,也可以用then或者分号来缩短成每个when为一行。
这是非常方便的。再给个常见的例子吧。
[pre lang="ruby"]
#使用if的写法
if 1 === a #注意:在这里和if a == 1是一样的
    p "a 等于 1"
elsif 2 === a
    p "a 等于 2"
elsif 3 === a
    p "a 等于 3"
end

#使用case的写法,以分号来缩短
case a   
    when 1; p "a 等于 1"
    when 2; p "a 等于 2"
    when 3; p "a 等于 3"
end   
[/pre]
非常整洁,对吧?这就是Ruby语言的魅力所在啊!

在阅读RMVA默认脚本的时候,我们可能会看到一种非常奇怪的运算符:问号(?),后面还会跟着一个冒号(:)。
这个运作符叫“条件表达符”,由问号(?)和冒号(:)组成,是一个非常精简的运作符,就像一个小型的if...else...end组合。
[pre lang="ruby"]
#变量赋值
a = true
c = 5
#使用if-else-end组合
if a
    b = c + 1
else
    b = 10
end
#使用条件表达符
b = (a ? c + 1 : 10)   
[/pre]
?左边的表达式是条件。如果条件判断通过,那么:的左边所计算出来的值将被取用,否则取用:的右边。
因此格式是:条件 ? 成功 : 失败。使用条件表达符时,建议使用扩号包起来,以增添可读性。

C - 抓错机制

我们运行脚本时,常常遇到很多错误。什么除于零啦,读取存档时找不到文件啦,之类的。
虽然完善的条件分歧判断机制可以避免出现错误,但是总会有漏网之鱼。
而且,每次报了一个错,整个程序就会关闭,又要重开又要重玩重测啊…真是麻烦又扰民啊对吧。
因此,Ruby语言准备了一套抓错机制,由begin(开始), rescue(拯救), ensure(确保)和喜闻乐见的end组成。
(注意:begin在循环的部分有别的用途,所以这里只说它与rescue或/和ensure配合时的功能。

假设我们有两个变量,a和b,而我们要将它们相除。
[pre lang="ruby"]
a = 5
b = 0
p (a / b) #报错: (整数)除于零
[/pre]

当然,我们的确已经预料到了,b = 0会导致a / b的操作报错。因此我们可以用if条件分歧来处理这种问题:
[pre lang="ruby"]
a = 5
b = 0
if b == 0
    p "运算错误!"
else
    p (a / b)
end   
#输出:"运算错误!"
[/pre]

然而这些需要预判。如果预料到这段代码发生的错误实在是太随机太蛋疼了,那就需要用到抓错机制了。
抓错机制的格式主要是:begin 执行代码 rescue 补救代码 end
改写上面的例子的话:
[pre lang="ruby"]
a = 5
b = 0
begin
    p (a / b)
rescue
    p "运算错误!"
end
#输出:"运算错误!"
[/pre]
rescue实际上还可以截取错误数据(就是原本报错弹窗的时候显示出来的),但是在这儿就不多做介绍了,可以自己科普学习。
在rescue的后面加上 => 就可以指定一个获取错误数据的变量。在rescue和=>之间可以指定截取错误数据的类型。

在这个情况之下,执行代码如果出现错误,不会弹窗报错然后游戏关闭,而是执行补救代码。
在RMVA默认脚本内,抓错机制实际运用的地方是DataManager模块里的,关于存档和读档的方法。
如果存档或读档失败,你会听到“哔哔”的提示音效。这就是抓错机制在奏效了。

当然,玩Ruby呢,肯定要知道的是…有好多好多的方法可以浓缩代码啊啊啊!什么都缩成一行啊!
于是如果执行代码和补救代码都只有一行,上面的例子又可以写成:
[pre lang="ruby"]
a = 5
b = 0
p (a / b) rescue p "运算错误!"
#输出:"运算错误!"
[/pre]
格式是: 执行代码 rescue 补救代码

说完rescue,现在来说说ensure。这个关键词在RMVA默认脚本中没有用到,但还是介绍一下好了。
ensure的意思是“确保”,也就是说,不管天灾人祸地震核爆也好,这段代码都要执行啊。
因为RMVA默认脚本里面没有用到的关系,所以我只用它一般使用的地方来说。

假设我们要打开一个文件,处理一些数据,然后关闭文件。但是处理数据的时候可能会出事报错!
报错之后,文件开了没关,会发生什么事情我是不知道啦(因为我没干过这种事),也许你可以去研究一下。
因此,我们需要一个就算报错(没有用rescue的情况)也会执行的代码段来运行关闭文件的语句。
在这里因为文件操作是挺复杂的(本教程会在偏后期的地方说到),所以目前只以文字来代替。
[pre lang="ruby"]
#没有rescue的情况
begin
    打开文件
    处理数据
ensure
    关闭文件 if 文件已打开
end   

#有rescue的情况
begin
    打开文件
    处理数据
rescue
    补救代码(比如显示错误讯息)
ensure
    关闭文件 if 文件已打开
end   
[/pre]
不过实际上,大部分情况之下,是不需要ensure的。在这里介绍它只是为了…完整性。

我们现在知道了,如果执行代码失败了,会触发rescue后边的补救代码。如果成功而不是失败呢?那就用else。
[pre lang="ruby"]
begin
    执行代码
rescue
    补救代码(失败后执行)
else
    成功后执行的代码
ensure
    必须执行的代码
end   
[/pre]

如果有重试的需求的话,Ruby语言准备了一个关键词:retry (重试),来重新运行执行代码。
一般它是在补救代码里面,在做调整之后重新运行执行代码。
[pre lang="ruby"]
begin
    执行代码
rescue
    补救/调整
    retry #重新运行一直到没出错为止
end   
[/pre]
当然你需要用条件分歧来操纵重试的次数,不然上面这个例子如果一直出错的话可是会无限循环卡死的啊…
retry在接下来的“迭代”部分里面也起着作用。

D - 循环

循环,就是在某个条件成立的前提之下,重复相同的代码。如果没有条件,那就是无限循环(如RMVA事件系统的循环指令)。
Ruby语言提供了两个主要的循环语句,也就是while(正在)和until(直到)。
while和until的分别和if(如果)与unless(除非)一样。后者是前者条件的相反(until a 和 while !a 一样)。
它们的条件判定法也和if/unless的一样,都是见到false(假值)或者nil(真值)则不通过检测,不然就通过。

假设我们要定义一个循环,运行时将变量a的值增加1(a += 1)并显示此时a的值,而循环条件是a小过10。(a初始时是7)
[pre lang="ruby"]
a = 7
while a < 10
    a += 1
    p a
end
#输出:
8
9
10   
[/pre]
我们可以看到的是,循环刚开始的时候,会先判断条件(a < 10)是否通过。当时a的值是7,所以通过了。
那么这个循环在完成一次的运行之后,a的值是8。这时,条件判断将再次进行。通过了吗?通过了。
所以就这样一直循环,直到a的值成了10。条件判断失败了,因为要求是a小于10,而a已经是10了。
条件判断不通过的话,循环将会中断,不会再继续了。

在上一个部分有提到过,begin(开始)这个关键词在循环中也有用到,就是创建条件后置的循环。
让我们比较一下条件前置循环和条件后置循环的格式:
[pre lang="ruby"]
#条件前置
while 条件 #或者 until 条件
    执行代码
end

#条件后置
begin
    执行代码
end while 条件    #或者end until 条件
[/pre]

条件判断放在前面和后面有什么差别呢?条件放在后面的话,就算打从一开始条件就不符合,代码还是会执行至少一次。
条件放在前面的话,当然如果条件一开始就不符合了,这循环就根本不运行。

还记得then(那就)这个关键词,还有 if/unless ... then ... end 这个格式吗?
喜闻乐见的是,while/until则有do(做)这个关键词作为修饰。while/until ... do ... end 是它的格式。
[pre lang="ruby"]
#普通的循环
while a < 10
    a += 1
end   
until a >= 10 #注意:< 和 >= 是相反的,就如 > 和 <=
    a += 1
end   

#用do修饰,然后缩成一行
while a < 10 do a += 1 end
until a >= 10 do a += 1 end
[/pre]

还记得倒装if/unless吗?等等…为啥总提if/unless?那当然是while/until有相似的写法啦!喜闻乐见的倒装!
[pre lang="ruby"]
#普通的循环
while a < 10
    a += 1
end   
until a >= 10
    a += 1
end   

#倒装的循环
a += 1 while a < 10
a += 1 until a >= 10
[/pre]

嗯,你问我如何写无限循环?说真的,只要让循环的条件永远通过就行了啊!我的习惯则是用while true
[pre lang="ruby"]
while true
    无限循环的内容
end
[/pre]

啥,你又问,无限循环的话游戏不就卡死了吗?要来做啥啊?非常好,还记得RMVA里面的“跳出循环”事件指令吗?
Ruby语言提供了两个在循环内部进行流程操纵的关键词,break(跳出),next(下一个)和redo(重来)。
作为范例,我把之前那个例子改写成使用break来跳出的无限循环:
[pre lang="ruby"]
#条件前置
while true
    break if a >= 10 #或者break unless a < 10
    a += 1
end   
#条件后置
while true
    a += 1
    break if a >= 10 #或者break unless a < 10   
end   
[/pre]
除了用这种方式结束循环之外,(在方法内)使用return(返回)操作也会直接结束循环然后返回数据。

next(下一个)和redo(重来)的用途是跳过此次未执行的代码,然后重头开始循环。
next和redo的分别是,next需要再次通过条件判断,而redo不需要。
假设我们有如此的代码:(redo使用的情况非常稀有,而且RMVA默认脚本也没有用到,因此不提供使用范例)
[pre lang="ruby"]
a = 0
while a < 10
    a += 1
    next if a % 2 == 0 #判断是否偶数
    p a
end
#输出:
1
3
5
7
9
[/pre]
很明显,如果a是偶数,就会直接跳过未执行的代码(显示数据),然后重新回到上边,执行条件判断。

E - 迭代

在写脚本的时候,我们常需要固定次数的循环,并按照循环的次数来执行特定的操作。
[pre lang="ruby"]
i = 0 #初始值
while i < 5 #脱出循环的条件
    p i #执行操作:显示变量
    i += 1 #增加次数
end
#输出:
0
1
2
3
4
[/pre]
当然,聪明的你已经猜到为什么在这儿又列出一次while循环了。当然是有更短更简便的方法啊!

迭代,是程序中对一组指令(或一定步骤)的重复,也可以用来描述一种特定形式的具有可变状态的重复。
在上面的例子中,你可以看到,变量i的值每次重复运行时都会改变(因为i += 1)。这就成了一种迭代。
(注意:我的习惯是用于迭代的变量以i,j,k为主)

Ruby提供了很多种类型的“迭代器”,也就是用于进行迭代的方法。
如果你们曾经学过C,C++,Java等语言,应该会对for这个关键词不陌生。不过在Ruby,这可是禁止事项。
不是Ruby没有for关键词,而是有太多太多种方法可以取代它的存在了。
嗯,你还是新手,不知道什么是for?那不需要管这个,我们继续说“迭代器”。

在Ruby语言中,有非常多种方法可以进行迭代。就像上面那个例子,我们可以这么写:
[pre lang="ruby"]
5.times do |i|
    p i
end
[/pre]
当然也可以这么写:
[pre lang="ruby"]
0.upto(4) do |i| #注:5 - 1 = 4
    p i
end   
[/pre]
还有这么写:
[pre lang="ruby"]
(0...5).each do |i|
    p i
end   
[/pre]
更可以这么写:
[pre lang="ruby"]
(0...5).step(1) do |i|
    p i
end   
[/pre]

如果我们要倒数进行迭代的话,用while来达成的话是:
[pre lang="ruby"]
i = 4 #初始值,注意 5 - 1 = 4
while i >= 0 #脱出循环的条件
    p i #执行操作:显示变量
    i -= 1 #增加次数
end
#输出:
4
3
2
1
0
[/pre]
而使用迭代器的方式是:
[pre lang="ruby"]
4.downto(0) do |i|
    p i
end   
[/pre]

上面列出了五种我们在这部分会介绍的迭代器,也就是数字的times, upto, downto方法,和范围的each, step方法。
每种迭代器都有它自己的用途和适合应用的情况。不过,我们先来说说代码块(code block)这件事。

代码块,是上面的例子中,do到end之间的部分。这是Ruby强大的特性之一,能把代码作为参数传递给方法。
代码块中也可以附上参数(被两个"|"符号夹着,如果需要超过一个就用逗号(,)分隔),储存迭代的状态数据。
在这儿,我们不需要知道代码块和迭代器背后的原理,只需要知道怎么去运用它就行。

当然,Ruby代码有很多很多种写法,这个我们在之前的部分已经体验过了。什么倒装句之类的。
那么在Ruby里面,我们可以而已用{和}符号来代替do和end。
[pre lang="ruby"]
#使用do和end
5.times do |i| p i end
#使用{和}
5.times {|i| p i }
[/pre]
当然我自己的习惯是用后者({}),毕竟我有被其他编程语言影响到。
不过为了可读性,我建议在要缩成一行的时候使用后者(范例中的状况),否者使用前者。

接下来进入正题。首先要介绍的是数字的times迭代器。它的用途是从0开始,迭代到该数字减1的次数。
这等于状态的初始值是0,而循环结束的条件是:状态 < 数字。
[pre lang="ruby"]
3.times do |i|
    p i
end
#输出:
0
1
2   
[/pre]
不过要注意的是,如果是基于浮点数(如3.5)来调用times,和下面会介绍到的upto和downto的话,会先向下取整成整数。

接下来介绍upto和downto这两个迭代器。upto有“上升直到”而downto有“下降直到”的意思。
upto和downto是从指定的初始值(数字),向上(upto)或向下(downto)迭代到递入的参数为止(包括该参数)。
[pre lang="ruby"]
#使用upto,迭代范围:2到5(包括5)
2.upto(5) do |i|
    p i
end
#输出:
2
3
4
5

#使用downto,迭代范围:从4到1(包括1)
4.downto(1) do |i|
    p i
end
#输出:
4
3
2
1
[/pre]

有些人会问,哎呀,我需要比较直观一点的,可以直接看出迭代范围的方法,怎么办?答案就是基于范围的迭代器。
首先,我们有each(每个)方法。比如我们有一个范围是… 1..4(1到4,包括4):
[pre lang="ruby"]
(1..4).each do |i|
    p i
end
#输出:
1
2
3
4   
[/pre]
于是我们可以看到,(1..4).each 和 1.upto(4) 的效果是一样的。注意:含有浮点数的范围不能使用迭代器。
范围数据类型的两个点(..)和三个点(...),还记得分别是什么吗?
复习一下吧。它们的分别是,两个点是包括后面的数,三个点是不包括。
那么也就是说,(1...4).each 和 1.upto(4-1)是同样的效果,而(0...5).each 和 5.times 是同样的效果。

以上提到的四种迭代法,都是一步一步迭代的。有些是往上(数字的times和upto, 范围的each),有些是往下(数字的downto)。
如果每次要增加两步或者更多,要怎么办?范围提供了一个迭代器,叫做step(步行),也就是指定迭代时增加的“步数”(默认参数是1)。
假设我们有一个范围是1..10,而我们要迭代…每次3步。
[pre lang="ruby"]
(1..10).step(3) do |i|
    p i
end
#输出:
1
4
7
10
[/pre]
注意:最低步数是1,递入负数会报错

知道了这些写迭代的方法后,你应该知道为啥for关键词在Ruby里面的使用是被评击的吧。
啥,你是新手,没用过其他程序语言,还不知道什么是for?非常好,让我们来看看这是什么玩意儿。

for(迭代/对于)在这里是配着in(里面)用的。在Ruby里,for循环的格式是:
[pre lang="ruby"]
for 状态变量 in 范围 do #注意:do 是选择性的,就会while循环差不多
    执行代码
end
[/pre]

给个例子:
[pre lang="ruby"]
for i in (1..4)
    p i
end
#输出:
1
2
3
4   
[/pre]
嗯,你应该是发现到了。这代码怎么和之前我们提到的,范围的each那么相似?答对了,它们是相等的!
因此,我不建议使用for这个关键词,而是调用迭代方法,递入代码块来进行迭代(就像上面的各种例子)。
比较一下RMXP和RMVA的默认脚本的话,RMVA的默认脚本已经没有使用for了,可以看得出成长。

还记得之前在“循环”的部分学到的,break(跳出), next(下一个) 和 redo(重来) 这三个关键词吗?
它们在迭代里面起到一样的作用,所以就不再多做说明了。
那么之前在“除错机制”部分有提到过,retry(重试)这个关键词在迭代中有起到作用。
retry的用途就是从头开始执行迭代,也就是重置初始值。
[pre lang="ruby"]
3.times do |i|
    p i
    retry if i == 2
end   
#输出:
0
1
2
0
1
2
…(然后就无限循环了)
[/pre]
当然有没有机会在迭代里用到retry也是一个问题,就看看就好吧。
   




1.6 数组与哈希表

展开折叠

在编写游戏脚本的时候,我们常需要非常多的变量。一个角色,就有体力啊魔力啊攻击啊什么的。
那么要如何更有效地管理这些数据,是一种学问。对此,Ruby提供了两种“数据容器”,也就是数组(Array)和哈希表(Hash)。

A - 数组

数组(Array),可以理解为“数据的组合”。百度百科:数组是相同数据类型的元素按一定顺序排列的集合。
不过在Ruby语言里面,数组内的元素可以是任何类型,不管是整数,字符串,符号,甚至是数组本身。
RMVA默认脚本中使用数组的幅度非常大。队伍内的角色,角色的装备,窗口上显示的指令列表等等。

创建数组的方式有数种。第一种就是直接赋值。格式是:[数组内容] (如果有多于一个元素,就用逗号(,)来分隔)
[pre lang="ruby"]
#创建一个含有整数1,2,3,4,5的数组
arr = [1,2,3,4,5]

#创建一个含有字符串"abc","cde","efg"的数组
arr2 = ["abc","cde","efg"]

#创建一个整数和字符串都有的数组
arr3 = [1,"abc",2,"cde",3,4,5,"efg"]

#创建什么都没有的空数组
arr4 = []

#显示数据
p arr #输出:[1,2,3,4,5]
p arr2 #输出:["abc","cde","efg"]
p arr3 #输出:[1,"abc",2,"cde",3,4,5,"efg"]
p arr4 #输出:[]
[/pre]
注:我的习惯是以arr(Array的缩写)来作为数组的暂时变量名

放入数组的内容实际上是表达式。也就说我们可以当场运算了数值后放进数组中。
[pre lang="ruby"]
#加法的方法
def add(a,b)
    return a + b
end   

#变量定义
c = 5
d = 7

#创建数组
arr = [1, 3, c, d, d + 2, c + 6, add(c,d) + 1]

#显示数据
p arr #输出:[1,3,5,7,9,11,13]
[/pre]

以上的例子创建的数组的长度(元素的数量)会自动调整。给5个元素,长度就是5咯。
如何知道数组的长度?我们可以用以下几个方法:size(大小), length(长度), count(数量)。
count可以接受参数来返回更进一步的信息,这个我们之后会提到。
[pre lang="ruby"]
#创建数组
arr = [1,2,3]
arr2 = []
arr3 = ["abc",1,2,5,7,"cde"]

#显示数据
p arr.size #输出:3
p arr2.length #输出:0
p arr3.count #输出:6
[/pre]
在这之后的范例,有需要知道数组内元素的数量的时候,会用size来获取。

有些时候我们需要创建只有空壳的数组,也就是光有长度但没有实际数据的数组。
看到这里,大家可能就懵了。别担心,来回想一下nil(空值)这个特殊的数据类型。
有些时候,我们需要创建一个数组,指定它的长度,但是暂时还不把数据放进去。
为什么呢?这可能是要先弄一个包含着500个元素的大空壳数组,然后再用迭代的方式把内容塞进去。
不然啊,在代码里直接写上500个数字什么的实在是太蛋疼了啊。

好,那么我们要知道的是,创建数组的另一个方法,就是用Array.new(Array是数组,new是新的意思)。
它又分成很多种用法,让我们直接来范例吧。
[pre lang="ruby"]
#创建空的数组
arr = Array.new #同 arr = []

#创建长度为4的空壳数组
arr1 = Array.new(4) #同 arr = [nil, nil, nil, nil]

#使用代码块来执行填入内容的迭代计算
arr2 = Array.new(5) {|i| i = i + 1} #0 + 1 = 1, 1 + 1 = 2, 2 + 1 = 3...

#使用代码块填入重复的内容
arr3 = Array.new(4){"a"} #同arr = ["a", "a", "a", "a"]

#显示内容
p arr #输出:[]
p arr1 # 输出:[nil,nil,nil,nil]
p arr2 # 输出:[1,2,3,4,5]
p arr3 # 输出:["a","a","a","a"]
[/pre]

创建了数组之后,我们要知道怎么进行元素的存取。我们会用到的符号,是元素引用符([ ])。
数组的每个元素,都配着一个索引(index)。这个索引是从0开始算起的。也就是说,数组内的第一个元素,索引是0。
数组内的第三个元素,索引是2。第十一个元素,索引是10,以此类推。
[pre lang="ruby"]
arr = [1,2,3,4]
p arr[0] #输出: 1
p arr[1] #输出: 2
p arr[2] #输出: 3
p arr[3] #输出: 4
p arr[4] #输出:nil
p arr[105] #输出:nil
[/pre]
如果[]内填入的索引值超出了许可的范围,那么就会得到nil(空值),也代表着获取失败。

那么如果填入的索引值是负数呢?那就是从后面开始获取。
[pre lang="ruby"]
arr = [1,2,3,4]
p arr[-1] #输出: 4
p arr[-2] #输出: 3
p arr[-3] #输出: 2
p arr[-4] #输出: 1
p arr[-5] #输出:nil
p arr[-100] #输出:nil
[/pre]

除了单独获取元素之外,我们还可以从一个数组中获取一组数据,返回的是新的数组。
[pre lang="ruby"]
arr = [1,2,3,4,5,6,7,8]
p arr[2,3] #输出:[3,4,5] ,意思是从2号索引开始,取3个元素
p arr[1..4] #输出:[2,3,4,5] ,基本上就是递入一个范围作为索引,获取1到4号索引之间的元素
p arr[2..-3] #输出:[3,4,5,6] ,意思就是从2号索引开始,获取元素直到(包括)倒数第三个索引为止
[/pre]

那么知道怎么获取数组内的元素之后,让我们来看看如何赋值吧。赋值呢,就…加个=符号啊。
[pre lang="ruby"]
arr = [1,2,3,4,5,6,7,8]
arr[4] = 9 #此时arr的值:[1,2,3,4,5,9,7,8]
arr[-2] = 10 #此时arr的值:[1,2,3,4,5,9,10,8]
arr[3,2] = [11,12] #此时arr的值:[1,2,3,11,12,9,10,8]
arr[0..2] = [13,14,15] #此时arr的值:[13,14,15,11,12,9,10,8]
arr[1..3] = 16 #此时arr的值:[13,16,12,9,10,8]
[/pre]
好复杂啊,对吧?你只要记得,获取的码后面加个=就是赋值了。arr[4]是获取, arr[4] = 就是赋值嘛。

数组也有加减等基本运算。主要是这六个符号:+(加), -(减), *(乘), |(位或), &(位和),<<(左位移)。
[pre lang="ruby"]
arr = [1,1,2,2,3,3,4,4,5,6]
p arr << 7 #输出:[1,1,2,2,3,3,4,4,5,6,7] ,注意 << 7 与 += [7] 相似
p arr += [8,9] #输出:[1,1,2,2,3,3,4,4,5,6,7,8,9]
p arr -= [2,3,4,7,8,9] #输出:[1,1,5,6]
p arr *= 3 #输出:[1,1,5,6,1,1,5,6,1,1,5,6]
p arr |= [1,2,3,4] #输出:[1,5,6,2,3,4]
p arr &= [0,1,5,4,7,8,9] #输出:[1,5,4]
[/pre]
首先是<<符号。你看到两个向左的箭头嘛,你想象成是“注入”就好。把单个元素注入数组里面。
数组的加法(+),就是把两个数组直接尾对头粘连起来。[a,b,c] + [c,d,e] = [a,b,c,c,d,e] 这样的。
那么减法(-)呢,就是从前面的数组里面把后边数组有的元素都去除掉。要注意的是,重复的元素全部都会被去除。
乘法(*),乘法基本上就是相同的数组连续相加几次(后面整数)。还有一种是字符串的,你自己尝试就知道了呗。(被打)
位或(|),和加法差不多,但是重复的元素会被去除,每个元素都只剩下一个了。
位和(&),就是将两边的数组里面共同拥有的元素留下(重复的会去除),其他的全部去除。
当然除了<<会影响本来的变量以外,其他的运算都是返回新数组的。要用=来赋值才会影响本来的变量。

数组到底是了什么而存在的啊?就只是为了少用一点变量吗?当然不只是这样。
Ruby的数组内建的方法可是多得咋舌呢。最直接体现数组用途的,就是迭代了吧。
数组有三个主要迭代方法:each(每个元素), each_index(每个索引), each_with_index(每个元素和索引)。
[pre lang="ruby"]
#使用each迭代数组里的每个元素
[1,3,"abc",7,9].each do |i|
    p i
end
#输出:
1
3
"abc"
7
9   

#使用each_index迭代数组的索引,与元素无关
["abc","x","y","z","cde"].each_index do |i|
    p i
end
#输出:
0
1
2
3
4   

#使用each_with_index迭代数组的元素和索引,注意有两个代入变量
#注:value是"值"的英文,index是"索引"的英文
#为了区分才用了这些变量名字,平时大可用什么i啊v的来代替,看个人喜好而定
["a","b","c","d","e"].each_with_index do |value, index|
    p value + " " + index.to_s
end
#输出:*注:to_s是转换成字符串,整数要先转换了才能与字符串相加。
"a 0"
"b 1"
"c 2"
"d 3"
"e 4"
[/pre]

Ruby的数组可以装各种类型的数据,就连数组本身也可以。这是之前提到的。
于是在这里就延伸出了一种用法:多维数组。数组套着数组就是多维数组了呀。
[pre lang="ruby"]
#创建一个二维数组
arr = [[1,2,3],[4,5,6],[7,8,9]]
#获取二维数组的元素
p arr[0][2] #输出:3 (第一行,第三列)
#给二维数组的元素赋值
arr[1][1] = 0 #此时arr的值:[[1,2,3],[4,0,6],[7,8,9]]
#使用双重迭代来将二维数组表示成矩阵
arr.each do |a| #注意:这里的a是数组
    s = ""
    a.each do |i|
        s += i.to_s
    end
    p s
end   
#输出:
"123"
"406"
"789"
[/pre]

数组想要套多少层都没问题,甚至也不一定每个元素都要是数组。(比如[1,2,[3,4,5],6,[7,8]]是可以的)
那么在这里要仔细说明一下,多维数组的获取到底是什么样的原理。
[pre lang="ruby"]
#创建二维数组
arr = [[1,2,3],[4,5,6],[7,8,9]]
#获取二维数组的元素
p arr[0][2] #输出:3
#使用暂存的方式获取二维数组的元素
p a = arr[0] #输出:[1,2,3]
p a[2] #输出:3
[/pre]
于是你可以看到,arr[0][2] 就是先获取了arr[0]这个数组,然后再用[2]来获取指定的元素。
我们也可以这么形容:arr[0][2] = [1,2,3][2] = 3(因为arr[0] = [1,2,3])。

在“方法定义与调用”的部分里,我们曾经提到有一种方法的定义是可以接受无限量的参数的。
这是因为多余的参数会被存放在一个数组里面待取用。就让我们直接看范例吧。
[pre lang="ruby"]
#方法定义
def abc(*args) #注意那个*符号!
    a = 0
        p args
    args.each do |i|
            a += i
        end
    return a       
end

#方法调用
p abc #输出(分两行):[] 和 0
p abc(1) #输出(分两行):[1] 和 1
p abc(1,2) #输出(分两行):[1,2] 和 3
p abc(1,2,3,4) #输出(分两行):[1,2,3,4] 和 10
[/pre]
额外参数(*args)必须放在最后面,如:def abc(a,b,*args),而且不能和默认参数混用。

Ruby的数组还有非常非常多的内置方法。我们将在本章最后面的“系统方法”部分一一认识。

B - 哈希表

数组是以整数为索引,有顺序的一种数据集合。但是,如果没有排序的需求呢?如果索引不是数字,而是字符串或者其他的数据类型呢?
就算索引是使用的数字,但是如果差距实在是太大太大了呢?(比如一个值的索引要设置5,另一个560,再一个1900,难道要弄长度1900的数组吗?)
这时,我们可以用哈希表(Hash,哈希是音译)。

哈希表是一种键值对集合。一个键(key,钥匙的意思)对应一个值(value)。
这么形容好了。假设哈希表是一排锁着的柜子,里面收藏着数据。那么每一层都有对应的钥匙,能打开然后取出/置入数据。
哈希表的键和值可以是任何类型的数据。与数组不一样的是,使用哈希表的时候,我们不注重元素储存的顺序。
RMVA里面使用哈希表的地方有:队伍拥有的物品/装备,地图的事件,素材导入的暂存(Cache模块)等等。

创建Hash的方法也有几种。最简单的就是直接赋值。与数组的[]不同,创建哈希表的符号是{}。
哈希表内容的格式是: 键(key) => 值(value) ,如果有超过一个的话,用逗号(,)来分隔。
[pre lang="ruby"]
#创建空的哈希表
hash = {}
#创建单个内容的哈希表
hash1 = {"a" => 1}
#创建多个内容的哈希表
hash2 = {"a" => 1, "b" => "abc", 3 => "c"}
#显示内容
p hash #输出:{}
p hash1 #输出:{"a" => 1"}
p hash2 #输出:{"a" => 1, "b" => "abc", 3 => "c"}
[/pre]

在介绍其他创造哈希表的方法之前,我们需要知道如何存取哈希表的数据。这和数组是一样的,只是[]里面要填入的是键(key)。
[pre lang="ruby"]
#创建哈希表
hash = {"a" => 1, "b" => "abc", 3 => "c"}
#读取数据
p hash["a"] #输出:1
p hash["b"] #输出:"abc"
p hash[3] #输出:"c"
p hash[666] #输出:nil
#储存数据
hash["a"] = 5 #此时hash是{"a" => 5, "b" => "abc", 3 => "c"}
hash[3] = :abc #此时hash是{"a" => 5, "b" => "abc", 3 => :abc}
hash[666] = [1,2,3] #此时hash是{"a" => 5, "b" => "abc", 3 => :abc, 666 => [1,2,3]}
[/pre]
从上面的例子我们可以看到,尝试引用哈希表内不存在的键时,会返回一个可以设置的默认值。如果没有默认值,则返回nil(空值)。
如果引用哈希表内不存在的键来赋值的话,这个键值配对就会加入哈希表里面,作为储存的数据。

好,那么就让我们来看看,还有那些创建哈希表的方法吧。
[pre lang="ruby"]
#创建哈希表
hash = Hash.new #创建一个空的哈希表,同 hash = {}
hash1 = Hash.new(666) #创建一个空的哈希表,并设置默认值
#尝试读取不存在哈希表内的键
p hash[233] #输出:nil
p hash1[233] #输出:666
[/pre]
默认值在一些时候很有用,比如背包内物品的数量,没有的话就返回0,这样的用法。

与数组相似,哈希表也有三种迭代方法,分别是each(每个),each_key(每个键)和each_value(每个值)。
[pre lang="ruby"]
hash = {"a" => "x", "b" => "y", "c" => "z"}
#使用each遍历哈希表的键值配对
hash.each do |key,value| #注意:格式是 |键,值| ,与数组的each_with_index(|值,索引|)相反
    p key + " " + value
end
#输出:
a x
b y
c z

#使用each_key遍历哈希表的键
hash.each_key do |key|
    p key
end
#输出:
a
b
c

#使用each_key遍历哈希表的值
hash.each_value do |value|
    p value
end
#输出:
x
y
z
[/pre]

哈希表的各种内置方法,也会在“系统方法”的部分一一介绍。
   




1.7 系统方法

展开折叠

本章的最后一个部分将介绍Ruby所提供的系统方法。
能善用这些方法对游戏制作来说能起到很大的帮助。

A - 核心全局方法

Ruby的核心(Kernel)提供了最主要的几个系统方法。这些方法可以在脚本的哪儿都可以使用,所以叫全局方法。

1. p, print, puts (这三种都是在控制台内显示数据的方法)
p这个方法,本教程里面大肆使用。这方法,应该不会感到陌生了吧。
实际上,Ruby还有两个可以在控制台里显示数据的方法,就是print和puts。
很多人问我,你怎么就是喜欢用p,而不是用puts呢?来来来,让我慢慢解释。

print和puts在控制台显示数据的时候,会调用数据的to_s内置方法,将它转换成字符串。
p则是调用了inspect(检视)内置方法。虽然一般上to_s和inspect生成的内容都差不多的,但!是!
字符串本身在调用inspect方法来转换成字符串的时候,会有双引号包着。
这样会更容易检查该变量是否字符串,而不容易与其他数据类型混淆。

那么print和puts的差别,就是print不会自动换行,而puts会自动换行。
[pre lang="ruby"]
str = "abc"
p str
p str
puts str
puts str
print str
print str

#输出:
"abc"
"abc"
abc
abc
abcabc
[/pre]

2. rand(随机数生成,random的缩写,意思是随机)
rand的用途是生成一个0.0到1.0之间的浮点数(如0.56789)。
如果递入整数作为参数,那么就会生成0到该整数之间的一个随机整数。
[pre lang="ruby"]
p rand #输出:0.3355309950867542(0.0到1.0之间的随机浮点数)
p rand #输出:0.7407691203989254(0.0到1.0之间的随机浮点数)
p rand(1000) #输出:819(0到1000之间的随机整数,范围是0...1000,不包括1000)
p rand(10) #输出:7(0到10之间的随机整数,范围是0...10,不包括10)
[/pre]

3. srand (随机数设定,seed rand的缩写,意思是随机的“种子”)
srand的用途是设定随机生成器的“种子”(seed),以得到固定的随机数。
格式是srand(整数)。如果没有递入参数,那么srand将随机生成一个数字来作为“种子”。
srand调用时将返回之前所使用的“种子”
[pre lang="ruby"]
srand 1234
p rand(100) #输出:47
p rand(100) #输出:98
p srand 3456 #输出:1234 (之前的“种子”)
p rand(100) #输出:27
p rand(100) #输出:98
p srand 1234 #输出:3456
p rand(100) #输出:47
p rand(100) #输出:98
[/pre]
你会发现,设置1234作为srand的种子之后,首两次使用rand(100)必定分别获得47和98。

4. sprintf (文本格式化,string print format的缩写,三个英文字分别是:字符串,打印,格式)
sprintf的使用法可以在F1汉化版帮助文档的附录里面读到。
在这里就不多加解释了,只提供几个范例。
[pre lang="ruby"]
#将数字直接“打印”到字符串里面
p sprintf("%d", 12345) #输出:"12345"
#将浮点数“打印”进字符串,要求字符串长度是20,并显示到小数点后8位
p sprintf("%20.8f", 1234.56789) #输出:"       1234.56789000"
[/pre]

5. loop (无限循环)
还记得while(true)这个无限循环法吗?其实这可以用loop来代替。
[pre lang="ruby"]
loop do
    p "无限循环哦哦哦哦哦~"
end
#输出:
"无限循环哦哦哦哦哦~"
"无限循环哦哦哦哦哦~"
"无限循环哦哦哦哦哦~"
...(然后就停不下来了)
[/pre]
就像while(true)那样,无限循环需要用break,next,redo等关键词来操纵流程。

6. raise (手动报错,raise的意思是提起)
raise这个方法是用于手动报错的。使用的情况呢,比如你是程序员嘛,你写了个脚本给别人用。
那么你知道如果设置错误的话迟早会出错,你就加个检查然后手动报错,告诉使用者是他的问题而不是你的。
raise的第一个参数可以设置错误讯息或者弹窗时的错误类型。关于错误类型,可以翻F1汉化版帮助文档来看看。
如果raise的第一个参数是字符串(错误讯息),那么错误类型会是RuntimeError(执行时出错)。否则,raise的第二个参数是错误讯息。
raise的第三个参数是关于出错的脚本和行数。获取出错脚本/行数的方法是caller。
[pre lang="ruby"]
raise "我去,出错了!" #执行这一句后会弹报错窗,错误类型是 RuntimeError(执行时出错),讯息是:我去,出错了!
raise ArgumentError, "参数设置不对啊亲。" #执行这一句后会弹出ArgumentError(参数错误),然后显示讯息:参数设置不对啊亲。
raise StandardError, "出错", caller #执行后弹窗,显示StandardError错误类型和讯息,也显示出错的脚本和行数。
[/pre]

7. eval (运行代码,evaluate的缩写,意思是执行运算)
它用于执行存在字符串里面的代码后将运行的最后一个语句的值返回,是个很恐怖的存在。
事件脚本指令的实现原理是用了eval这个方法。
[pre lang="ruby"]
#例子1
str = "i = 5; i+= 1; p \"a\" + i.to_s"
eval str #输出:"a6" ,基本上字符串里面的代码全都运行了
#例子2
a = 6
p eval "a *= 2; a + 3" #输出:15 ,因为运行的最后一个语句会作为返回值
[/pre]

B - 数学运算方法

就如“方法调用”部分里提到的,Ruby内置三角函数等高级数学运算的方法。在这儿就不一一列出例子了,毕竟用的幅度不高。
使用下列的方法时必须用 Math.方法名 的格式。(方法调用部分里有代码范例)

1. 三角函数(注意:单位是弧度而不是角度)
sin (正弦函数), cos (余弦函数) tan (正切函数)
asin (反正弦函数), acos (反余弦函数) atan (反正切函数)
sinh (双曲正弦函数), cosh (双曲余弦函数), tanh (双曲正切函数)
asin (反双曲正弦函数), acosh (反双曲余弦函数), atanh (反双曲正切函数)
atan2 (反正切函数2)注:此函数接受X和Y两个参数,格式为:atan2(x,y)

2. 对数函数
log (对数函数)注:格式为:log(真数,底数) ,如果不递入底数,那么就使用常量e为底数。
log2 (底数为2的对数函数)
log10 (底数为10的对数函数)

3. 乘方,方根
exp (常量e的乘方)
sqrt (平方根)
cbrt (立方根)

4. 数学常量 注:这个部分列出的是常量而不是方法。使用格式为: Math::常量
PI (圆周率)
E (常量e)

C - 数据类型转换方法

Ruby提供了数种类型转换的方法,从一个数据类型转换到另一个数据类型。

1. to_i (转换到整数) 适用数据类型:字符串,浮点数
[pre lang="ruby"]
#将字符串转换成整数(注意:可以递入参数指定进制)
p "1234".to_i #输出:1234 (默认为十进制)
p "0b011101".to_i(2) #输出:29 (二进制)
p "011101".to_i(2) #输出:29 (二进制,前面不加0b也可以)
p "0x5F6C".to_i(16) #输出:24428 (十六进制)
p "5F6C".to_i(16) #输出:24428 (十六进制,前面不加0x也可以)
p "efgh".to_i #输出:0 (转换失败)
#将浮点数转换成整数
p (3.14).to_i #输出:3
p (-15.6).to_i #输出:-15
[/pre]

2. to_f (转换到浮点数) 适用数据类型:字符串,整数
[pre lang="ruby"]
#将字符串转换成浮点数
p "3.14".to_f #输出:3.14
p "abc".to_f #输出:0.0 ,注意:转换失败
p "1.602e-19".to_f #输出:1.602e-19 ,注意:这等于 1.602 * (10 ** -19)
#将整数转换成浮点数
p 2333.to_f #输出:2333.0
[/pre]

3. to_s (转换成字符串) 适用数据类型:几乎所有数据类型
[pre lang="ruby"]
#将整数转换成字符串(注意:可以递入参数指定进制)
p (10086).to_s #输出:"10086" (默认为十进制)
p (133).to_s(2) #输出:"10000101" (二进制)
p (240).to_s(16) #输出:"f0" (十六进制)
#将浮点数转换成字符串
p (4.5).to_s #输出:"4.5"
#将符号转换成字符串
p (:a_b_c).to_s #输出:"a_b_c"
#将范围转换成字符串
p (1..10).to_s #输出:"1..10"
#将真值转换成字符串
b = true
p b #输出:"true"
#将数组转换成字符串
arr = [1,2,3,[7,8],4]
p arr.to_s #输出:"[1, 2, 3, [7, 8], 4]"
#将哈希表转换成字符串
hash = {1 => "a", 2 => [5,6,7]}
p hash.to_s #输出:"{1=>\"a\", 2=>[5, 6, 7]}"
[/pre]
注:某些符号,比如单引号和双引号,会前缀\符号来“转义”(就如事件显示文字指令的控制符)。

4. to_sym (转换成符号) 适用数据类型:字符串
[pre lang="ruby"]
#将字符串转换成符号
p "abc123".to_sym #输出::abc123
p "@a_b_c".to_sym #输出::@a_b_c
p "123abc".to_sym #输出::"123abc"
[/pre]
此方法可以生成原本直接创建时会出错的符号。

5. to_a (转换成数组) 适用数据类型:范围,哈希表
[pre lang="ruby"]
#将范围转换成数组
p (1..10).to_a #输出:[1,2,3,4,5,6,7,8,9,10]
#将哈希表转换成数组
hash = {"a" => 2, "b" => 4, 5 => 6}
p hash.to_a #输出:[["a", 2], ["b", 4], [5, 6]]
[/pre]

D - 整数/浮点数内置方法

1. integer?(判定是否整数)
[pre lang="ruby"]
#判断是否整数 - 用于鉴别整数和浮点数
p (10086).integer? #输出:true
p (23.333).integer? #输出:false
[/pre]

2. odd?(判定是否奇数), even?(判定是否偶数) 注:只适用于整数
[pre lang="ruby"]
#判断整数是否奇数
p (8).odd? #输出:false
p (7).odd? #输出:true
#判断整数是否偶数
p (8).even? #输出:true
p (7).even? #输出:false
[/pre]

3. zero?(判定是否为零)
[pre lang="ruby"]
#判定是否为零
p (1024).zero? #输出:false
p (0).zero? #输出:true
p (3.142).zero? #输出:false
p (0.0).zero? #输出:true
[/pre]

4. pred(上一个数,,predecessor的缩写), next(下一个数) 注:只适用于整数
[pre lang="ruby"]
#变量赋值
a = 22
#获取上一个数
p a.pred #输出:21 ,注:与 p (a - 1) 相同
#获取下一个数
p a.next #输出:23 ,注:与 p (a + 1) 相同
[/pre]

5. abs(绝对值, absolute的缩写)
[pre lang="ruby"]
#获取绝对值
p (666).abs #输出:666
p (-666).abs #输出:666
p (3.142).abs #输出:3.142
p (-3.142).abs #输出:3.142
[/pre]

6. times(次数), upto(上至), downto(下至)
数字的times, upto和downto都有在“迭代”的部分说明过,可以回去翻阅。
浮点数调用这个方法时,会自动向下取整。
[pre lang="ruby"]
#times, upto 和 downto 迭代器
5.times {|i| print i} #输出: 01234
print "\n" #换行
1.upto(5){|i| print i} #输出:12345
print "\n" #换行
5.downto(1){|i| print i} #输出:54321
print "\n" #换行
[/pre]

7. numerator(分子), denominator(分母)
[pre lang="ruby"]
#整数调用这两个方法时,分子必定为自身,分母必定为1
p (25).numerator #输出:25
p (25).denominator #输出:1
#获取浮点数的分子和分母
p (0.75).numerator #输出:3
p (0.75).denominator #输出:4
#0.75用分数来表达是(3/4),这肯定是没错的
[/pre]

8. chr(转换为字符,character的缩写) 注:只适用于整数
[pre lang="ruby"]
#获取字符
p (100).chr #输出:"d" ,注:字母d的ASCII码是100
p (64).chr #输出:"@" ,注:符号@的ASCII码是64
p (256).chr #出错!chr方法只适用于范围(0..255)的整数
[/pre]

9. round(四舍五入取整), ceil(向上取整, ceiling的缩写), floor(向下取整)
round, ceil, floor这三种取整法都能接受一个递入参数,指定小数点前后的位数。正数是后,负数是前。
[pre lang="ruby"]
#round, ceil和floor对整数使用时的情况
p 123.round #输出:123 (没有变动,因为本来就是整数)
p 123.round(2) #输出:123.0 (变成浮点数类型了)
p 123.round(-1) #输出:120(四舍五入,十位)
p 165.round(-2) #输出:200(四舍五入,百位)
#四舍五入取整
p (3.142).round(2) #输出:3.14(四舍五入,小数点后2位)
p (13666.256).round(-4) #输出:10000(四舍五入,万位)
p (3.9).round #输出:4(四舍五入,转换成整数)
p (3.1).round #输出:3(四舍五入,转换成整数)
#向上取整
p (3.9).ceil #输出:4(向上取整,转换成整数)
p (3.1).ceil #输出:4(向上取整,转换成整数)
#向下取整
p (3.9).floor #输出:3(向下取整,转换成整数)
p (3.1).floor #输出:3(向下取整,转换成整数)
[/pre]

10. gcd(最大公因数,greatest common divisor的缩写),lcm(最小公倍数,lowest common multiple的缩写)
[pre lang="ruby"]
#获取最大公因数
p 36.gcd(45) #输出:9
p 12.gcd(9) #输出:3
#获取最小公倍数
p 36.lcm(45) #输出:180
p 12.lcm(9) #输出:36
[/pre]

11. gcdlcm(同时获取最大公因数和最小公倍数)
[pre lang="ruby"]
#同时获取最大公因数和最小公倍数,储存在数组里
p 36.gcdlcm(45) #输出:[9, 180]
p 12.gcdlcm(9) #输出:[3, 36]
[/pre]

E - 范围内置方法

1. begin(开头), first(第一个), end(尾端), last(最后一个)
[pre lang="ruby"]
#变量赋值
a = (2..10)
b = (4...15)
#begin和first都是获取范围的开头值
p a.begin #输出:2
p a.first #输出:2
p b.begin #输出:4
p b.first #输出:4
#end和last都是获取范围的结尾值
#不管范围的模式是..(包括尾端)还是...(不包括尾端)都一样
p a.end #输出:10
p a.last #输出:10
p b.end #输出:15
p b.last #输出:15
[/pre]

2. each(每个), step(步数)
范围的each和step都有在“迭代”的部分说明过,可以回去翻阅。
[pre lang="ruby"]
#each迭代
(1..4).each do |i|
        p i
end
#输出:
1
2
3
4
#step迭代
(1..10).step(3) do |i|
        p i
end
#输出:
1
4
7
10
[/pre]

3. include? (判断是否包含)
[pre lang="ruby"]
#判断范围是否包含递入的参数
p (1..10).include?(9) #输出:true
p (1..10).include?(10) #输出:true
p (1..10).include?(11) #输出:false
p (1...10).include?(9) #输出:true
p (1...10).include?(10) #输出:false (因为不包括尾端)
p (1...10).include?(11) #输出:false
[/pre]

4. exclude_end? (判断是否不包括尾端)
[pre lang="ruby"]
#判断是否不包括尾端
p (2..10).exclude_end? #输出:false
p (4...15).exclude_end? #输出:true
[/pre]

F - 符号内置方法

1. [] (元素索引符)
符号里的每一个字符都可以像数组那样透过[]符号取出
[pre lang="ruby"]
#变量赋值
a = :RPGMaker
#使用[]符号获取数组内的字符,输出为字符串
p a[0] #输出:"R"
p a[6] #输出:"e"
p a[-1] #输出:"r"
p a[-3] #输出:"k"
p a[1,3] #输出: "PGM"
p a[2...7] #输出:"GMake"
p a[3..-1] #输出:"Maker"
[/pre]

2. length(长度), size(大小)
[pre lang="ruby"]
#length和size都是获取符号的长度
a = :hello
b = :rpgmaker
p a.length #输出:5
p a.size #输出:5
p b.length #输出:8
p b.size #输出:8
[/pre]

3. capitalize(首字母大写,其余小写)
[pre lang="ruby"]
#获取一个新符号,首字母大写而其他的字母小写
p (:hello).capitalize #输出::Hello
p (:HeLlo).capitalize #输出::Hello
[/pre]

4. upcase(全部字母大写), downcase(全部字母小写), swapcase(字母大小写互换)
[pre lang="ruby"]
#获取一个新的符号,全部字母都大写
p (:RPGMaker).upcase #输出::RPGMAKER
#获取一个新的符号,全部字母都小写
p (:RPGMaker).downcase #输出::rpgmaker
#获取一个新的符号,字母大小写互换
p (:RPGMaker).swapcase #输出::rpgMAKER
[/pre]

5. casecmp(不计字母大小写的复合比较)
[pre lang="ruby"]
#复合比较两个符号,不计较字母的大小写
#等同 a.downcase <=> b.downcase (假设a和b都是符号)
p (:hello).casecmp(:HeLlo) #输出:0 (代表等于)
p (:hello5).casecmp(:HeLlo2) #输出:1 (代表大于)
p (:hello1).casecmp(:HeLlo2) #输出:-1 (代表小于)
[/pre]

G - 字符串内置方法

1. % (位和运算符)
[pre lang="ruby"]
#字符串的%运算符的功能与核心的sprintf方法相似
#请参考核心方法部分的sprintf以获得更多信息
#递入多个参数时需要用数组或哈希表来承载
str = "%05d" % 123
p str #输出:"00123"
#注:与 str = sprintf("%05d", 123) 相似

str = "%-5s: %08x" % [ "ID", 12345678 ]
p str #输出:"ID   : 00bc614e"
#注:与 sprintf("%-5s: %08x","ID",12345678) 相似

str = "123 = %{abc}" % { :abc => "xyz" }
p str #输出:"123 = xyz"
#注:与 sprintf("abc = %{abc}",{ :abc => "xyz" }) 相似
[/pre]

2. * (乘法运算符), << (左位移运算符)
[pre lang="ruby"]
#字符串的*运算和和数组的*运算相似
str = "abc"
p str * 3 #输出:"abcabcabc"

#字符串的<<运算和和数组的<<运算相似
str = "abc"
str << "def" << "ghi"
str << "jkl"
p str #输出:"abcdefghijkl"
[/pre]

3. [] (元素索引符), []=(元素赋值符)
[pre lang="ruby"]
#字符串内的字符可以像数组那样使用[]取出或者用[]=赋值
#用法和数组的[]和[]=相似,只是取出或赋值的数据是字符串
#如果索引超出字符长度,[]引用会返回nil(空值)而[]=赋值会报错
str = "abcdefgh"
p str[3] #输出:"d"
p str[-2] #输出:"g"
p str[1,3] #输出:"bcd"
p str[2..5] #输出:"cdef"
p str[0...-2] #输出:"abcdef"

str = "abcdefghijkl"
str[3] = "z"; p str #输出:"abczefghijkl"
str[-2] = "y"; p str #输出:"abczefghijyl"
str[1,3] = ""; p str #输出:"aefghijyl"  ("bcz"被""取代了)
str[2..5] = "123"; p str #输出:"ae123jyl" ("fghi"被"123"取代了)
str[4] = "456"; p str #输出:"ae12456jyl" ("3"被"456"取代了)
[/pre]

4. empty?(判断是否为空)
[pre lang="ruby"]
#变量赋值
a = ""
b = "rpgmaker"
#判断字符串是否是空着的
p a.empty? #输出:true
p b.empty? #输出:false
[/pre]

5. length(长度), size(大小)
length和size都是获取字符串的长度
[pre lang="ruby"]
str = ""
str1 = "66rpg"
str2 = "rpgmaker"
#获取字符串的长度
p str.length #输出:0
p str.size #输出:0
p str1.length #输出:5
p str2.size #输出:5
p str3.length #输出:8
p str4.size #输出:8
[/pre]

6. insert(注入), delete(删除), delete!(毁灭性删除),clear(清空)
[pre lang="ruby"]
#insert用于在字符串的指定位置注入另一个字符串
#格式:insert(位置, 字符串)
str = "abcdefgh"
str.insert(5,"XYZ")
p str #输出:"abcdeXYZfgh"

#delete用于删除字符串内所有匹配递入参数的部分, 作为新字符串返回
str = "66rpg66rpg66rpg"
p str.delete("rpg") #输出:"666666"

#后缀!的delete!是毁灭性操作,也就是会影响原本的字符串
str = "66rpg66rpg66rpg"
str.delete!("rpg")
p str #输出:"666666"

#clear用于清空字符串内的所有字符
str = "abcdefg"
str.clear
p str #输出:""
[/pre]

7. start_with?(判断开始), end_with?(判断结尾)
[pre lang="ruby"]
#start_with?用于判断字符串是否以某个字符序列开头
a = "abcdefgh"
p a.start_with?("fgh") #输出:false
p a.start_with?("abc") #输出:true
#end_with?用于判断字符串是否以某个字符序列结尾
a = "abcdefgh"
p a.end_with?("fgh") #输出:true
p a.end_with?("abc") #输出:false
[/pre]

8. include?(判断是否包含)
[pre lang="ruby"]
#include?用于判断字符串内是否包含作为递入参数的字符序列
a = "abcdefgh"
p a.include?("cde") #输出:true
p a.include?("xyz") #输出:false
[/pre]

9. casecmp(不计大小字母的的复合比较)
[pre lang="ruby"]
#复合比较两个字符串,不计较字母的大小写
#等同 a.downcase <=> b.downcase (假设a和b都是字符串)
p "rpgmaker".casecmp("RpGmAkER") #输出:0 (代表等于)
p "rpgmakerx".casecmp("RpGmAkERc") #输出:1 (代表大于)
p "rpgmakera".casecmp("RpGmAkERc") #输出:-1 (代表小于)
[/pre]

10. bytesize(获取总字节大小), ascii_only?(判断是否只含有ASCII字符)
[pre lang="ruby"]
#bytesize用于获取一个字符串的总字节大小。什么是字节大小?比如ASCII码占1个字节,UTF-8则占3个字节。
#你不知道ASCII和UTF-8的意思?百度搜索是你的好伙伴。
a = "abcdefgh"
b = "66RPG梦想世界"
p a.bytesize #输出:8
p b.bytesize #输出:17 (5个英文字母/数字,各1字节 + 4个中文字,各3字节)

#ascii_only?用于判断字符串内是否只有ASCII字符(英文字母,数字,一些半角符号等等)
a = "abcdefgh"
b = "66RPG梦想世界"
p a.ascii_only? #输出:true
p b.ascii_only? #输出:false
[/pre]

11. index(获取位置), rindex(反向获取位置)
[pre lang="ruby"]
#递入一个字符序列,尝试获取它在字符串内第一个匹配的位置,如果找不到则返回nil
#第二个参数指定搜索停止的位置。
#rindex则是返回最后一个匹配的位置
str = "1a2b3c4d3c2b1a"
p str.index("3c") #输出:4
p str.rindex("3c") #输出:8
p str.index("4d",-3) #输出:nil

#index和rindex也可以接受正则表达式作为参数来尝试匹配
#关于正则表达式,请参考F1汉化帮助文档的附录
[/pre]

12. each_byte(每个字节), each_char(每个字符), each_codepoint(每个码位), each_line(每行)
[pre lang="ruby"]
#each_byte, each_char, each_codepoint和each_line都是字符串自带的迭代方法
#each_byte迭代的内容是字节,这一点可以参考bytesize方法的解释
str = "abcd"
str.each_byte{|b| p b}
#输出:
97
98
99
100

#each_char字面上的意思,就是对每一个字符进行迭代
str = "abcd"
str.each_char{|c| p c}
#输出:
"a"
"b"
"c"
"d"

#each_codepoint是对非ASCII编码的字符用的(比如中文字),对每个字符的编码位置进行迭代
#不知道编码和码位而想要知道更多信息的话,建议去百度搜索科普一下
str = "6R梦想世界"
str.each_codepoint{|c| p c}
#输出:
54
82
26790                                                                                                                                                                    
24819                                                                                                                                                                    
19990                                                                                                                                                                    
30028

#each_line是对行数进行迭代,可接受一个参数,定义分隔符
str = "第一行\n第二行\n第三行"
str.each_line("\n"){|s| p s}
输出:
"第一行\n"
"第二行\n"
"第三行"
#注意:使用p显示字符串时,转义符(比如\n)也会被显示出来。puts则不会。
#在这里是想要表示,分隔符会仍然存在迭代的变量里面,不会被移除。
[/pre]

13. center(置中), ljust(置左,left adjust的缩写), rjust(置右,right adjust的缩写)
[pre lang="ruby"]
#center, ljust和rjust三个方法都是用于增加字符串的长度,并返回新的字符串。
#如果字符串比指定的长度还长,那么将直接返回原本的字符串。否则,填入指定的填充字符来进行增长。
#格式:方法名(目标长度,填充字符序列)
str = "hello"
p str.center(20," ") #输出:"       hello        "  
p str.ljust(20," ") #输出:"hello               "
p str.rjust(20," ") #输出:"               hello"   

str = "999"
p str.center(10,"0") #输出:"0009990000"
p str.ljust(10,"0") #输出:"9990000000"
p str.rjust(10,"0") #输出:"0000000999"  
[/pre]

14. strip(移除空白), lstrip(移除前面空白), rstrip(移除后面空白)
注:后缀!即成毁灭性操作
[pre lang="ruby"]
#strip, lstrip, rstrip都用于移除字符串内的空白,作为新的字符串返回
#空白指空格(" "),tab("\t"),换行符("\n")和返回符("\r")
str = "     abcde fghij       "
p str.strip #输出:"abcde fghij"
p str.lstrip #输出:"abcde fghij       "
p str.rstrip #输出:"     abcde fghij"
#以上三种方法后缀!则成为毁灭性操作,会更改原本的字符串
[/pre]

15. reverse(反转), reverse!(毁灭性反转)
[pre lang="ruby"]
#获取一个新字符串,字符的顺序被反转
str = "abcdefgh"
p str.reverse #输出:"hgfedcba"

#后缀!的方法是毁灭性操作,也就是会影响原本的字符串
str = "abcdefgh"
str.reverse!
p str #输出:"hgfedcba"
[/pre]

16. capitalize(首字母大写,其余小写), capitalize!(毁灭性操作)
[pre lang="ruby"]
#获取一个新字符串,首字母大写而其他的字母小写
str = "rpgmaker"
str1 = "rPgMAkER"
p str.capitalize #输出:"Rpgmaker"
p str1.capitalize #输出:"Rpgmaker"

#后缀!的方法是毁灭性操作,也就是会影响原本的字符串
str = "rpgmaker"
str.capitalize!
p str #输出:"Rpgmaker"
[/pre]

17. upcase(全部字母大写), downcase(全部字母小写), swapcase(字母大小写互换)注:后缀!即成毁灭性操作
[pre lang="ruby"]
#upcase用于获取一个新字符串,全部字母都大写。
str = "rpgmaker"
str1 = "rPgMAkER"
p str.upcase #输出:"RPGMAKER"
p str1.upcase #输出:"RPGMAKER"

#downcase用于获取一个新字符串,全部字母都小写。
str = "RPGMAKER"
str1 = "rPgMAkER"
p str.downcase #输出:"rpgmaker"
p str1.downcase #输出:"rpgmaker"

#swapcase用于获取一个新字符串,字母大小写都互换
str = "rPgMAkER"
p str.swapcase #输出:"RpGmaKer"

#以上三个方法后缀!后(upcase!, downcase!, swapcase!)都会影响原本的字符串
[/pre]

18. split(分隔)
[pre lang="ruby"]
#用于生成一个数组,内含经特定分隔符或正则表达式分隔而成的部分。
#第二个参数指定范围,也就是分隔的限制
#格式为:split(分割符或正则表达式, 范围)
str = "66rpg rpgmaker vx ace"
p str.split(" ") #输出:["66rpg", "rpgmaker", "vx", "ace"]
p str.split("r") #输出:["66", "pg ", "pgmake", " vx ace"]
str = "66rpg"
p str.split("") #输出:["6", "6", "r", "p", "g"]
p str.split("",2) #输出:["6", "6", "rpg"]
[/pre]

19. scan(扫描)
[pre lang="ruby"]
#使用正则表达式扫描字符串,并将所有匹配的字符作为数组而返回
#正则表达式的创建法和格式请参考F1汉化版帮助文档的附录
str = "66rpg rpgmaker vx ace"
p str.scan(/\w+/) #输出:["66rpg", "rpgmaker", "vx", "ace"]
p str.scan(/.../) #输出:["66r", "pg ", "rpg", "mak", "er ", "vx ", "ace"]

#scan的后面可以附上代码块,直接执行迭代(与str.scan.each相似)
str = "66rpg rpgmaker vx ace"
str.scan(/\w+/){|s| p s}
#输出:
"66rpg"                                                                                                                                                                  
"rpgmaker"                                                                                                                                                               
"vx"                                                                                                                                                                     
"ace"
[/pre]

20. slice(分割), slice!(毁灭性分割)
[pre lang="ruby"]
#slice和[]元素引用的功能相似
str = "abcdefgh"
p str.slice(5) #输出:"f"
p str.slice(-1) #输出:"h"
p str.slice(1,2) #输出:"bc"
p str.slice(0..3) #输出:"abcd"

#slice!是毁灭性的,也就是取出的字符序列会从字符串里删除
str = "rpgmaker"
p str.slice!(0,3) #输出:"rpg"
p str #输出:"maker"
p str.slice!(-1) #输出:"r"
p str #输出:"make"

#slice和slice!都能递入正则表达式作为参数,在此就不多做讲解了。
[/pre]

21. chop(移除后方字符), chop!(毁灭性移除后方字符)
[pre lang="ruby"]
#移除字符串最后面的字符后,返回一个新字符串
str = "abcdef"
p str.chop #输出:"abcde"

#chop!是毁灭性的操作,会影响原字符串
str = "abcdef"
str.chop!
p str #输出:"abcde"
[/pre]

22. chomp(移除后方指定字符), chomp!(毁灭性移除后方指定字符)
[pre lang="ruby"]
#移除字符串后方指定的字符后,返回一个新字符串
#如果没有递入参数,chomp只会对返回符(/r),换行符(/n),和返回+换行符(/r/n)组合下手
#如果找不到可以移除的字符,那么chomp会原封不动地返回本来的字符串
p "abcdef\n".chomp #输出:"abcdef"
p "abcdef".chomp("ef") #输出:"abcd"

#chomp!是毁灭性的操作,会影响原字符串
str = "abcdef\r\n"
str.chomp!
p str #输出:"abcdef"
[/pre]

23. sub(单次替换内容, substitute的缩写), gsub(全局替换内容,global substitute的缩写)注:后缀!即成毁灭性操作
[pre lang="ruby"]
#sub和gsub用于寻找匹配并替换内容,然后返回新的字符串
#sub和gsub的分别就是sub替换了一次之后就会停止,而gsub会处理并替换所有匹配的字符序列
#递入的查找参数可以是字符串,也可以是正则表达式(请参考F1汉化帮助文档的附录)

str = "RMXP RMXP RMXP"
p str.sub("XP", "VA") #输出:"RMVA RMXP RMXP"
p str.gsub("XP", "VA") #输出:"RMVA RMVA RMVA"

str = "hello there"
p str.sub(/[aeiou]/, "*") #输出:"h*llo there"
p str.gsub(/[aeiou]/, "*") #输出:"h*ll* th*r*"

#后缀!的sub!和gsub!是毁灭性操作,也就是会对原本的字符串作改动
[/pre]

H - 数组内置方法

1. empty?(判断是否为空)
[pre lang="ruby"]
#变量赋值
a = []
b = [5, 6, 7, 8]
#判断数组是否是空着的
p a.empty? #输出:true
p b.empty? #输出:false
[/pre]

2. include?(判断是否包含),any?(判断是否存在),count(判断数量),nitems(非空值的元素数量)
[pre lang="ruby"]
#include?用于判断数组内是否包含某个元素
p [1,2,3].include?(5) #输出:false
p [1,2,3].include?(2) #输出:true

#any?如果直接调用,是用于判断数组内是否有非空值的元素
p [nil, nil, nil].any? #输出:false
p [1, 2, nil].any? #输出:true

#any?可以接受代码块(一个局部变量,代表元素)来进行迭代,如果有任何一个判断通过则返回true。
p [1,2,3,4,5].any?{|i| i < 0} #输出:false
p [1,2,3,4,5].any?{|i| i >= 3} #输出:true

#count直接调用的时候,和size和length一样,是获取数组内的元素数量
p [1, 2, 3].count #输出:3
p [4, 5, 6, 7, 8].count #输出:5

#count可以接受代码块(一个局部变量,代表元素)来进行迭代,并返回通过判断的元素的数量
p [1,2,3,4,5].count{|i| i < 0} #输出:0
p [1,2,3,4,5].count{|i| i >= 3} #输出:3

#nitems用于获取数组内非空值的元素数量
p [nil, nil, nil].nitems #输出:0
p [1, 2, nil].nitems #输出:2
[/pre]

3. push(推入), pop(拉出), shift(首位移出), unshift(首位移入), clear(清空)
[pre lang="ruby"]
#push用于在数组的尾端加入新元素(返回加入新元素后的数组)
arr = [1, 2, 3]
arr.push(7)
p arr #输出:[1, 2, 3, 7]

#pop用于移除并获取数组尾端的元素
arr = [1, 2, 3, 4]
p arr.pop #输出:4
p arr #输出:[1, 2, 3]

#shift用于移除并获取数组最前端的元素
arr = [1, 2, 3, 4]
p arr.shift #输出:1
p arr #输出:[2, 3, 4]

#unshift用于在数组的最前端加入新元素(返回加入新元素后的数组)
arr = [1, 2, 3]
arr.unshift(7)
p arr #输出:[7, 1, 2, 3]

#clear用于清空数组内的所有元素
arr = [1, 2, 3, 4]
arr.clear
p arr #输出:[]
[/pre]
以上几个方法可以用来实现"队列"(queue)和"栈"(stack)。如果不知道什么是队列和栈,可以去百度搜索科普一下。
push用于“进栈”或者“进列”的操作。pop用于“出栈”的操作,而shift用于“出列”的操作。clear当然是清除栈或者队列了。
相反方向的话,unshift用于进栈/进列, shift用于出栈,而pop用于出列。
[pre lang="ruby"]
#如果看不懂这是在干啥的话,我觉得…嗯大概你还没去科普吧…
#栈的演示(使用push和pop)
stack = []
p stack.push(1) #输出:[1]
p stack.push(2) #输出:[1, 2]
p stack.push(3) #输出:[1, 2, 3]
p stack.pop #输出:3
p stack.push(4) #输出:[1, 2, 4]
p stack.push(5) #输出:[1, 2, 4, 5]
p stack.pop #输出:5
p stack.pop #输出:4
p stack.pop #输出:2
p stack.push(6) #输出:[1, 6]
p stack.clear #输出:[]

#队列的演示(使用push和shift)
queue = []
p queue.push(1) #输出:[1]
p queue.push(2) #输出:[1, 2]
p queue.push(3) #输出:[1, 2, 3]
p queue.shift #输出:1
p queue.push(4) #输出:[2, 3, 4]
p queue.push(5) #输出:[2, 3, 4, 5]
p queue.shift #输出:2
p queue.shift #输出:3
p queue.shift #输出:4
p queue.push(6) #输出:[5, 6]
p queue.clear #输出:[]
[/pre]

4. first(第一个), last(最后一个)
[pre lang="ruby"]
#变量赋值
arr = ["a","b","c","d","e"]
#获取数组里的第一个元素
p arr.first #输出:"a"
#获取数组里的最后一个元素
p arr.last #输出:"e"
[/pre]

5. insert(注入), delete(删除), delete_at(从指定位置删除)
[pre lang="ruby"]
#insert用于往数组的指定位置注入元素(位置可以是负数)
#此方法返回成功注入的数值。如果注入失败会返回nil。
#格式:insert(位置,元素)
arr = [1,2,3,4,5]
arr.insert(3,6)
p arr #输出:[1, 2, 3, 6, 4, 5]

#delete用于删除数组里的指定元素
#如果有重复的元素,全部都会被删除
#此方法返回成功被删除的数值。如果删除失败会返回nil。
arr = [1,3,3,4,5,6,7]
arr.delete(3)
p arr #输出:[1,4,5,6,7]
arr.delete(5)
p arr #输出:[1,4,6,7]

#delete_at用于删除数组里指定位置的元素(位置可以是负数)
#此方法返回成功被删除的数值。如果删除失败会返回nil。
arr = [1, 2, 3, 4, 5, 6, 7]
arr.delete_at(3)
p arr #输出:[1, 2, 3, 5, 6, 7]
[/pre]

6. shuffle(随机洗牌), shuffle!(毁灭性随机洗牌), sample(随机取样)
[pre lang="ruby"]
#shuffle用于获取随机排序(洗牌)后的新数组
arr = [1,2,3,4,5,6,7,8]
p arr.shuffle #输出:[3, 5, 1, 6, 4, 7, 2, 8] (随机)

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
arr = [1,2,3,4,5,6,7,8]
arr.shuffle!
p arr #输出:[8, 7, 5, 1, 2, 4, 6, 3] (随机)

#sample用于获取1个(返回整数)或多个(不重复,最多到数组的长度)随机元素
arr = [1,2,3,4,5,6,7,8]
p arr.sample #输出:6 (随机)
p arr.sample(3) #输出:[7, 8, 5] (随机)
p arr.sample(20) #输出:[1, 3, 5, 4, 7, 2, 6, 8] (随机,最多到arr的长度罢了)
[/pre]

7. index(获取索引值),rindex(反向获取索引值)
[pre lang="ruby"]
#递入一个参数,尝试获取它在数组内第一个匹配元素的索引值,如果找不到则返回nil
#rindex则是尝试获取数组内最后一个匹配的索引值
arr = [2,3,4,5,7,3,4,5]
p arr.index(4) #输出:2
p arr.rindex(4) #输出:6
p arr.index(9) #输出:nil
p arr.rindex(9) #输出:nil

#这两个方法也可以接受代码块(一个局部变量,代表元素)来迭代并返回第一个通过判断的元素的索引值
#index是从第一个元素开始迭代,而rindex是从最后一个元素开始迭代
arr = [1,6,9,3,5,2,1,4]
p arr.index{|i| i >= 5} #输出:1
p arr.rindex{|i| i >= 5} #输出:4
[/pre]

8. values_at(元素提取)
[pre lang="ruby"]
#变量赋值
arr = [1,3,5,7,9,11,13,15]
#元素提取递入的参数最少一个,最多无限
#提取的元素会作为新的数组而返回
p arr.values_at(3) #输出:[7]  (与p [arr[3]] 相同)
p arr.values_at(0, 4, 6, 7) #输出:[1, 9, 13, 15] (与p [arr[0], arr[4], arr[6], arr[7]] 相同)
p arr.values_at(-1, 5, -5) #输出:[15, 11, 7] (与p [arr[-1], arr[5], att[-5]] 相同)
[/pre]

9. select(选择), select!(毁灭性选择), keep_if(条件保留)
[pre lang="ruby"]
#使用select时递入代码块,接受元素为局部变量
#所有通过判断的元素将会作为新的数组返回
arr = [1,2,3,4,5,6,7,8,9]
arr1 = arr.select do |i|
        i < 5 || i == 8
end
p arr1 #输出:[1,2,3,4,8]
p arr #输出:[1,2,3,4,5,6,7,8,9](arr不受影响)

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
#select!和keep_if的功能是一样的,只是名字不同
arr = [1,2,3,4,5,6,7,8,9]
arr1 = arr.select! do |i|
        i < 5 || i == 8
end
p arr1 #输出:[1,2,3,4,8]       
p arr #输出:[1,2,3,4,8](arr已被更改)
[/pre]

10. reject(反选择), reject!(毁灭性反选择), delete_if(条件删除)
[pre lang="ruby"]
#使用reject时递入代码块,接受元素为局部变量
#所有通过判断的元素将会作为新的数组返回
arr = [1,2,3,4,5,6,7,8,9]
arr1 = arr.reject do |i|
        i < 5 || i == 8
end
p arr1 #输出:[5,6,7,9]
p arr #输出:[1,2,3,4,5,6,7,8,9](arr不受影响)

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
#reject!和delete_if的功能是一样的,只是名字不同
arr = [1,2,3,4,5,6,7,8,9]
arr1 = arr.reject! do |i|
        i < 5 || i == 8
end
p arr1 #输出:[5, 6, 7, 9]
p arr #输出:[5, 6, 7, 9]        (arr已被更改)
[/pre]

11. reverse(反转顺序), reverse!(毁灭性反转顺序)
[pre lang="ruby"]
#反转数组内元素的顺序,作为新的数组返回
arr = [1,2,3,4,5,6,7,8]
p arr.reverse #输出:[8, 7, 6, 5, 4, 3, 2, 1]

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
arr = [1,2,3,4,5,6,7,8]
arr.reverse!
p arr #输出:[8, 7, 6, 5, 4, 3, 2, 1]
[/pre]

12. uniq(移除重复元素,unique的缩写), uniq!(毁灭性移除重复元素)
[pre lang="ruby"]
#移除数组内的重复元素(每项只留一个),作为新的数组返回
arr = [1,2,2,2,3,3,3,3,5,6,9,9,0]
p arr.uniq #输出:[1, 2, 3, 5, 6, 9, 0]

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
arr = [1,2,2,2,3,3,3,3,5,6,9,9,0]
arr.uniq!
p arr #输出:[1, 2, 3, 5, 6, 9, 0]
[/pre]

13. sort(排序), sort!(毁灭性排序)
[pre lang="ruby"]
#对数组内的元素做出排序,作为新的数组返回
arr = [6,7,4,5,2,3,9,6,7,3,1]
p arr.sort #输出:[1, 2, 3, 3, 4, 5, 6, 6, 7, 7, 9]

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
arr = [6,7,4,5,2,3,9,6,7,3,1]
arr.sort!
p arr #输出:[1, 2, 3, 3, 4, 5, 6, 6, 7, 7, 9]
[/pre]

14. slice(分割), slice!(毁灭性分割)
[pre lang="ruby"]
#slice和[]元素引用的功能相似
arr = [1,3,5,7,9,11,13,15]
p arr.slice(5) #输出:11 (与arr[5]相同)
p arr.slice(-2) #输出:13(与arr[-2]相同)
p arr.slice(3,2) #输出:[3, 5, 7](与arr[3,2]相同)

#slice!是毁灭性的,也就是取出的数值会从数组里删除
arr = [1,3,5,7,9,11,13,15]
p arr.slice!(5) #输出:11
p arr #输出:[1, 3, 5, 7, 9, 13, 15] (可以看到11已经被删除了)
p arr.slice!(2..5) #输出:[5, 7, 9, 13]
p arr #输出:[1, 3, 15]
[/pre]

15. compact(移除空值), compact!(毁灭性移除空值)
[pre lang="ruby"]
#移除所有在数组里的空值(nil),作为新的数组返回
arr = [1,7,nil,3,nil,nil,5,8,9]
p arr.compact #输出:[1, 7, 3, 5, 8, 9]

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
arr = [1,7,nil,3,nil,nil,5,8,9]
arr.compact!
p arr #输出:[1, 7, 3, 5, 8, 9]
[/pre]

16. rotate(旋转), rotate!(毁灭性旋转)
[pre lang="ruby"]
#按照递入的参数而执行向左(正数)或向右(负数)的“整体移动”,作为新的数组返回
arr = [1, 2, 3, 4, 5, 6, 7, 8]
p arr.rotate(2) #输出:[3, 4, 5, 6, 7, 8, 1, 2] (向左整体移动2步)
p arr.rotate(4) #输出:[5, 6, 7, 8, 1, 2, 3, 4] (向左整体移动4步)
p arr.rotate(-1) #输出:[8, 1, 2, 3, 4, 5, 6, 7] (向右整体移动1步)
p arr.rotate(-3) #输出:[6, 7, 8, 1, 2, 3, 4, 5] (向右整体移动3步)

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
arr = [1, 2, 3, 4, 5, 6, 7, 8]
arr.rotate!(2)
p arr #输出:[3, 4, 5, 6, 7, 8, 1, 2]
[/pre]

17. collect/map(集合), collect!/map!(毁灭性集合)
[pre lang="ruby"]
#迭代数组里面的每个元素,把计算的结果整合并作为新的数组返回
#collect和map的功能是一样的,只是名字不同
arr = [1, 2, 3, 4, 5, 6, 7, 8]
arr1 = arr.collect do |i|
        i *= 2
end
p arr1 #输出:[2, 4, 6, 8, 10, 12, 14, 16]

#后缀!的方法是毁灭性操作,也就是会影响原本的数组
arr = [1, 2, 3, 4, 5, 6, 7, 8]
arr.collect! do |i|
        i *= 2
end
p arr #输出:[2, 4, 6, 8, 10, 12, 14, 16]
[/pre]

18. inject(集合迭代计算)
[pre lang="ruby"]
#迭代数组里面的每一个元素,对一个变量做出计算
#接受一个参数和一个代码块。参数是变量的初始值,代码块接受2个局部变量(变量,元素)。
arr = [1, 2, 3, 4, 5, 6, 7, 8]
a = arr.inject(5) do |v,i| #v是变量,i是元素
        v += i
end       
p a #输出:41 (注:5 + (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8) = 41)
[/pre]

I - 哈希表内置方法

1. empty?(判断是否为空)
[pre lang="ruby"]
#变量赋值
a = {}
b = {1 => 2, 3 => 4}
#判断哈希表是否是空着的
p a.empty? #输出:true
p b.empty? #输出:false
[/pre]

2. default(默认值), default=(设置默认值)
默认值在“哈希表”的部分提及过。如果使用[]尝试取出值时找不到键的话,会返回默认值。
[pre lang="ruby"]
#变量赋值
hash = {1 => 2, 3 => 4, 5 => 6}
#查看当前默认值
p hash.default #输出:nil
p hash[3] #输出:4
p hash[7] #输出:nil
#设置默认值
hash.default = 666
p hash.default #输出:666
p hash[3] #输出:4
p hash[7] #输出:666
[/pre]

3. include? / has_key?(判断是否存在键), has_value?(判断是否存在值)
[pre lang="ruby"]
#变量赋值
hash = {1 => "a", 3 => "b", 5 => "c"}
#判断是否存在键
p hash.include?(3) #输出:true
p hash.has_key?(3) #输出:true
p hash.include?(4) #输出:false
p hash.has_key?(4) #输出:false
#判断是否存在值
p hash.has_value?("b") #输出:true
p hash.has_value?("x") #输出:false
[/pre]

4. keys(获取所有键), values(获取所有值), flatten(铺平成数组)
[pre lang="ruby"]
#变量赋值
hash = {1 => "a", 3 => "b", 5 => "c"}
#获取装有所有键的数组
p hash.keys #输出:[1, 3, 5]
#获取装有所有值的数组
p hash.values #输出:["a", "b", "c"]
#将哈希表铺平成数组(这和to_a不一样,铺平后是一维数组而不是二维)
p hash.flatten #输出:[1, "a", 3, "b", 5, "c"]
[/pre]

5. length(长度), size(大小)
length和size都是获取哈希表内键值配对的数量
[pre lang="ruby"]
#变量赋值
hash = {}
hash1 = {"a" => "c"}
hash2 = {1 => 2, 2 => 3, 3 => 4, 5 => 7}
#获取键值配对的数量
p hash.length #输出:0
p hash.size #输出:0
p hash1.length #输出:1
p hash1.size #输出:1
p hash2.length #输出:4
p hash2.size #输出:4
[/pre]

6. invert(反转键与值)
[pre lang="ruby"]
#变量赋值
hash = {1 => "a", 3 => "b", 5 => "c"}
#获取一个新的哈希表,键与值是反过来了的
p hash.invert #输出:{"a" => 1, "b" => 3, "c" => 5}
[/pre]

7. clear(清空),delete(删除)
[pre lang="ruby"]
#清空哈希表
hash = {1 => 2, 3 => 4, 5 => 6}
hash.clear
p hash #输出:{}
#根据递入的键删除哈希表内的键值配对,会返回删除的值
#如果找不到键,就会返回nil,而哈希表不会受影响
hash = {1 => "z", 3 => "x", 5 => "c"}
p hash.delete(7) #输出:nil (没有找到该键值配对)
p hash.delete(3) #输出:"x"
p hash #输出:{1 => "z", 5 => "c"} (3 => "x"这个配对已经被删除了)
[/pre]

8. merge(合并), merge!(毁灭性合并)
[pre lang="ruby"]
#哈希表没有如数组般的加法运算,而是需要调用merge方法来进行合并
#递入的哈希表的键值配对会覆盖原本哈希表已有的键值配对
#合并的结果将作为新的哈希表而返回
hash = {1 => "a", 3 => "b", 5 => "c"}
hash1 = {1 => "x", 2 => "y", 3 => "z"}
p hash.merge(hash1) #输出:{1=>"x", 3=>"z", 5=>"c", 2=>"y"}
p hash #输出:{1 => "a", 3 => "b", 5 => "c"}(hash不受影响)

#后缀!的方法是毁灭性操作,也就是会影响原本的哈希表
hash = {1 => "a", 3 => "b", 5 => "c"}
hash1 = {1 => "x", 2 => "y", 3 => "z"}
p hash.merge!(hash1) #输出:{1=>"x", 3=>"z", 5=>"c", 2=>"y"}
p hash #输出:{1=>"x", 3=>"z", 5=>"c", 2=>"y"} (hash已被更改)
[/pre]

9. select(选择), select!(毁灭性选择), keep_if(条件保留)
[pre lang="ruby"]
#使用select时递入代码块,接受键和值两个局部变量
#所有通过判断的键值配对将会作为新的哈希表返回
hash = {1 => "a", 3 => "b", 5 => "c", 7 => "d", 9 => "e"}
hash1 = hash.select do |key, value| #key是键,value是值
        key < 5 || value == "e"
end
p hash1 #输出:{1 => "a", 3 => "b", 9 => "e"}       
p hash #输出:{1 => "a", 3 => "b", 5 => "c", 7 => "d", 9 => "e"}(hash不受影响)

#后缀!的方法是毁灭性操作,也就是会影响原本的哈希表
#select!和keep_if的功能是一样的,只是名字不同
hash = {1 => "a", 3 => "b", 5 => "c", 7 => "d", 9 => "e"}
hash1 = hash.select! do |key, value| #key是键,value是值
        key < 5 || value == "e"
end
p hash1 #输出:{1 => "a", 3 => "b", 9 => "e"}       
p hash #输出:{1 => "a", 3 => "b", 9 => "e"}(hash已被更改)
[/pre]

10. reject(反选择), reject!(毁灭性反选择), delete_if(条件删除)
[pre lang="ruby"]
#使用reject时递入代码块,接受键和值两个局部变量
#所有不通过判断的键值配对将会作为新的哈希表返回
hash = {1 => "a", 3 => "b", 5 => "c", 7 => "d", 9 => "e"}
hash1 = hash.reject do |key, value| #key是键,value是值
        key < 5 || value == "e"
end
p hash1 #输出:{5 => "c", 7 => "d"}       
p hash #输出:{1 => "a", 3 => "b", 5 => "c", 7 => "d", 9 => "e"}(hash不受影响)

#后缀!的方法是毁灭性操作,也就是会影响原本的哈希表
#reject!和delete_if的功能是一样的,只是名字不同
hash = {1 => "a", 3 => "b", 5 => "c", 7 => "d", 9 => "e"}
hash1 = hash.reject! do |key, value| #key是键,value是值
        key < 5 || value == "e"
end
p hash1 #输出:{5 => "c", 7 => "d"}       
p hash #输出:{5 => "c", 7 => "d"}(hash已被更改)
[/pre]

11. sort(转换成数组后排序)
[pre lang="ruby"]
#哈希表没有直接排序的方法,因为本来就不注重顺序
#因此要排序的话,是先转换成数组再排序的
hash = {3 => "b", 7 => "d", 1 => "a", 9 => "e", 5 => "c"}
p hash.sort #输出:[[1, "a"], [3, "b"], [5, "c"], [7, "d"], [9, "e"]]
#默认的hash.sort和hash.to_a.sort是一样的,因为是先转换成数组再排序
[/pre]

12. Hash[](基于数组创建哈希表)注:这不是调用的方法
[pre lang="ruby"]
#哈希表在使to_a或sort转换成数组后(flatten不行),或者格式相同的数组可以再次被转换成哈希表
#使用格式是Hash[数组],我们会用上面sort的例子来作为范例
arr = [[1, "a"], [3, "b"], [5, "c"], [7, "d"], [9, "e"]]
hash = Hash[arr]
p hash #输出:{1 => "a", 3 => "b", 5 => "c", 7 => "d", 9 => "e"}

#所以将sort和Hash[]混合使用后,得出哈希表真正的排序语句
hash = {3 => "b", 7 => "d", 1 => "a", 9 => "e", 5 => "c"}
hash1 = Hash[hash.sort]
p hash1 #输出:{1 => "a", 3 => "b", 5 => "c", 7 => "d", 9 => "e"}
[/pre]
   


第二章 - Ruby面向对象编程


2.1 面向对象

展开折叠

面向对象编程(Object-oriented programming, 缩写为OOP)是一种具有对象(Object)概念的编程范型(模范,方法),同时也是一种程序开发的方法。
它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。对象里的程序可以访问及经常修改对象相关连的数据。
在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。
(参考:维基百科)

什么是对象?
打开脚本编辑器,我们可以看到大部分默认脚本里都有一个关键字:class。
    111.png

这个关键词用于定义一个类,也就是数据类型的蓝图。
对象,就是类的实例,是基于数据类型的蓝图所创造出来的一个“物件”。
它拥有自己的变量,拥有自己的方法,也可以与其他对象交互。

上面的附图内显示的,是Game_Temp这个类的定义(详细信息请看绿字注释)。
它的实例在创建后将被储存在$game_temp这个变量里面。$是什么意思呢?这个我们之后会提到。

在Ruby里面,一切都是对象。一切数据,不管是整数、字符串还是数组,都是基于类而创建的,更不用说自定义的数据类型。
类本身也是对象。这一个特性我们会在之后关于“模块”的部分进一步探讨。

如果到这儿还是无法明白的话,请看浓缩版本的吧:
类(Class),是数据类型的蓝图,定义着方法和会用到的变量。
对象(Object),是基于类而创造出来的实例(Instance,实际例子),有着属于自己的一套方法和变量。

   




2.2 类与实例

展开折叠

重要的事情要说三次!所以,再说一次!类是数据类型的蓝图!对象是基于类而创造出来的实例!

在定义一个类的时候,我们需要用class关键词。格式如下:
[pre lang="ruby"]
class 类的名字
    (内容)
end
[/pre]
类的内容可以是调用方法(在游戏开始时会进行处理),设置变量/常量,或者是定义方法。
在完成定义后,这个蓝图会作为一个对象被存在与类的名字相同的常量里。(这就是为啥之前说过,类也是对象)
那么,我们可以调用它的new方法来创建实例。嗯…new…这个方法…好像哪儿见过,对吧?
没错,Array.new就是手动创建一个数组(Array)的实例而Hash.new是手动创建一个哈希表(Hash)的实例。
让我们来定义一个空的类,并尝试创造它的实例吧。而且不止一个,要三个!
[pre lang="ruby"]
#定义类
class AAA
    #暂无内容
end

#创建实例
a = AAA.new
b = AAA.new
c = AAA.new

#显示数据
p a
p b
p c
[/pre]
好,那么在RMVA里面运行以上列出的代码之后,我们会看到控制台上显示着这么样的玩意儿:(注:后边的十六进制数是随机的)
    333.png
这…是啥?AAA是类的名字没错,但是后面的十六进制数是什么东西?为什么显示的三个变量,那串十六进制数都不一样?
这是它的对象ID。就算是基于同个类所创造的,不同的实例,对象ID是不一样的。换句话说,相同的实例,对象ID是一样的。
就像身份证号。同样都是人类,你的身份证号和他的身份证号是不同的,代表着是不同的两个人。
那么我们可以总结,以上的代码基于一个类,生成了三个不同的实例。如何证明它们的不同?看对象ID就知道了。

接下来,我们来尝试运行这段代码。
[pre lang="ruby"]
#定义类
class AAA
    #暂无内容
end

#创建实例
a = AAA.new
b = a
c = b

#显示数据
p a
p b
p c
[/pre]
输出:
    444.png

嗯?三个都一样?那就是说… a, b和c三个变量所存着的,是同一个对象?
没错。在执行 a = AAA.new 这个语句的时候, 我们基于AAA类创建了一个实例,然后把它的引用(reference)赋值给变量a。
因此,在执行b = a,c = b这两句的时候,是基于这个引用来赋值的。好,那么问题来了。什么叫引用?
在Ruby语言里面,我们可以这么形容:假设一个电脑机房内的每个电脑都有自己的密码,只有知道密码的人可以使用它。
这个情况下,如果把创建实例比喻为往这个机房里增添一台新电脑(类比喻为电脑的制造蓝图而它的实例就是那台电脑),那么引用相当于这台电脑的"密码"。

我们把对象的引用存到一个变量里后,就可以使用句号(.)来调用它的方法。这个我们在介绍"方法定义与调用"(章节1.4)时曾经提到过。
我们也可以将引用赋值给别的变量,甚至是作为参数来传递给别的方法。总而言之,有了引用,我们才能与一个对象做出互动。
如果引用没了呢?这就是另一个话题了。当一个对象被孤立(isolated)(整个程序的所有变量都没有它的引用),它就会被视为“垃圾”(garbage)。
Ruby自带的垃圾回收(garbage collection)系统会不定时地清理这种对象,以腾出内存空间,避免系统资源的浪费。
关于“垃圾回收”,我们之后会进一步地讨论。
   




2.3 实例方法与变量

展开折叠

类的定义里面包括方法和变量。每一个基于这个类而创建的实例,都会有这些方法和变量。

接下来,我们要介绍的是类的initialize(初始化)方法。它是在创造对象后立即调用的方法,一个对象的一生只会调用一次。
这个方法是选择性定义的。不过大部分的情况下,这个方法是需要用上的)
[pre lang="ruby"]
class AAA
    def initialize
        p "hello!!!"
    end
end

a = AAA.new
#输出:
#"hello!!!"
[/pre]
我们也可以让initialize方法接受参数。这些参数在使用new方法创建对象的时候递入。
[pre lang="ruby"]
class AAA
    def initialize(a,b)
        p a + " " + b
    end
end

a = AAA.new("66RPG","RPGMaker")
#输出:
#"66RPG RPGMaker"
[/pre]
initialize方法无法手动调用。尝试调用时会报错。

我们可以在类里面定义实例方法(instance method),也就是类的实例可以使用的方法。
[pre lang="ruby"]
class AAA
    def initialize
        p "初始化"
    end

    def add(a,b)
        p a.to_s + " + " + b.to_s + " = " + (a+b).to_s
    end

    def sub(a,b)
        p a.to_s + " - " + b.to_s + " = " + (a-b).to_s
    end
end

a = AAA.new
a.add(5,6)
a.add(7,8)
a.sub(10,3)
a.sub(6,1)

#输出:
#"初始化"
#"5 + 6 = 11"
#"7 + 8 = 15"
#"10 - 3 = 7"
#"6 - 1 = 5"
[/pre]

当然在实例里面,方法是可以互相调用的(不过注意别搞出死循环来啊!就是a调用b,b调用a,然后就没有然后了)。
[pre lang="ruby"]
class AAA
    def abc
        cde
    end

    def cde
        p "hello!"
    end
end

a = AAA.new
a.abc #输出:"hello!"
[/pre]

我们在读默认脚本的时候,常见到一个关键词:self(自身)。就如这个英文单词的意思,它的用途是获取这个对象自身的引用。
self可以用于修饰类里面的,对于自身实例方法的调用(更容易辨认)。
[pre lang="ruby"]
class AAA
    def abc
        self.cde #前面有没有加self都没差的,加了只是为了好看/更容易辨认
    end

    def cde
        p "hello!"
    end
end
a = AAA.new
a.abc #输出:"hello!"
[/pre]
使用self获取的引用当然可以给变量赋值,还能作为参数递给其他方法。这我们就不再提及了。
self的另一个使用法就是作为返回数据以达成调用链的功效。这是艺术啊!
[pre lang="ruby"]
class AAA
    def add(a,b)
        p a.to_s + " + " + b.to_s + " = " + (a+b).to_s
        return self #返回自身的引用
    end

    def sub(a,b)
        p a.to_s + " - " + b.to_s + " = " + (a-b).to_s
        return self #返回自身的引用
    end
end
a = AAA.new
a.add(1,2).sub(4,3).add(6,7).sub(9,5)

#输出:
#"1 + 2 = 3"
#"4 - 3 = 1"
#"6 + 7 = 13"
#"9 - 5 = 4"
[/pre]
如果看不懂这条“调用链”的运作原理的话,可以看看下面的详解:
[pre lang="ruby"]
注:此代码无法运行

a.add(1,2).sub(4,3).add(6,7).sub(9,5)

相等于

b = a.add(1,2)
c = b.sub(4,3)
d = c.add(6,7)
e = d.sub(9,5)

[/pre]
实际上只要是有返回数据的方法都能如此调用。
比如我们要把一个浮点数给向下取整了之后,转换成字符串:
[pre lang="ruby"]
#普通写法
a = 3.14159
a = a.round(2)
a = a.to_s

#调用链
a = (3.14159).round(2).to_s
[/pre]
为了代码的简洁,能尽量链起来的方法调用就链起来咯。当然宽度不够时就没办法了(如1.00版RMVA残念的事件脚本指令输入窗)。



在说完方法之后,是时候说变量了。我们在RMVA的默认脚本里见过很多种变量。@开头的有,@@开头的也有,$开头的也有,没前缀的当然有。
这些前缀是什么意思呢?没有前缀又是什么意思呢?那就让我们接着看下去吧!

没有前缀的变量,叫做局部变量(local variable)。它们所存在的领域(作用域)只有在一段代码块内,不论是方法,还是递入迭代器的代码块。
在代码块执行完毕后,这个变量就会失效了。如果一个对象的所有引用都失效,那么它就会因为被“孤立”而被垃圾回收机制不定时清除。
局部变量必须先赋值才能被读取,否则将会报错。
[pre lang="ruby"]
a = 10
b += a #报错!局部变量b未赋值!

a = 10
b = 0
b += a #没问题
[/pre]

局部变量作用域范例 - 代码块:
[pre lang="ruby"]
5.times{|i| print i} #输出:01234
p i #报错!局部变量i未赋值!
[/pre]


局部变量作用域的范例 - 方法:
[pre lang="ruby"]
def abc(a)
    b = 5
    return a + b
end

abc(10)
p b #报错!局部变量b未赋值!
[/pre]

局部变量和无参数的方法调用时非常相似。因此,如果出现撞名的情况,较后定义的会生效。
[pre lang="ruby"]
def a
    return 10
end

def b
    a = 2
    return a + 5
end

p b #输出:7
[/pre]

如果没有 a = 2 这行呢?
[pre lang="ruby"]
def a
    return 10
end

def b
    return a + 5
end

p b #输出:15
[/pre]
由此可见,系统如果找不到该代码块内有能用的局部变量,就会去找能用的方法了。两个都没有?报错啊。

虽然如此,我们可以使用self(获取对象自身的引用)来指定调用方法(而不是用局部变量)。
[pre lang="ruby"]
def a
    return 10
end

def b
        a = 2
    return self.a + 5
end

p b #输出:15
[/pre]
不过最好还是尽可能避免出现实例方法和局部变量撞名的情况。

说完了局部变量,现在我们来说说实例变量(instance variable)。实例变量的前缀是@,如@hp,@map_id等等。
实例变量,就如其名,就是实例的变量。每个实例都有它自己的一套实例变量。就算是同一个名字,不同实例变量的值有可能不同。
实例变量装的可能是怪物的HP,可能是人物的经验值,就连屏幕上每一个图片的坐标,也都是存在实例变量里的。

一个类所会用到的实例变量一般在initialize(初始化)方法里面定义:
[pre lang="ruby"]
class Person #人
    def initialize(age,height,weight)
        @age = age #岁数
        @height = height #高度
        @weight = weight #重量
    end

    def show_info #显示讯息
        p "岁数: " + @age.to_s + " 高度: " + @height.to_s + " 重量: " + @weight.to_s
    end
end

a = Person.new(16,165,55)
b = Person.new(21,176,63)
a.show_info
b.show_info
p a
p b

#输出:
#岁数: 16 高度: 165 重量: 55
#岁数: 21 高度: 176 重量: 63
#<#Person:0x12345678 @age=16, @height=165, @weight=55> #注意:对象ID是瞎掰的,反正就是不同就对了
#<#Person:0x12347788 @age=21, @height=176, @weight=63>  
[/pre]
从上面的例子中我们可以看到,Person类的两个实例,虽然都有“岁数”(@age),“高度”(@height)和“重量”(@weight)三个实例变变量,但是里面装着的数值都有可能不同。
就像在游戏内,就算是同样类型的怪物,怪物A和怪物B有着分开的HP和MP条,甚至可能连攻击数值都不同(如果有套用敌人数值浮动的脚本系统)。

实例变量只能在实例里面使用。如果要从外部引用来读取或赋值的话,要通过方法才行。毕竟引用符(句号 . )只能调用方法啊…
[pre lang="ruby"]
class AAA
    def initialize
        @abc = 666
    end
end

a = AAA.new
p a.abc #报错:找不到方法"abc"
a.abc = 5 #也是会报错,找不到方法"abc="
[/pre]
从以上的例子我们可以知道,如果要对AAA类的@abc实例变量施行读取/写入操作,必须要定义方法abc和abc=才行。
当然不一定要方法名和实例变量名相同,不过为了方便使用,还是相同的比较好。
[pre lang="ruby"]
class AAA
    def initialize
        @abc = 666
    end

    def abc
        return @abc
    end

    def abc=(v) #注意:什么变量名字都行,我个人习惯是v
        @abc = v
    end
end

a = AAA.new
p a.abc #输出:666
a.abc = 233
p a.abc #输出:233
[/pre]
不过这里又出了一个问题。我有很多很多实例变量啊!难道全部都要定义一个读取和一个写入方法吗?二十个三十个,根本吃不消啊!

这可以别担心。Ruby提供了三个可以在定义类时呼叫的方法:attr_reader(属性读取,attr是attribute的缩写), attr_writer(属性写入),attr_accessor(属性读写)。
这三个方法接受无限个符号作为参数。假设实例变量是@abc,那么符号就是:abc。
[pre lang="ruby"]
class AAA
    attr_reader :a, :b #等同定义了 def a; return @a end 和 def b; return @b end
    attr_writer :c, :d  #等同定义了 def c=(v); @c = v end 和 def d=(v); @d = v end
    attr_accessor :e, :f, :g #等同读取和写入方法都定义了
    def initialize
        @a = 1
        @b = 2
        @c = 3
        @d = 4
        @e = 5
        @f = 6
        @g = 7
    end
end

v = AAA.new
p v.a #输出:1
p v.b #输出:2
p v.c #报错!没有定义读取方法!
p v.d #报错!没有定义读取方法!
p v.e #输出:5
p v.f #输出:6
p v.g #输出:7
p v.a = 9 #报错!没有定义写入方法!
p v.b = 8 #报错!没有定义写入方法!
p v.c = 7 #输出:7
p v.d = 6 #输出:6
p v.e = 5 #输出:5
p v.f = 4 #输出:4
p v.g = 3 #输出:3
[/pre]
是不是所有实例变量都要这么做呢?答案是,不,这要看情况。我们会在之后谈到“封装”的部分深入这一点。
如果要“删除”实例变量,那就给它赋值个nil(空值)就好。(如 @abc = nil)

Ruby是非常灵活的语言,但是这也表示…眼残没药医。如果我们把实例变量的名字打错了会发生什么事情呢?会报错吗?
答案是不会。如果一个实例里面的某个实例变量未赋值,那么尝试读取这个实例变量的时候,会得到nil(空值)。
因此,你搞错实例变量名字的地方不会直接报错,而是别的地方出乱子(比如找不到方法或者类型不搭而无法执行运算之类的)。
同样地,忘记初始化(反正就是读取之前必须赋值就对了)变量也会导致这种事情的发生,所以要多加小心啊。

接着,我们要介绍的是类变量(class variable)。它的前缀是@@。人家实例变量是一个@,类变量是两个@。
类变量在RMVA默认脚本里应用的地方非常少,只有Sprite_Base,Window_MenuCommand和Window_DebugLeft有用到。
类变量就是所有该类的实例共有的变量。不管从哪个实例读取这个变量,这个值都是一样的。
类变量的初始化不必,而且最好不要在方法里面进行。它的主要用途在于记忆一些需要保留给别的实例的数据。
与实例变量相似,类变量如果在未赋值的情况下尝试读取,会返回nil(空值)。

范例:类变量的初始化
[pre lang="ruby"]
class AAA
    @@aaa = 233

    def show_aaa
        @@aaa
    end
end

a = AAA.new
a.show_aaa #输出:233

b = AAA.new
a.show_aaa #输出:233
[/pre]

范例:类变量作为计数器的使用(用来生成实例的ID)
[pre lang="ruby"]
class AAA
    @@count = 0 #初始化计量
    def initialize
        @@count += 1 #计量+1
        @id = @@count #设置ID为计量
    end

    def show_id #显示ID
        p "ID = " + @id.to_s
    end
end

a = AAA.new
a.show_id #输出:ID = 1
b = AAA.new
b.show_id #输出:ID = 2
c = AAA.new
c.show_id #输出:ID = 3
[/pre]
类变量的这个用法可以记录实例创建的次序(ID越大的当然就越迟)。

范例:类变量作为数据记忆的使用:
[pre lang="ruby"]
class AAA
    @@last_choice = "无" #上一个选项
    def initialize(choice)
        p "上一次的选项:" + @@last_choice + " ,这一次的选项:" + choice
        @@last_choice = choice
    end
end

AAA.new("一号") #输出:"上一次的选项:无 ,这一次的选项:一号"
AAA.new("三号") #输出:"上一次的选项:一号 ,这一次的选项:三号"
AAA.new("五号") #输出:"上一次的选项:三号 ,这一次的选项:五号"
[/pre]

最后,就是最万恶的,以$为前缀的全局变量(global variable)。全局变量,字面上的意思,就是那儿都能用的变量。
滥用全局变量真爽啊!不,别这样做。这是因为这会让代码非常难读,而且如果搞不好的话,可能会有冲突的危机。(脑子真的清醒的话…嗯随便吧)
全局变量与实例变量和类变量相似,未赋值的情况下尝试读取会获得nil(空值)。

范例:全局变量的“威能”
[pre lang="ruby"]
class AAA
    def abc
        $xyz += 5
    end

    def cde
        $xyz = 2
    end

    def efg
        p $xyz + 3
    end
end

a = AAA.new
$xyz = 6
a.abc
p $xyz #输出:11
a.efg #输出:14
$xyz = 3
a.efg #输出:6
a.cde
p $xyz #输出:2
[/pre]

全局变量在任何地方都可以被任何对象执行读取或写入的操作。它所引用对象的实例方法也可以在任何时候,在任何地方被调用。
RMVA默认脚本里面,大部分主要游戏对象(前缀Game_)的引用都是由全局变量存着的。比如地图(Game_Map)的实例是存是在$game_map里的。
公共开关是存在$game_switches里面的,而公共变量是存在$game_variables里面的。(它们两个都类似数组,可以用[]和[]=运算符来读取/赋值)
比如呢,对6号公共变量赋值233,就是$game_variables[6] = 233。打开66号公共开关的话,$game_switches[66] = true。
不管是在脚本编辑器内甚至是事件脚本指令(包括行动路径的),都可以如此来运用公共开关和公共变量。
关于哪个游戏对象是哪个全局变量存着的详细列表,请参考本教程的附录。
   




2.4 封装、继承与多态

展开折叠

面向对象编程的三个基本特征分别是封装(Encapsulation),继承(Inheritance)和多态(Polymorphism)。
在这个部分里面,我们将陆续地认识这些特征的意思,和Ruby为了体现这些特征而准备的语句和机制。

A - 封装

封装是对象和类概念的主要特性,主要概念是“把对象的属性(实例变量)和行为(实例方法)结合成一个独立的单位,并尽可能隐蔽内部细节”。

假设我们有一个用陶瓷封装起来的集成电路芯片。既然封装起来了,里面的电路构造当然也就看不见。作为使用者的我们不需要去关心芯片内部的构造。
我们只需要知道这个芯片有多少个引脚,它的电气参数(如电压,电阻)有啥,每个引脚各自的功用,当然还有这个芯片是用来干什么的。

在面向对象编程里,封装的效果是让对象以外的部分不能随意访问该对象的实例变量和方法。
Ruby默认的情况下,除了initialize(初始化)的所有方法都是公开(public)的。也就是说,只要有这个对象的引用,就能调用它的实例方法。

Ruby提供了两个可以在定义类时呼叫的方法:private和protected。(使用法同attr_reader, attr_writer和attr_accessor)
private(私有)让指定的方法无法通过引用来调用(包括self)。如果从外部调用就会报错。initialize方法默认是这个样的。
protected(保护)和private类似,但是可以透过self获取的,自身的引用来调用这个方法。
[pre lang="ruby"]
class AAA
    private :abc #设置方法abc为私有
    protected :cde #设置方法abc为被保护

    def abc; end
    def cde; end
    def efg; end

    def test #内部测试
        abc #没问题
        cde #没问题
        self.abc #报错:私有方法无法被调用
        self.cde #没问题
    end
end

a = AAA.new
a.test #开始内部测试
a.abc #报错:私有方法无法被调用
a.cde #报错:被保护的方法无法被调用
a.efg #没问题
[/pre]

Ruby的实例变量默认是私有的(无法从外部读写)。想要“公开”实例变量,我们就得定义读取和写入的方法。
定义类时可以调用attr_accessor的方法快速完成基本读写方法的定义。这些我们在上一个部分就提到过了。

可是,在RMVA默认脚本里面没有用到private和protected啊?
的确是没用到。而且上面也提到过,这是一种概念。只要不让对象的变量和方法被随意调用,就算是达成了封装。
什么是随意调用?为什么要避免这种情况?我们直接看下面的范例。
[pre lang="ruby"]
class Square #正方形
    #定义读写方法(将实例变量公开)
    attr_accessor :side_length, :area, :perimeter

    def initialize(v)
        raise "错误!边长必须超过0!" if v <= 0
        @side_length = v #边长
        @area = v ** 2 #面积,边长的次方
        @perimeter = v * 4 #周长,边长的4倍
    end

end

square = Square.new(10) #创建一个“正方形”类的实例,初始化边长为10
p square.side_length.to_s + " " + square.area.to_s + " " + square.perimeter.to_s #输出:10 100 40

#如果我们要换个边长的话…
square.side_length = 20
square.area = 20 ** 2
square.perimeter = 20 * 4
p square.side_length.to_s + " " + square.area.to_s + " " + square.perimeter.to_s #输出:20 400 80

#好,现在我们来看乱改会发生什么事…
##啥?居然毫无防备地被乱改了?这不就出事了吗?
square.side_length = -555
square.area = 999
square.perimeter = 233
p square.side_length.to_s + " " + square.area.to_s + " " + square.perimeter.to_s #输出:-555 999 233
[/pre]
在这个“正方形”类里面,用到的实例变量全都公开出去了。不止是读,还有写。
如果别的对象从外部误用了不该调用的方法,或者动了不该被修改的变量(用attr_writer/accessor增添了写入方法)呢?
这些,我们都说不准。如果出了问题,要除错也是个费时费力的事。

那么如果用正确的封装法的话:
[pre lang="ruby"]
class Square #正方形

    #定义读取方法(唯读)
    attr_reader :side_length, :area, :perimeter

    def initialize(v)
        self.side_length = v
    end

    def side_length=(v)
        raise "错误!边长必须超过0!" if v <= 0
        @side_length = v #设置边长
        @area = v ** 2 #面积 = 边长的次方
        @perimeter = v * 4 #周长 = 边长的4倍
    end

    def area=(v)
        raise "错误!面积必须超过0!" if v <= 0
        @area = v #设置面积
        @side_length = Math.sqrt(v) #边长 = 面积的方根
        @perimeter = @side_length * 4  #周长 = 边长的4倍
    end

    def perimeter=(v)
        raise "错误!周长必须超过0!" if v <= 0
        @perimeter = v #设置周长
        @side_length = v / 4 #边长 = 周长除于4
        @area = @side_length ** 2  #面积 = 边长的次方   
    end
end

#创建实例
square = Square.new(10)
p square.side_length.to_s + " " + square.area.to_s + " " + square.perimeter.to_s #输出:10 100 40

#透过调用特制的方法来设置变量
#我们可以看到,其他的变量也跟着做调整,而且有对错误参数的处理机制
square.set_side_length(-555) #报错:错误!边长必须超过0!
square.set_side_length(40)
p square.side_length.to_s + " " + square.area.to_s + " " + square.perimeter.to_s #输出:40 1600 160
square.set_area(144)
p square.side_length.to_s + " " + square.area.to_s + " " + square.perimeter.to_s #输出:12 144 48
square.set_perimeter(32)
p square.side_length.to_s + " " + square.area.to_s + " " + square.perimeter.to_s #输出:8 64 32
[/pre]

封装不只是能够减少外部调用导致的错误。如果是方法本身出错,修改那个方法就好。或许说,我们可以更快知道出错的地方,并施于修复。
当然了,一味地强调封装反而会给写代码的过程带来负担而失去意义。因此,一切都要看实际情况而定。
一般为RMVA写插件脚本的情况下,只要搞清楚方法的调用是否会跳过某些重要的步骤而出现意想不到的结果,并尽量避免这种情况就行了。

B - 继承

继承是两个类之间的一种关系。我们可以定义一个子类(child class/subclass)来继承父类(parent class)的方法。
在Ruby里定义类时,我们可以给它指定一个超类(superclass)。格式是:class 子类名 < 父类名 。
[pre lang="ruby"]
class AAA
    attr_accessor :abc

    def initialize
        @abc = 666
    end

    def hello
        p "hello!"
    end
end

class BBB < AAA
    def cde
        p "23333! " + @abc.to_s
    end
end

a = AAA.new
p a.abc #输出:666
a.abc = 55
p a.abc #输出:55
a.hello #输出:"hello!"
a.cde #报错:找不到方法!

b = BBB.new
b a.abc #输出:666
b.abc = 55
b a.abc #输出:55
b.hello #输出:"hello!"
b.cde #输出:"23333! 55"
[/pre]
从上面的例子我们可以看到,BBB类继承了AAA类的方法,而且增添了新的方法。
(注:不提变量是因为Ruby的实例变量在第一次赋值时才当场创建的,所以继承了方法相当于继承了变量)

为什么要这么做呢?这是因为,子类(继承的类)和父类(被继承的类)有“是一种”(is a)的关系。继承是为了强调这一点。
猫和狗都是哺乳动物。消防员和警察都是公务员。四方形和三角形都是形状。武器和护甲都是装备。
我们就用猫和狗来当例子好了。
[pre lang="ruby"]
class Mammal #哺乳动物
    def breathe #呼吸
        "吸气~ 呼气~"
    end
end

class Cat < Mammal #猫
    def meow #喵喵叫
        "喵~"
    end
end

class Dog < Mammal #狗
    def bark #汪汪叫
        "汪!"
    end
end

#创建实例和调用
cat = Cat.new
cat.breathe #输出:"吸气~ 呼气~"
cat.meow #输出:"喵~"

dog = Dog.new
dog.breathe #输出:"吸气~ 呼气~"
dog.meow #输出:""汪!"
[/pre]
我们可以见到的是,Cat和Dog这两个类都拥有Mammal类的方法(在这个例子里是breathe。哺乳动物都是会呼吸的嘛)。
这两个派生(derive)出来的子类有着自己的方法。猫会喵喵叫,狗会汪汪叫。这是没问题的。
因此,继承的一个重要意义就是特化(specialize)一个类,让它进一步地表现出它的功能和特性。

从RMXP跳槽过来的RMVA新手们(假设已经到了会改脚本的阶段)可能会在这里遇到一些问题。
RMVA默认脚本大肆运用了继承(和另一个面向对象基本特征:多态),让同一段代码可以被重复使用。
什么意思呢?非常简单。如果两个(或多个)类的内容非常相似,那就把一样的地方拿出来做成超类,让这两个类继承它。
最明显的例子就是Scene_MenuBase(菜单场景的基底)和继承它的Scene_Menu(主菜单场景),Scene_Equip(装备菜单场景)等等。

附图:RMVA默认场景的继承关系
   1234.png

主菜单,装备菜单,技能菜单,道具菜单,存档菜单等等,这些场景的共同点是什么?背景是半透明的啊。
怎么做到的?将地图画面截图之后施行模糊化和调色,然后作为场景的背景贴图。(详细内容请自行参考Scene_MenuBase的方法)
当然,这些菜单场景共同的方法还有帮助窗口(如装备和物品描述)的生成,和角色的切换。

虽然如此,并不是所有重复的代码都适合用继承的概念来简化,尤其是会影响可读性和导致难以理解的时候。这得看使用的情况和前后关系(context)。
而且呢,继承最主要的用途还是强调“是一种”的关系。主菜单,装备菜单,技能菜单,道具菜单,存档菜单等等,这些都是?菜单啊。
最后,Ruby语言让一个父类可以被很多个子类继承,但是一个子类只能继承一个父类。
有兴趣的话可以去科普一下“钻石问题”或者“菱形缺陷”,得知这个限制背后的原因。

C - 多态

多态(Polymorphism)是"多种形态"的缩写。它的定义为“一种将不同的特殊行为和单个泛化记号相关联的能力”。(来源:维基百科)
简单来说,相同的讯息(调用同样名字的方法)基于不同的物件(不同类的实例)会引发不同的动作(执行内容不一样)。

最简单的多态就是不同的类有同样名字的方法,但是内容不同。
[pre lang="ruby"]
class Cat#猫
    def make_sound #发出声音
        meow
    end

    def meow #喵喵叫
        p "喵~"
    end
end

class Dog#狗
    def make_sound #发出声音
        woof
    end

    def woof #汪汪叫
        p "汪!"
    end
end

#创建实例和方法调用
cat = Cat.new
cat.make_sound #输出:"喵~"

dog = Dog.new
dog.make_sound #输出:"汪!"
[/pre]
为什么要这么麻烦呢?我们知道那是猫,那它一定是喵喵叫的啊。调用meow方法不就行了吗?
但是,如果我们不知道那个实例实际上是什么类型,或者是不需要知道呢?这时多态就派上用场了。
假设我们有数个笼子,里面可能关着猫或者狗。我们只能靠叫声来分辨它们。(注意:类的定义和上面的例子一样。)
[pre lang="ruby"]
#随机往数组内放Cat或者Dog的实例,各50%几率,一共5个
arr = Array.new(5){rand > 0.5 ? Cat.new : Dog.new}
#用each迭代器遍历数组里的元素,并调用make_sound方法
arr.each do |i|
    i.make_sound
end
#输出:(假设数组里的是[猫,狗,狗,猫,狗])
#"喵~"
#"汪!"
#"汪!"
#"喵~"
#"汪!"
[/pre]
多方便啊。我们不需要去判断到底那是Cat还是Dog的实例而去决定要调用什么方法,因为调用两者共有的方法就对了。

继承与多态有密切的联系。我们可以用Ruby语言实现方法覆盖(method overriding)。
这非常简单。只要在子类里面定义的方法与父类的重名,那么覆盖就会发生了。子类的方法将会优先被调用。
[pre lang="ruby"]
class AAA
    def abc
        p "父类的原方法"
    end
end

class BBB < AAA
    def abc
        p "子类覆盖的方法"
    end
end

#创建实例和方法调用
a = AAA.new
a.abc #输出:"父类的原方法"

b = BBB.new
b.abc #输出:"子类覆盖的方法"
[/pre]
不过很多情况下,子类方法与父类方法的区别只是增加了代码,不管是在前面,在后面,还是前后都有。
这种情况下,我们可以用super关键词来代表父类的原方法(同名方法)。
[pre lang="ruby"]
class AAA
    def abc
        p "父类的原方法"
    end
end

class BBB < AAA
    def abc
        p "子类增添的代码(前)"
        super
        p "子类增添的代码(后)"
    end
end

#创建实例和调用方法
b = BBB.new
b.abc
#输出:
"子类增添的代码(前)"
"父类的原方法"
"子类增添的代码(后)"
[/pre]
当然了,使用super算是方法调用,可以递参数给父类的原方法。(比如super(1,"a")),更可以用在表达式中(如 return 1 + super)。

super关键词毕竟意思是“调用父类的同名方法”。如果是要从别的方法或者外部调用父类的原方法呢?
Ruby提供了alias_method(方法别名)来给方法取一个别名。格式是:alias_method 方法的别名, 方法的原名 。(注意:符号和字符串都行)
[pre lang="ruby"]
class AAA
    def abc
        p "父类的原方法"
    end

    #设置别名为abc_super
    alias_method :abc_super, :abc
end

class BBB < AAA
    def abc
        p "子类覆盖的方法"
    end
end

#创建实例和调用
a = AAA.new
a.abc #输出:"父类的原方法"
a.abc_super #输出:"父类的原方法"

b = BBB.new
b.abc #输出:"子类覆盖的方法"
b.abc_super #输出:"父类的原方法"
[/pre]
要注意的是,alias_method必须在原方法定义完成后才能调用,否则会报错(找不到方法)。

最后我们来看继承和多态两个特征交杂的例子吧:
[pre lang="ruby"]
class Regular_Polygon #正多边形

    #定义实例变量的读写方法
    attr_accessor :side_length

    def initialize(v) #初始化
        @side_length = v
    end

    def sides #边的数量
        raise "错误!未定义!" #手动报错
    end

    def area #面积
        raise "错误!未定义!" #手动报错
    end

    def perimeter #周长
        return side_length * sides
    end

    def internal_angle #内角
        return 360 / sides #360度除于边的数量
    end

    def show_info #显示信息
        p "边数:#{sides} 边长:#{side_length} 面积:#{area} 周长:#{perimeter} 内角:#{internal_angle}"
    end
end

class Square < Regular_Polygon #正方形
    def sides #正方形有四个边
        return 4
    end

    def area #正方形的面积是边长的次方
        return side_length ** 2
    end
end

class Equilateral_Triangle < Regular_Polygon #等边三角形
    def sides #三角形有三个边
        return 3
    end

    def area #等边三角形的面积是 边长的次方 乘于 3的方根 再除于 4
        return (Math.sqrt(3) / 4) * (side_length ** 2)
    end
end

#创建实例调用方法
a = Equilateral_Triangle.new(5)
a.show_info #输出:"边数:3 边长:5 面积:10.825317547305483 周长:15 内角:120"

b = Square.new(7)
b.show_info #输出:"边数:4 边长:7 面积:49 周长:28 内角:90"
[/pre]
   




2.5 猴子补丁

展开折叠
一提到“猴子”,你们想到了什么呢?一种动物?论坛里的某个风云人物?
实际上,猴子补丁(Monkey Patching)是一种编程技法。定义是“在不修改源代码的情况下进行功能追加和变更。”
为什么这种技法叫“猴子补丁”呢?这有两种说法。第一种就是因为这种拼凑代码的行为就像猴子一般在乱搞(某种意义上真的像是这样)。
第二种说法是,以前这种技法叫“游击补丁”(guerilla patching),而英文单词guerilla(游击队)和gorilla(大猩猩)相似。
所以嘛,不知怎么地这技法就被叫成猴子补丁了咯… 老外的脑洞可不是一般的大啊。

我们常见到“插件脚本”(plugin script)这个名词。我自己也写过安装RMVA插件脚本的教程(看签名档!)。字面上的意思,“插”下去改改设置就得了。
那么为什么要以插件脚本的形式来修改RMVA的脚本呢?这是为了不影响默认的脚本,尽量避免因为出现错误而覆水难收。
尤其是忘记改了默认脚本的哪儿和原本这儿是长什么样子的情况,要排解问题实在是浪费时间和精力啊。

这,就是“猴子补丁”发挥力量的时候了。Ruby作为一个开放性的动态语言,支持拓展和重定义成为它的优势。
首先要说说类的拓展。我们知道,要定义一个类的时候,要用class关键词,而格式是class 类的名字; ...(类的内容)... end。
但是如果因为某些原因而导致要分开两段来写呢?这是可以的。

假设我们有一个类的定义为:
[pre lang="ruby"]
class AAA
    def abc
        p "方法abc"
    end
   
    def cde
        p "方法cde"
    end
end
[/pre]
在类的定义结束后,我们想要给这个类追加方法或者声明(如attr_accessor),但是我们又不想改动本来的类定义。要怎么办呢?
那么在Ruby语言,我们可以在它的下面追加多一次class...end,里面有追加的方法的定义和声明。
[pre lang="ruby"]
#原本的类定义
class AAA
    def abc
        p "方法abc"
    end
   
    def cde
        p "方法cde"
    end
end

#追加的类定义
class AAA
    def efg
        p "方法efg"
    end
end

#创建实例与方法调用
a = AAA.new
a.abc #输出:"方法abc"
a.cde #输出:"方法cde"
a.efg #输出:"方法efg"
[/pre]

那么在RMVA脚本编辑器里面,我们需要知道游戏运行的时候,左边列表内所有脚本是从上到下运行的。运行完了一个就运行下一个,完成了就关闭程序。
所以我们可以把上面的例子如此放入脚本编辑器内而获得输出的结果:
   2244.png

好,那么我们知道了如何透过猴子补丁的方式来给一个类追加方法。那么如果我们在追加类定义里面用了与原类定义相同的方法名字呢?
那就会造成方法覆盖了。新的方法取代(压制)旧的方法,达成了修改脚本的目标。
[pre lang="ruby"]
#原方法定义
class AAA
    def abc
        p "汪汪汪"
    end
end

#追加方法定义
class AAA
    def abc
        p "喵喵喵"
    end
end

#创建实例与方法调用
a = AAA.new
a.abc #输出:"喵喵喵" #而不是"汪汪汪"
[/pre]

我们知道,在用继承和多态的时候,如果子类覆盖父类的方法时,实际上只是在前面,后面或者前后都有增添代码,可以使用super关键词来调用父类的原方法。
就算是子类的其他方法或者透过引用从外部调用,也可以(在父类内)用alias_method来给原方法定一个别名。
那么同样地,在运用猴子补丁技法的时候,我们也可以给原方法取一个别名,让新方法可以调用。

Ruby为此准备了alias关键词。它和alias_method的功能相似,但是有一定的差距。
alias的使用格式是:alias 方法别名 原方法名 。两者之间不需要逗号(毕竟alias是关键词,所以系统有特殊的处理法)。
方法别名和原方法名可以直接就是方法名,符号,或者字符串。我的个人习惯是用符号,毕竟它在编辑器里面的颜色还是挺好看的(而且易读)。
[pre lang="ruby"]
#假设方法的别名是 old_area 而方法的原名是 area
#直接用方法名
alias old_area area
#用符号
alias :old_area :area
#用字符串
alias "old_area" "area"
[/pre]

在给旧方法创建了一个别名之后,就能让新方法像用了super关键词那样,调用旧的方法,并在调用的前面或者后面增加代码。
[pre lang="ruby"]
#原类定义
class AAA
    def abc
        p "旧方法的代码"
    end
end

#追加类定义
class AAA
    #设置别名
    alias :old_abc :abc

    def abc
        p "新方法增添的代码(前)"
        old_abc
        p "新方法增添的代码(后)"
    end
end

#创建实例与调用方法
a = AAA.new
a.old_abc #输出:"旧方法的代码"
a.abc
#输出:
"新方法增添的代码(前)"
"旧方法的代码"
"新方法增添的代码(后)"
[/pre]

alias和alias_method有着一定的差别。由于alias是一个关键词而alias_method是一个方法,前者使用时,参数中间不需要逗号,而后者需要。
另外,alias的作用领域在声明的地方,而alias_method的作用领域在呼叫它的地方。这个不太重要,因为我们大多都是直接在类定义里设置别名的。
不过我的建议是,将alias_method留给继承关系,而将alias用在猴子补丁这块儿。而且,我建议一律使用符号作为参数,容易看也容易写。
这样会比较容易明白,这到底是要继承父类的原方法,还是调用同个类内被覆盖的旧方法。

我们在Ruby编程里面可以用猴子补丁技法来拓展和更改接近任何事物。不喜欢系统方法?改啊。嫌名字难听或者太长?别名啊。方法不够?加啊。
假设我们要给数值(整数和浮点数)增添一个"钳"(clamp)方法,将数值限制到一个范围内。这在Ruby 2.4版成为了系统方法,但是RMVA的Ruby 1.9.2版没有。
注1:"钳"的运作原理是:如果该数值小于范围的最低值,就返回最低值。如果大于最高值,就返回最高值。否则,就返回那个数值。
注2:整数和浮点数的父类是Numeric(数值),所以要在这儿追加操作。
[pre lang="ruby"]
class Numeric #数字(整数,浮点数的父类)(为啥我会知道?F1帮助文档里有啊…)
    #给数字增添"钳"功能
    def clamp(min,max)
        return min if self < min #数值小于范围的最低值,就返回最低值
        return max if self > max #数值大于最高值,就返回最高值
        return self #否则就返回数值
    end
end

#实际测试
p 5.clamp(4,6) #输出:5 (在范围内所以小于自身)
p (7.888).clamp(12.3,60.5) #输出:12.3(钳到了最小值)
p 257.clamp(0,255) #输出:255 (钳到了最大值)
[/pre]
基本上没人能阻止你用猴子补丁技法来篡改系统方法,不管是追加还是更改功能。不过难度是有的,毕竟你不知道原本的代码是长啥样子。
就连RGSS3提供的内置类(如Bitmap, Sprite)也都能如此篡改。看不到原本的代码不代表你不能靠想象力啊(笑)。

最后要介绍的是运算符重载(Operator Overloading)。什么运算符?+ - * / [] 这些啊。重载?什么意思?就是更改它们的功能啊。
我去,这么冰淇淋?对的,就是这么冰淇淋。在Ruby语言内,运算符的操作是方法的一种。大部分运算符的功能都可以重新定义。
这就是为啥在Ruby语言,不止数字可以用加(+)和减(-)操作符,连字符串和数组都可以用加减操作符(而它们的功能在上一个章节介绍过了)。

作为实践例子,我们来创建一个Vector2D(二维向量)的类。二维向量有X值和Y值。我们也重载了to_s方法,让puts显示数据时显示它的值。
[pre lang="ruby"]
class Vector2D #二维向量
    attr_accessor :x #X值
    attr_accessor :y #Y值

    def initialize(x = 0,y = 0) #初始化
        @x = x
        @y = y
    end

    def to_s
        return "(#{x}, #{y})"
    end

end

#创建实例和调用
a = Vector2D.new(1,2)
b = Vector2D.new(3,4)
puts a #输出:(1, 2)
puts b #输出:(3, 4)

#加法运算(a + b)
c = Vector2D.new
c.x = a.x + b.x
c.y = a.y + b.x
p c #输出:(4, 6)
[/pre]
我去,这加法运算…好麻烦啊啊啊!不能用c = a + b 这样的,比较简单的方法吗?
当然行啊,运算符重载就是用来干这档事的。我们就来重载加法(+)运算符吧!
注意:以下的范例是追加类定义。如果要实际测试的话请自行合并代码。
[pre lang="ruby"]
#给Vector2D类追加+运算符的重载定义
class Vector2D
    def +(v)
        return Vector2D.new(@x + v.x, @y + v.y)
    end
end

#测试
a = Vector2D.new(1,2)
b = Vector2D.new(3,4)
c = a + b
puts c #输出:(4, 6)
[/pre]
嘿,这不就搞定了吗?运算符重载真强啊!只要把剩余的需要的运算符都重载了,这个类就完美了(请自己尝试)!

以下是可以重载的Ruby运算符:
[pre lang="ruby"]
[] []= 元素引用,元素赋值
! ~ (逻辑非,位非)
+@ -@ (一元加,一元减)
+ - * / (加,减,乘,除)
** % (乘方,取余)  
<< >> (左位移,右位移)
& | ^ (位和,位或,位异或)
<= < > >= (小于或等于,小于,大于,大于或等于)
== === != (等于,特殊等于,不等于)
=~ !~ (匹配,不匹配)*注:一般用于正则表达式
<=> (联合比较运算)
[/pre]
  




2.6 类方法与模块

展开折叠

在Ruby,一切皆为对象。不管是字符串,整数,数组,范围,还是基于自定义的类所创建出来的实例(那当然了)。
但是,就连类本身也是对象!什么意思呢?这么说吧。类就像一张蓝图,上面记载着创建出来的实例会是什么样子的。
但是这张蓝图的图纸,本身也是个对象。是生产图纸的蓝图的实例。要生产用来画蓝图的图纸,也得有一张蓝图来记载图纸的规格啊材料什么的。

要如何引用这个类对象呢?非常简单。它的名字就是存着这个对象的常量。(比如String存着字符串的类对象,Float存着浮点数的类对象等等)
解释器(系统)在处理类定义的时候,它会给这个类定义创建一个对象,然后将这个对象的引用存到同名的一个常量里面。
这个对象的类是Class,也就是类对象本身的蓝图。

为什么我们要知道这些呢?那是因为,我们接下来会介绍的,就是类方法(class method)和与它有关的事物。
类方法,就是类本身的实例方法。new就是其中一个类方法。比如说我们要创建一个空的数组。我们可以用Array.new来达成目标。
那么Array是数组的类,而new是它的类方法。那么让我们来看看如何定义类方法吧!

在类定义里面定义类方法的方式,是在方法的名字前面前缀"self.",代表给自身(类)定义方法。类方法可以是运作符重载。
我们就以上一个部分(运作符重载)使用的Vector2D(二维向量)类来当范例。注意:以下的范例是追加定义,因此请自行合并代码。
[pre lang="ruby"]
#追加定义
class Vector2D
    def self.zero #创建零向量
        return self.new(0,0)
    end

    def self.[](x,y) #过载[]运算符来当做new用
        return self.new(x,y)
    end
end

#实际调用
a = Vector2D.zero
puts a #输出:(0, 0)
b = Vector2D[4,5]
puts b #输出:(4, 5)
[/pre]
嘿,这不是挺好的嘛!有各种方法来创建新的对象啊!要弄什么向量啦矩阵啦也方便很多。

但是这又出现了两个问题。第一,每次都要前缀"self."很麻烦。第二,要取别名时怎么办?
我们可以加用 class << self ... end 来开启单例类的定义空间。单例类(singleton class)就是单一对象的类定义,只对该对象有效。
比如我们可以把上面的范例如此重写,而我们又可以用alias来给类方法取别名:
[pre lang="ruby"]
#追加定义
class Vector2D
    class << self #开启单例类的定义空间
        def zero #创建零向量
            return self.new(0,0)
        end

        def [](x,y) #过载[]运算符来当做new用
            return self.new(x,y)
        end

        alias :zero_alias :zero #给self.zero定一个别名:zero_alias
     end
end

#实际调用
a = Vector2D.zero
puts a #输出:(0, 0)
b = Vector2D[8,4]
puts b #输出:(8, 4)
c = Vector2D.zero_alias #尝试使用zero的别名
puts c #输出:(0, 0) #成功了!
[/pre]
我个人的习惯是如果需要用到alias(或者alias_method)才用class << self。这会因人而异。

那么之前我们有介绍过变量的类型。有实例变量,类变量…那么既然类是个对象,它能不能用实例变量呢?能。
基于它所创造的实例能不能用这个实例变量呢?不能。那么…前缀@@的类变量和无法改动的常量呢?
类和基于它所创造的实例都能用。好,那么就来看看范例:
[pre lang="ruby"]
class AAA
    #类方法
    class << self   
        def set_var #设置类的实例变量和类变量
            @self_var = 666
            @@class_var = 233
        end

        def show_var#显示类的实例变量和类变量
            p "类"
            p @self_var
            p @@class_var
        end
    end
    #实例方法
    def show_var #显示该实例的实例变量和类变量
        p "实例"
        p @self_var
        p @@class_var
    end
end

#实际调用
AAA.set_var
AAA.show_var
AAA.new.show_var
#输出:
#"类"
#666
#233
#"实例"
#nil
#233
[/pre]
由此可见,类里的@实例变量只能由类方法使用,而@@类变量则是类和它的实例所共用的。

那么我们在说到实例变量和类变量时有说过,实例变量是在initialize方法里面初始化的。类变量则可以在类定义的空间内直接赋值初始化。
那么我们来做个试验吧!如果把@前缀的实例变量放在方法外面会发生什么事!
[pre lang="ruby"]
class AAA
    #类方法
    class << self   

        #在单例类定义空间内赋值:实例变量,类变量,常量
        @self_instance_var = 233
        @@self_class_var = 555
        SELF_CONSTANT = 666

        def show_var #显示变量和常量
            p "类"
            p @self_instance_var
            p @@self_class_var
            p SELF_CONSTANT #注:会报错
            p @instance_var
            p @@class_var
            p CONSTANT
        end
    end

    #在类定义空间内赋值:实例变量,类变量,常量
    @instance_var = 777
    @@class_var = 888
    CONSTANT = 999

    #实例方法
    def show_var #显示变量和常量
        p "实例"
        p @self_instance_var
        p @@self_class_var
        p SELF_CONSTANT  #注:会报错
        p @instance_var
        p @@class_var
        p CONSTANT
    end
end

#实际调用
AAA.show_var
AAA.new.show_var

#输出:
#"类"
#nil #@self_instance_var
#555 #@@self_class_var
#报错:找不到常量SELF_CONSTANT (需要修复才能继续)
#777 #@instance_var
#888 #@@class_var
#999 #CONSTANT
#"实例"
#nil  #@self_instance_var
#555 #@@self_class_var
#报错:找不到常量SELF_CONSTANT (需要修复才能继续)
#nil #@instance_var
#888 #@@class_var
#999 #CONSTANT
[/pre]
那么从上面的实验我们可以观察到:
[pre lang="ruby"]
在类定义空间(class << self)里面,
-> @实例变量无法被读取(nil)
-> @@类变量可以被类方法和实例方法读取
-> 常量无法被读取,而且会报错
在类定义空间里面,
-> @实例变量可以被类方法读取,但是无法被实例方法读取
-> @@类变量可以被类方法和实例方法读取
-> 常量可以被类方法和实例方法读取
[/pre]
那么在类里定义的常量除了可以在类的内部使用(不管是类方法或是实例方法),还可以从外部直接引用。
不过,引用常量的方法与调用方法不同。不是属性解析符(. 句号)而是常量解析符(:: 双冒号)。
[pre lang="ruby"]
class AAA
    #定义常量
    CONSTANT = 666
end

#使用常量解析符(::)引用AAA类内的CONSTANT常量
p AAA::CONSTANT #输出:666
[/pre]

好,那么我们已经知道了怎么定义类方法和在类里定义常量,也知道类其实是Class(可以说是类的蓝图)的实例。
在RMVA默认脚本里,我们可以看到一些脚本里定义的并不是类(class)而是另一种玩意儿:模块(module)。

什么是模块?用最简单的方法来描述的话,就是不能创建实例的一种类。不过,实际情况却是相反的。
类是可以创建实例的一种模块。这是因为类是模块的子类。(Class < Module ,注意:Module是模块的蓝图类)
既然是这样,为什么要用模块呢?第一种原因非常直白,就是用于表示“不应该存在实例”。
RMVA默认脚本中的DataManager, SceneManager, BattleManager等模块,都是只用类方法,类本身的实例变量和常量的。
它们并不需要实例方法和实例变量,所以定义为module(模块)而不是class(类)。这可以避免使用者误操作而弄出了个实例。

创建模块的方法,就是将定义时的class替换成module罢了。要注意的是,模块没有继承能力。
[pre lang="ruby"]
#模块定义
module AAA

    #定义常量
    CONSTANT = 666

    #定义类方法
    def self.abc
        p "RMVA"
    end

    #开启单例类定义空间来定义类方法
    class << self
        def cde
            p "66RPG"
        end
    end

end

#实际调用
AAA.new #报错!找不到new方法!(嗯,模组没有new也无法创建实例)
AAA.abc #输出:"RMVA"
AAA.cde #输出:"66RPG"
p AAA::CONSTANT #输出:666
[/pre]

模块的使用还有第二个鲜为人知的原因:用于实现多重继承。也就是将模块内定义的实例方法混入别的类/模块里面。
所有的类和模组默认都有两个方法:include(包括)和extend(拓展)。用法和attr_accessor相似,就是在类/模组定义里调用。
使用格式是:include 模组名 或者 extend 模组名。它们可以在不影响类继承的情况下,给一个类/模块添加实例方法(include)或者类方法(extend)。

首先来看看include的用法:
[pre lang="ruby"]
#定义模块
module AAA

    #定义常量
    CONST = 666

    #定义类方法
    def self.abc
        p 555
    end

    #定义实例方法(对模块本身来说无用)
    def cde
        p 233
    end
end

#定义类
class CCC
    #使用include来混入模组内的常量和实例方法
    include AAA
end

#实际调用
c = CCC.new
c.abc #报错!找不到方法!#混入只对模块内的实例方法有效
c.cde #输出:233 #代表模块AAA里定义的实例方法abc成功混入了CCC类
CCC.abc #报错!找不到方法!#混入只对模块内的实例方法有效
CCC.cde #报错!找不到方法!#使用include混入方法是作为类的实例方法而不是类方法
p CCC::CONST #输出:666 #代表模块AAA里定义的常量成功混入了CCC类
[/pre]

我们也可以在模块里include。然后,可以通过这个模快将常量和实例方法转交给类。
模块内通过include而获得的常量可以被通常方式引用,但是实例方法却不能,只能用来让别的类include获取。
[pre lang="ruby"]
#定义模块
module AAA
    CONST = 666
    def abc
        p 233
    end
end

#定义模块 - 用include混入模块AAA
module BBB
    include AAA
end

#定义类 - 用include混入模块BBB
class CCC
    include BBB
end

#实际调用
c = CCC.new
c.abc #输出:233 #代表模块AAA里定义的实例方法成功通过模块BBB来混入CCC类
p BBB::CONST #输出:666 #模块AAA里的常量混入了模块BBB
p CCC::CONST #输出:666 #模块AAA里的常量通过模块BBB混入了CCC类
BBB.abc #报错!找不到方法!include混入的方法是作为实例方法,所以模块无法调用
[/pre]

见识了include的功能,现在我们来看extend。extend将模块内定义实例方法作为类方法来混入别的类或者模块。
[pre lang="ruby"]
#模块定义
module AAA
    CONST = 666
    def abc
        p 233
    end
end

#类定义 - 用extend来将模块AAA里的实例方法混入,作为自己的类方法
class CCC
    extend AAA
end

#实际调用
CCC.abc #输出:233
p CCC::CONST #报错!找不到常量!#这是因为extend并不会混入常量
c = CCC.new
c.abc #报错!找不到方法!#通过extend混入的是作为类方法,因此实例无法调用
[/pre]
extend不止可以添加类(class)的类方法,也能添加模(module)的类方法。毕竟类就是可以创建实例的模组嘛。

include和extend如果同时用会怎么样?那就类方法也有,实例方法也有,常量也有咯。这是要看情况和需求的。
模块的这个混入能力让我们可以更有效地整理方法。比如C类通过include混入A模块和B模块来添加常量和实例方法,然后extend了E模块来添加类方法。
另一个D类则include了B模块和F模块,然后extend了G模块… 这样可以在不影响既有的继承关系之下,另外再增添方法和常量。
这是Ruby的一大精髓啊,可不能被埋没了。不过…变量呢?实例变量不就是有用到时才赋值创建的嘛,对吧。

如果想要让模块里定义的实例方法可以从外部被调用,怎么办?那就在模块的定义里调用module_function来指定可以被外部调用的实例方法。
用法基本上和attr_accessor差不多。让我们直接看范例:
[pre lang="ruby"]
#定义模块
module AAA
    #定义实例方法
    def abc
        p 666
    end

    def cde
        p 233
    end

    module_function :abc #指定可以被外部调用的实例方法
end

AAA.abc #输出:666
AAA.cde #报错!找不到方法!
[/pre]
调用module_function后,指定的实例方法会成为该模块的类方法。
这些实例方法仍然可以通过include或者extend被混入类/模组里,但是会成为私有的实例/类方法。

那么这个部分的最后,我们要说到的是模块和类的嵌套(nested class/module)。
我们在RMVA默认脚本里可能有看过这样的代码:RPG::Item,RPG::Weapon,RPG::Animation::Frame …这些在F1帮助文档里面的数据结构部分也见过。
这些其实是类的名字。数据库元素的类。RPG::Item是道具数据库内的元素的类。RPG::Weapon则是武器数据的类。
那么我们现在要讨论的,就是这个"RPG::"是什么玩意儿。我们再复习一下类的创建原理。

        解释器(系统)在处理类/模块定义的时候,它会给这个类/模块定义创建一个对象,然后将这个对象的引用存到同名的一个常量里面。

如果我们在公共区域(没有被任何class或者module包着)定义一个类或者模块,那么这个常量,就是全局的了。
那我们在类的定义或者模块的定义里面又定义了一个类或者模块的话呢?嵌套就产生了。内层类的引用会作为外层类的常量来储存。
[pre lang="ruby"]
module AAA
    module BBB
        CONST = 666

        def self.xxx
            p "xyz"
        end
    end

    class CCC
        class DDD
            def efg
                p 10086
            end
        end        

        def cde
            p 555
        end
    end
   
    self.abc
        p 233
    end
end

#实际调用:
p AAA.abc #输出:233
p AAA::BBB::CONST #输出:666
AAA::BBB.xxx #输出:"xyz"
c = AAA::CCC.new
c.cde #输出:555
d = AAA::CCC::DDD.new
d.efg #输出: 10086
[/pre]

不管是类(class)还是模块(module)都能互相嵌套。这能让类/模块作为命名空间(namespace)来让代码的管理更方便。
命名空间是啥?就是如果你有完全不相干的,名字相同的两个类,你可以将它嵌套进不同的类/模块来作鉴别。嵌套进去的就是命名空间。
比如名字同是Weapon的另个类,我们可以把它嵌套进名为SRPG的类/模块里面(就成了SRPG::Weapon),让它有个特殊的名字。
这样的话,SRPG::Weapon就不会跟没有嵌套的Weapon搞混了。尤其是Ruby有猴子补丁的情况下,这是很严重的事情。
一般上,如果只是用来作为命名空间而没有其他用意的话,我的建议是一律用模组(module)作为“容器”。

我们其实还有另一种方法可以实现嵌套(巢状结构)。请看范例:
[pre lang="ruby"]
module AAA; end #空的模块,作为容器

class AAA::BBB
    #内容1
end

class AAA::BBB::CCC
   #内容2
end
[/pre]
以上的代码和下列的代码是一样的:
[pre lang="ruby"]
module AAA
    class BBB
            #内容1
        class CCC
            #内容2
        end
    end
end
[/pre]
当然用哪个比较好,这是说不清的,因为这得看个人风格。不过我建议是初次定义时用后者(嵌套),而追加定义用前者(用::来平铺)。  





2.7 垃圾回收机制

展开折叠

Ruby毕竟是面向对象的语言,一切事物都是对象。也就是说,我们时时刻刻都在创建对象。一个字符串是一个对象,一个数组又是一个对象。
那么问题就来了。对象毕竟是一组数据,是要耗费内存的(不知道什么是内存的…快去百度搜索!)。电脑的记忆体空间是有限的。
RMVA的内存消耗限制大概是900MB~1.1GB之间吧。也许因人而异。在我自家的2GB旧电脑上,内存消耗到了这个范围程序就崩溃了。
既然我们一直在创建对象,那么就是会一直在消耗内存啊。对象多,内存消耗就越多,那久而久之不就崩溃了吗?

为了避免这种问题的发生,Ruby配备了一个叫做“垃圾收集器”(Garbage Collection,简称GC)的机制。
在说到对象和引用的时候,我们知道了引用的重要性。如果程序内的所有活跃的变量都没有存着某个对象的引用,那么它就是被“孤立”了。
这个被“孤立”的对象,可以说是已经“死了”。既然死了,这个尸体总不能挡着路啊。这就是GC的工作范围。

GC什么时候会执行任务?不知道,也无法预测。我们也不怎么需要注意,除非你在做内存消耗非常巨大的事情。
我们作为RMVA的使用者所需要知道的,就是如何让GC可以辨识到这是垃圾,而清除它。

要消除一个对象的引用,最直接的方法就是把存着它的引用的变量赋值成别的东西,比如nil(空值)。
[pre lang="ruby"]
a = Array.new
a = nil
[/pre]

除了全局变量(前缀$),类变量(前缀@@)和类实例变量(类方法用的@变量)之外的变量都是暂时性的。也就是说,它的寿命总有到尽头的时候。
我们知道,局部变量(无前缀)的作用域是代码块内罢了。也就是说,代码块结束的时候,局部变量会被删除而导致原本存着的对象失去引用。
[pre lang="ruby"]
def abc
        a = Array.new
        #方法结束后,局部变量a就被删除了,里面的数组也就被孤立了
end

abc #方法调用
[/pre]

实例变量(前缀@)的作用范围是实例的内部。如果该对象因受孤立而被GC清除,那么它里面所有实例变量都会被删除。
[pre lang="ruby"]
class AAA
        def initialize
                @aaa = Array.new
                #在实例被GC清除后,实例变量会被删除,导致@aaa里原本装着的数组被孤立,迟早被GC处理掉
        end
end

a = AAA.new
a = nil #原本装着的AAA类的实例被孤立,迟早会被GC处理掉
[/pre]

因此,如果要长期保存一个对象,我们必须确保它的引用被保存在永久保持活跃状态的变量里面。
这是RMVA默认脚本里,Cache(缓存)模块的功能。游戏所导入图像素材的引用会被存在一个哈希表里面。
这个哈希表的引用被存在Cache模块的一个类实例变量里面(@cache)。

我们知道,Ruby的垃圾回收机制是自动运行的。什么时候会清除一个受孤立的对象,我们无法知道。
但是,我们可以手动呼叫GC来立即处理掉所有受孤立的对象,尤其是在进行了大幅度消耗内存的操作之后。
只要呼叫GC.start就行了。GC是Ruby垃圾回收模块的名字,而start是开始的意思。
[pre lang="ruby"]
a = Array.new
a = nil #导致上一行的数组被孤立
GC.start #手动呼叫垃圾回收机制
[/pre]
不过,手动呼叫GC来清除对象的时候可能会耗费一些时间。一次过处理的对象多了也许会有点慢,所以要多加注意。
  




注意:2.8和2.9是较高级的内容,但是只要能成功理解,会对脚本的编写有很大的帮助。

2.8 对象之根与高阶操作

展开折叠

看到这里,想必你已经知道如何解读大部分RMVA的默认脚本,也可以参考注释模仿默认脚本做些许的修改了吧。
不过,学习是漫无止境的。在这一个部分,我们会探讨Ruby对象的“根源”,也会认识到一些高级方法和概念。

首先,你可曾想过,为什么一个空的类还是有可以调用的实例方法?
[pre lang="ruby"]
class AAA; end #定义空的类
p a.to_s #输出:"#AAA<0x12345678>" #注:后面的十六进制数是随机生成的
[/pre]
啥?空的类居然知道to_s(转换成字符串)方法?哪儿来的?难道它有超类(或者称为父类)?
没错。Ruby的所有对象都有内置的超类。我们可以找出它的超类,和它的超类的超类的超类…一直到它最初的根源。

这个方法叫做superclass(显示超类),是一个类方法。好,那我们来看看,空的类的超类是啥。
[pre lang="ruby"]
class AAA; end
p AAA.superclass #输出:Object
[/pre]
哇哦!找到了!AAA的超类是Object。Object可是对象的英文名啊。为了不搞混,还是把它称为“物件”好了。那么Object的超类又是啥?
[pre lang="ruby"]
p Object.superclass #输出:BasicObject
p BasicObject.superclass #输出:nil #就是没了
[/pre]
好,很明显,Object(物件)的超类是BasicObject(基本物件)。
因此我们可以推断出,任何在脚本里自定义的类,都有内置的继承链:类 < 超类 < Object < BasicObject。
to_s这个方法,想必是从它的内置超类里面传承下来的。

但是现在有一个问题。我们在介绍类方法和模块的时候提到过,类是一个对象,是Class类的实例。(模块是Module类的实例)
现在我们来验证一下这个说法,要使用的是class(显示自己是哪个类的实例)方法:
[pre lang="ruby"]
module AAA; end #定义空模块
class BBB; end #定义空类

p AAA.class #输出:Module
p BBB.class #输出:Class
[/pre]
验证成功!模块是Module的实例,而类是Class的实例。
现在我们来看看Class的继承树是怎样的,寻找它的根源。(用superclass方法)
[pre lang="ruby"]
p Class.superclass #输出:Module
p Module.superclass #输出:Object
p Object.superclass #输出:BasicObject
p BasicObject.superclass #输出:nil #就是没了
[/pre]
Class > Module > Object > BasicObject
哎哟,怎么又回到Object来了?是的。Ruby里面一切都是对象。一切的父类都是Object,这是很正常的事情。

把以上所得到的编成一张表的话,是长这个样子的:
   
这样我们就看清楚了各种类与实例之间的关系,也得出一个结论:Object(和它的父类BasicObject)是Ruby一切对象之根。

那么除了用superclass方法(注:superclass是Class类的实例方法)来获取一个类的超类之外,我们还可以用ancestors(祖先们)来获取继承树。
继承树里面除了显示自身和所有超类之外,还会显示出自身或者任何超类所混入的模块。
我们就以Array来做例子吧。
[pre lang="ruby"]
p Array.ancestors #输出:[Array, Enumerable, Object, Kernel, BasicObject]
[/pre]
从得出的列表我们可以知道:
Array(数组)混入了Enumerable(枚举)这个模块
Object(物件)混入了Kernel(核心)这个模块
Array的继承树是:Array < Object < BasicObject

那么问题来了。为什么我们要知道这些?只是因为这是较“高级”的事情,去探讨只是因为兴趣吗?
实际上,我们需要知道每个内置类的类方法和实例方法,以运用它们来简化原本非常复杂的事情。

举个例子好了。请问,如何知道一个对象是什么类的实例?告诉你吧,有两种判定法。
第一种是用is_a?方法。这是Object的实例方法,因此内建在所有对象里面了。第二种是用class(也是Object的实例方法)获取类,然后比较一下。
[pre lang="ruby"]
a = "123"
p (a.class == String) #输出:true
p (a.class == Array) #输出:false
p a.is_a?(Object) #输出:true
p a.is_a?(Hash) #输出:false
[/pre]
用class获取类来比较和用is_a?方法来判断的分别就是is_a?会考虑超类和混入的模块。
我们可以推断,a.is_a?(String) 的功能与 a.class.ancestors.include?(String) 相似。

那为什么我会知道要用class来获取类呢?那是因为我知道了一切对象内置的超类必定有Object。
然后,我在Ruby官方帮助文档(RubyDoc)里关于Object的资料页面里找到了class的功能。
这样看来,这个部分绝对是非常重要的呢。

但是,Ruby类的方法那么多,怎么知道一个类有什么方法呢?Object类里面提供了数种实例方法来获取方法列表。
要获取公共方法列表的话,要用 public_methods,要获取受保护方法列表的话,要用 protected_methods 。
公共方法和受保护方法都要的话,用 methods 就行了,但是如果要获取私有方法的列表,需要用private_methods 。
如果你嫌获取私有方法要另外调用很麻烦,你大可用猴子补丁技法给Object追加一返回methods + private_methods的方法。
获取的方法列表里面装着的是方法名字的符号。

现在我们来看看Object有多少种实例方法。
[pre lang="ruby"]
#注意:以下的数据是基于RMVA自带的Ruby 1.9.2的。其他版本Ruby可能会不同。
p Object.new.public_methods.count #输出:56
p Object.new.protected_methods.count #输出:0
p Object.new.private_methods.count #输出:71
[/pre]
我去!56个公共方法?71个私有方法?要完啊!这是因为Object混入了Kernel(核心)模块的实例方法,也继承了BasicObject的实例方法。

以上三种显示方法列表的方法都是给实例调用的。如果类与模块本身调用的话,会显示它们的类方法。
因此,我们需要用public_instance_methods,protected_instance_methods和private_methods获取类或模块的实例方法。
如果要同时获取公共和受保护的实例方法列表,就要用instance_methods。不过一般上内置的受保护方法非常少,所以直接用它就好。
[pre lang="ruby"]
p Object.instance_methods.count #输出:56
p Object.private_instance_methods.count #输出:71
[/pre]

那么我们来看看Object的类方法和实例方法到底有哪些,有多少。
[pre lang="ruby"]
p "Object的类方法(公共)"
p Object.methods.sort #sort是排序
p "Object的类方法(私有)"
p Object.private_methods.sort
p "Object的实例方法(公共)"
p Object.instance_methods.sort
p "Object的实例方法(私有)"
p Object.private_instance_methods.sort
[/pre]

输出:
   
   
哇哦!好多!所以啊,这可是个宝库。我们可以获取任何实例对象的方法列表,和任何类/模块的类方法和实例方法列表。

除了获取方法列表之外,我们还可以获取…常量,实例变量,类变量甚至全局变量的列表。
获取常量:constants,获取实例变量:instance_variables,获取类变量:class_variables,获取全局变量:global_variables 。
constants和class_variables是类方法,instance_variables和global_variables是实例方法。
global_variables是私有方法,所以调用时不能用解析符。不过不管在哪儿调用都一样的啦。
[pre lang="ruby"]
class AAA
    CONST = 100
    @@class_var = 555
    def initialize
        @abc = 5
        @cde = 8
    end
end

#实际调用
p AAA.constants #输出:[:CONST]
p AAA.class_variables #输出:[:@@class_var]
a = AAA.new
p a.instance_variables #输出:[:@abc, :@cde]
[/pre]

除此之外,Object还提供了instance_variable_get(获取实例变量)和instance_variable_set(设置实例变量)两个非常强大的方法。
有些时候,我们需要遍历一个对象的所有实例方法来施展一些操作。RMVA默认脚本里,Scene_Base类就有如此的方法。
这两个方法是:update_all_windows(更新所有窗口)和dispose_all_windows(释放所有窗口)。没错,这时透过遍历实例变量而达成的。
让我们来看看范例吧!
[pre lang="ruby"]
#类定义
class AAA
    def initialize
        @abc = 555
        @cde = 666
    end

    def show
        p "#{@abc} #{@cde}"
    end
end

#实际调用
a = AAA.new
p a.instance_variable_get(:@abc) #输出:555 #注意:必须有@前缀
p a.instance_variable_get("@cde") #输出:666 #注意:参数可以是符号或字符串
a.instance_variable_set(:@abc, 233) #第一个参数是名字,第二个是赋值内容
a.show #输出:"233 666" #成功篡改实例变量@abc了!
[/pre]

一般来说,如果要删除一个实例变量,我们直接给它赋值nil(空值)就好了。不过…如果强迫症发作硬是要删除呢?从列表内删除掉?
每个对象都有一个私有的实例方法:remove_instance_variable(移除实例变量)。
既然是私有的,就是说它无法从外部调用。我们要给它建一个“包裹”。
[pre lang="ruby"]
class AAA
    def initialize
        @abc = 555
        @cde = 666
    end

    def remove_ivar(v)
        remove_instance_variable(v)
    end
end
#实际调用
a = AAA.new
p a.instance_variables #输出:[:@abc, :@cde]  
a.remove_ivar(:@abc)
p a.instance_variables #输出:[:@cde]  
[/pre]
不过呢,毕竟面向对象的基本特征之一是“封装”,instance_variable_get之类的方法滥用的话,可能就会造成结构紊乱,所以得善用啊。

在Ruby内,一切都是对象。这包括最顶层(全局)的作用域,也就是main。它是一个Object类的实例。因此,所有Object类的实例方法都是全局方法。(比如p和rand)
[pre lang="ruby"]
p self #输出:main
p self.class #输出:Object
[/pre]
在全局作用域里定义的每一个方法,都会成为Object类的私有实例方法。定义的每一个类/模块,都会成为Object类的常量之一。
[pre lang="ruby"]
v = Object.private_instance_methods #暂存当前Object的私有实例方法
def aaa; end #定义空的全局方法
p  Object.private_instance_methods - v #显示Object新增的私有实例方法
#输出:[:aaa]

v = Object.constants #暂存当前Object的常量
class AAA; end #定义空的类
module BBB; end #定义空的模块
p Object.constants - v #显示Object新增的常量
#输出:[:AAA, :BBB]
[/pre]
因此,我们可以调用Object.constants来查看全局常量,然后筛选出类/模块(有至少一个小写字母,除非你命名自定义类为全大字母)。
[pre lang="ruby"]
p Object.constants.select{|s| s.to_s =~ /[a-z]/} #显示所有全局常量,筛选出所有至少包含一个小字母的常量
[/pre]
输出:
   
这就是RMVA系统所自带的类的列表了。有系统的String(字符串)啊Array(数组)啊什么的,还有RGSS3的Sprite(图像精灵)之类的。

Ruby的各种类还有很多很多我们可以运用的高效率方法。本教程没特别介绍的,就得靠自己去科普了。
  




2.9 方法对象与代码块

展开折叠
  
在上一个部分,我们学到了透过名字来获取或设置实例变量的方法(instance_variable_get和instance_variable_set)。
实际上啊,连方法调用都可以这么做。Object类提供了两个方法:send(发送)和public_send(公共发送)。
用法是:send(方法名字, 参数) 。有多少个参数就填多少个,否则不需要。public_send只能用于调用公共方法,因此较为安全。
[pre lang="ruby"]
#定义类和方法
class AAA
    def add(a,b)
        a + b
    end

    def abc
        p 666
    end
end
#实际调用
a = AAA.new
a.send(:abc) #输出:666 #相等于a.abc
b = a.send(:add, 7, 9) #相等于a.add(7,9)
p b #输出:16
[/pre]
send的功能非常强大。它可以强制调用一个对象的私有方法,更是可以基于储存起来的方法名字进行调用。
让我们来看看下面的,基于四则运算的这个例子:
[pre lang="ruby"]
a, b = 6, 2 #注意:这是a = 6; b = 2 的缩写
[:+,:-,:*,:/].each { |s| p a.send(s, b) end
#输出:
#8 # a + b #等同 a.send(:+, b)
#4 # a - b #等同 a.send(:-, b)
#12 # a * b #等同 a.send(:*, b)
#3 # a / b #等同 a.send(:/, b)
[/pre]
这是非常方便的。如果需要以同样的参数呼叫多个方法,可以考虑用这个方式来精简化。
当然,send和public_send最重要的使用情况还是调用的方法不固定的时候。

Ruby的一切都是对象。这句已经重复了说了不知道多少遍,不过还是得说。就连方法也是对象。
什么?方法也是对象?那是不是可以把一个对象方法的引用交给别的对象来调用啊?答对了!
这是RMVA默认脚本里,窗口(尤其是指令窗)和场景互动的主要方法。因此,我们必须得熟悉这个技巧。

获取实例变量的方法是instance_variable_get,那么获取方法引用的方法是method和public_method。
两者的区别就是,public_method只能获取公共方法,而method可以获取公共,私有和受保护方法的引用。
获取方法之后,我们可以用该方法对象的call方法(要附上参数)来实际调用那个方法。

让我们来看范例:
[pre lang="ruby"]
class AAA
    def initialize(v)
        @aaa = v
    end

    def abc
        p @aaa
    end
end
#实际调用
a = AAA.new(5)
b = AAA.new(7)
c = a.method(:abc)
d = b.method(:abc)
c.call #输出:5
d.call #输出:7
[/pre]
我们发现的是,c和d同样是AAA类的abc方法,但是它们属于不同的,AAA类的实例(分别属于a和b)。
因此,它们的作用域不一样,是基于所属的实例的。(这关系到实例变量和用self获取的自身引用)

我们可以透过调用方法的方式来创建,注销或者移除方法。先来说后者吧。
Module(模组的蓝图)提供了undef_method和remove_method两个方法。用法和attr_accessor相似。
def是定义,那么undef是?注销定义。remove则是移除的意思。两者的差距在于会不会追溯超类和混入模块。(后者会)
一般上我们都是用undef_method来注销方法的。比如让一个类无法透过new来创造实例。
[pre lang="ruby"]
class AAA
    class << self
        undef_method :new
    end
end

a = AAA.new #报错!找不到方法new!
[/pre]
这样的话,new这个方法就报废了。它也不会去看父类(Object等等)和混入模块有没有这个方法了。(remove_method移除的则会)
那么你会问的是,这有什么用?如果我要用其他的类方法来创建新的实例,没有new不就悲剧了吗?
其实new的定义是这样的:
[pre lang="ruby"]
class AAA
    class << self
        def new(*args)
            a = allocate
            a.send(:initialize, *args)
            return a
        end
    end
end
[/pre]
allocate是Class的实例方法。调用它会生成一个未初始化的对象。我们需要(用send)调用了initialize之后才是正确的方法。
因此,我们大可自己创建一个新的私有方法来避免透过new来创建新的方法。当然如果只是作为父类的,更是不用担心了。
话说回来…把new设置成私有不就行了吗?private :new 啊。啊…对哦…哈哈哈。

我们知道,在类里定义方法是要用def关键词,而定义类方法的话,最好的方式就是用class << self开启类方法的定义域。
不过,def关键词和alias关键词类似,都是脚本在读取的阶段就已经由解释器(系统)处理了的。
如果要在运行的时候追加方法的定义,尤其是方法名字和内容还未知的情况下,我们需要用define_method来达成这个效果。
让我们直接看范例,比较一下def和define_method的差别:
[pre lang="ruby"]
#注意:此段代码无法直接运行
#使用def定义方法
def add(a,b)
    return a + b
end

#使用define_method定义方法
define_method(:add) do |a, b|
    return a + b
end
[/pre]
参数的定义和def一样。def add(a = 0, b = 0, *args); end 和 define_method(:add) do |a = 0, b = 0, *args| end是一样的。

我们知道,如果呼叫method会获得这个实例的方法对象(的引用)。得到的是该实例所属的,绑定了的方法对象。
我们可以调用unbind参数来获得一个解绑了的,无绑定的方法。这是UnboundMethod(无绑定方法)的实例,无法被调用。
这个无绑定的方法必须调用bind来绑定一个对象,而且是限定与定义这个实例方法的类的实例对象,才能被调用。
目前我们并没有遇到过需要解绑方法的情况,因此不附上范例。

我们可以调用类方法instance_method(和限定公共的public_instance_method)来获取未绑定的实例方法。
这个方法可以用bind来指定一个对象作为接受者(receiver),完成方法的绑定而允许调用。
[pre lang="ruby"]
class AAA
    def initialize(v)
        @aaa = v
    end

    def abc
        p @aaa
    end
end
#实际调用:
a = AAA.new(7)
method_a = AAA.instance_method(:abc)
method_a = method_a.bind(a)
method_a.call #输出:7
[/pre]
为什么要知道这个呢?方法在创建实例的时候就有了啊,怎么还需要再来个移花接木呢?
是这样的。instance_method 和 bind 和 define_method 配合,能挥它威猛的实力。假设我们有如此的代码:
[pre lang="ruby"]
class AAA
    def add(a,b)
        a + b
    end

    def sub(a,b)
        a - b
    end

    def p_add(a,b)
        p "答案:"
        p add(a,b)
    end

    def p_sub(a,b)
        p "答案:"
        p sub(a,b)
    end
end
[/pre]
我们可以缩写成这样:
[pre lang="ruby"]
class AAA

    def add(a,b)
        a + b
    end

    def sub(a,b)
        a - b
    end

    ["add", "sub"].each do |s|
        sym = :"p_#{s}" #快速创建符号 如:p_add或者:p_sub
        old = instance_method(:"#{s}") #获取方法add或者sub
        define_method(sym) do |a,b| #创建新方法p_add或者p_sub
            p "答案:"
            p old.bind(self).call(a,b) #相当于 p add(a,b) 或者 p sub(a,b)
        end
    end
      
end
[/pre]
也许两个方法还不能展现这个做法的精辟,不过随着要定义的方法越来越多,或者是方法的名字或内容不固定,就显得很有用了。

类是对象,方法是对象,还有什么不是对象的?没有,一切都是对象。
还记得调用一些方法(比如刚才的define_method就是了)时附上的代码块(Block)吗?它也是对象!

代码块可以作为Proc类的实例被储存为对象。Proc是Procedure(过程)的缩写,可以视为匿名方法(不知道是谁的方法,反正执行就对了)。
创建Proc对象的方式有三种,而调用Proc和调用方法对象一样,都是通过call方法的。让我们直接看范例吧:
[pre lang="ruby"]
proc1 = proc {|a,b| a + b}
p proc1.call(1,2) #输出:3

proc2 = Proc.new {|a,b| a + b}
p proc2.call(1,2) #输出:3

proc3 = lambda {|a,b| a + b}
p proc3.call(1,2) #输出:3
[/pre]
注意:{|a,b| a + b} 和 do |a,b| a + b end 是相似的,但前者的优先度较高。不过只要调用方法时参数都用括号包住,才不用担心什么优先度呢。
而且,就如define_method时我们用的代码块那样,我们是可以指定默认参数和追加参数的。(如 |a = 0, *args|)

Proc.new和proc是一样的(一般用后者)。不过,lambda却不一样。lambda产生的Proc对象是会检查参数数量的。
[pre lang="ruby"]
proc1 = proc {|a,b| p a + b}
lambda1 = lambda {|a,b| p a + b}

proc1.call(1) #报错!执行运算失败! #这是因为只有一个参数,另一个成了nil
proc1.call(1,2) #输出:3 #正常
proc1.call(1,2,3) #输出:3 #这是因为多余的参数会被无视

lambda1.call(1) #报错!参数数量错误!需要2个参数但只给了1个!
lambda1.call(1,2) #输出:3 #正常
lambda1.call(1,2,3) #报错!参数数量错误!需要2个参数却给了3个!
[/pre]
一般我们把lambda产生的,会检查参数数量的Proc对象称为lambda表达式(lambda expression),作为真正的匿名方法(毕竟proc不会检查参数数量)。
lambda除了会检查参数数量之外,还可以从里面调用return来返回内容。proc则不行。要知道一个Proc对象是不是lambda,要调用lambda?方法来判断。

如果我们要给方法递代码块,怎么办?实际上,所有方法后面(参数的后方)都能接受代码块。问题只是在方法会不会去用它罢了。
让我们来看看代码块要怎么呼叫(call)吧!
[pre lang="ruby"]
def abc
    yield
end

def cde(&block)
    block.call
end

abc { p "hello" } #输出:"hello"
abc #报错!没有附上代码块!

cde { p "hello" } #输出:"hello"
cde #报错!没有附上代码块!
[/pre]
噢,如果没有给代码块指定一个名字,那么就要用yield(产生)关键词来运行这个代码块(可以附上参数,类似call)。
要指定一个名字的话,前缀是&,然后调用的时候呼叫call就对了,后面附上参数。注意:代码块不算在参数列表内。

不过这里就出了一个问题。有些方法,如Array(数组)的count(计量)方法,是可以选择要不要附上代码块的。
但是我们也看到了,如果在没有附上代码块的情况下用yield或者call,就会出错。那要怎么才能避免这个问题呢?
我们可以在方法里面调用block_given?方法来判定是否给了代码块,来判断要进行什么操作。
[pre lang="ruby"]
def abc
    if block_given?
        yield
    else
        p "没有代码块"
    end
end

abc {p "有代码块"} #输出:"有代码块"
abc #输出:"没有代码块"
[/pre]

Proc的意义就是把代码块封装起来,作为一个对象,因此可以作为普通的参数来递入给方法,或者直接使用。
如果已经是封装成Proc对象的代码块呢?怎么递入上列的方法里面?那就在参数前面加&符号。
一般遇到这种情况的时候,请用lambda而不是proc来封装代码块。proc比较偏于后台的运作,而不是前端的调用。
[pre lang="ruby"]
def abc(&block)
    block.call
end

a = lambda { p "hello" }
abc(&a) #输出:"hello"
[/pre]

总而言之,Ruby还有非常非常多的东西可以发掘,也还有很多还没介绍的事物。比如Fiber(纤程,将在解析事件原理时提到),Enumerator(枚举器)等等。
原本还想写更多的(甚至想介绍元编和进一步解说单例类),但是我觉得到这儿已经很足够了。接下来的章节,我们会真正地进入RGSS3的系统解析。
届时,如果遇到了什么看不懂的(或者忘了)信息,还请再次回来参考属于基础理论篇的第一和第二章。
  



评分

参与人数 5星屑 +80 赞 +1 收起 理由
徐铭的葬礼 + 1 感谢,望继续
guoxiaomi + 15 塞糖
威风镰鼬 + 20 写得好详细/w\
狛尨鸮の传说 + 15 塞糖
唯道集虚 + 30 辛苦了,加油……

查看全部评分

回复 5 0

使用道具 举报

梦石
0
星屑
6891
在线时间
7027 小时
注册时间
2013-11-2
回帖
1263

开拓者剧作品鉴家

 楼主| 发表于 2016-12-31 22:54:52 | 显示全部楼层
本帖最后由 RaidenInfinity 于 2018-6-22 23:38 编辑

第三章 - 游戏主循环与呈现


3.1 游戏工程与启动

展开折叠

好,现在我们终于来到RGSS3的系统解析部分了。那么我们知道,要使用RPGMaker VX Ace开始制作一个游戏,我们就要创建新的工程。
新的工程长这个样子:


- Game.exe是游戏的主进程。运行这个软件就会正式地启动游戏。
- Game.ini定义了游戏主进程的标题,和加载的DLL等资讯。一般它长这个样子:

从上到下分别是:使用的RTP,运行库的位置,脚本文件的位置,游戏标题。
如果使用的RTP留空,那么这个游戏就等于脱离了RTP。
- Game.rvproj2就是工程文件,没啥特别的。可以用记事本打开来更改使用编辑器的版本,不过大部分情况下是不需要的。
- Graphics和Audio文件夹分别是存放图像素材和声乐素材的地方。我们可以直接把素材拖进去,不需要通过媒体库导入。
- Movies是存放影片素材的地方。RMVA只支持ogv格式的影片,而且是一播就到底。这是要注意的事情。
- Fonts文件夹默认是没有的,要自己创建。字体文件直接放进Fonts文件夹就好,游戏会自动读取的。
- System文件夹是存放RGSS300.dll(或RGSS301.dll)的地方。
- Data文件夹是存放所有rvdata2文件(包括数据库,地图,脚本)的地方。

RGSS3的中文全名是Ruby游戏脚本系统3。我们一开始就有提到,脚本就像戏剧的剧本,是给演员(解释器)看了后在舞台(游戏程序)上“表演”的。
Game.exe(游戏程序)在运行的时候,第一件事就是加载运行库(RGSS300.dll)和启动并初始化Ruby解释器。
如果游戏是在测试运行的情况下启动的,那么“测试模式”的标志(全局变量 $TEST)就会开启也就是被设置为真(true)。
那如果是战斗测试的情况下启动(编辑敌群数据库时的)呢?那么“战斗测试”的标志(全局变量 $BTEST)就会开启。
另外,如果在编辑器内勾了显示主控制台的设定,在测试运行时会显示主控制台。这背后的原理我们就不去深究了。

在这之后,Scripts.rvdata2里定义的脚本会被读取,然后从上到下一个个运行。在所有脚本都运行结束后,程序就会关闭。
我们可以用下列的伪代码(不是真正的代码,只是形容罢了)来比喻这个过程:
[pre lang="ruby"]
#开始游戏
脚本列表 = [脚本1, 脚本2, 脚本3 ... ]
脚本列表.each do |脚本|
    eval 脚本, TOPLEVEL_BINDING
end
#结束游戏
[/pre]
注意:TOPLEVEL_BINDING 是一个系统内置的常量。它储存着“上层作用域”的引用,让每个脚本的全局作用域都相等。
eval(注:忘了eval是拿来做啥的话,请看章节1.7)可以将它作为第二个参数递入,效果如下:
[pre lang="ruby"]
#没有用TOPLEVEL_BINDING
eval "a = 5"
eval "p a" #报错! 找不到方法或局部变量未定义!

#有用TOPLEVEL_BINDING
eval "a = 5", TOPLEVEL_BINDING
eval "p a", TOPLEVEL_BINDING #输出:5
[/pre]
那么结论是?就这么简单:脚本编辑器内的脚本从上面运行到下面。下面(新)的覆盖上面(旧)的。因为很重要所以要提很多次!

脚本编辑器内有个叫Main的部分,里面只有一行:

  
这行脚本是什么意思呢?我们来看看。rgss_main { SceneManager.run }
rgss_main是RGSS3的内置系统方法。它接受一个代码块(请参考章节2.9后部关于代码块的部分),并调用它。
在玩家按下F12键的时候,rgss_main将重置,游戏将从头开始运行。有些脚本会受重置影响而出错,因此并不建议使用F12重置游戏。

SceneManager是“场景管理器”而run是“运行”。SceneManager.run的调用就像一个“起搏器”,启动了游戏的“心跳”。
我们会在下一个部分仔细说明这个“心跳”,也就是游戏的主循环。





3.2 游戏主循环

展开折叠
这可以说是本教程最重要的部分,所以请多加留意,尝试理解这个部分的信息。

人之所以能存活,是因为心脏不停地跳动,让血液的循环能够正常进行。但是如果血管因为各种原因而狭窄了,血液输送的速度就慢了下来。
这种情况下,心脏需要用更大的力气来把血液泵出去,造成血压高亢的现象。

同样地,游戏的运行也是有一个“主循环”在跑动。只要脱出了这个循环,游戏就结束了。如果遇到了负担很重的地方(尤其是绘制),循环进行的速度就会慢下来。
如果循环在1秒内达不到60圈(RMVA默认帧率是60帧/秒),那么就会出现掉帧的情况,造成肉眼可见的卡顿。
这个循环是怎样的呢?造成主循环负担大而运行缓慢的因素有哪些呢?这就是这个部分的重点了。

我们在上一个部分见到了RMVA游戏的“起搏器”:SceneManager.run,也就是SceneManager(场景管理器)模块的run方法。
现在,让我们来看看,这个方法到底是什么样的吧。

首先我们看到,run方法的第一行是DataManager.init。这个方法又是拿来干啥的呢?让我们看看它长什么样子:

DataManager(数据管理器)的这个方法是用于加载数据库。加载后的数据会被存在全局变量里,如角色数据存在$data_actors里。
此外,它也创建了游戏对象的实例,如Game_Map的实例会被存在$game_map里。

在这之后,我们要注意的是SceneManager.run的第三和第四行。假设不是战斗测试而是普通游戏运行的话,这两行是:
[pre lang="ruby"]
@scene = Scene_Title.new #创建Scene_Title(标题场景)的实例然后把它存在实例变量@scene里面。
@scene.main while @scene #只要@scene里存着的不是nil(空值),就反复调用@scene.main
[/pre]
好,那么这个while,可以说是促使循环的关键了。我们先来看看,如何结束游戏?直接调用系统方法exit?不。
最好的方法是调用SceneManager.exit。这个方法只有一行内容,就是 @Scene = nil 。
只要把@scene设置为nil,SceneManager.run里的while循环就会脱出,然后迎来的就是游戏的结束。

既然是while循环,这个main会调用几次呢?很多很多次?不。让我们继续看下去。
我们来看看Scene_Title的main方法。不过我们得记起一个很重要的事情:面向对象编程的特征之一是继承,而我们也在章节2.3B见过场景的继承树。
因此,如果一个方法在子类内找不到,就得找父类,或者父类的父类,一直到它的根源为止(甚至还得查混入的模块)。
Scene_Title里面有main方法吗?没有。那么要怎么办?找它的父类,Scene_Base。因为Scene_Title < Scene_Base。
于是Scene_Base里定义的main方法是这样的:

嘿,又是一个循环!这次是until (反过来的while)。update until scene_changing? 。
scene_changing? 方法是什么呢?这个方法的定义是:SceneManager.scene != self 。也就是判断场景是否已更换。
SceneManager.scene是SceneManager里面的@scene变量,透过定义的方法来读取。
所以我们可以判断的是,只要当前场景还没更换,场景的update方法就会一直被调用。这是真正的心跳啊。

那么如果场景更换了呢?那么场景更新的循环就会停止了,然后执行结束的方法(pre_terminate然后就是terminate)。
然后呢?会发生什么事情呢?那么这个场景的main方法就算是执行完毕了。
如果此时SceneManager.scene是nil,那么游戏就会结束。否则,新场景的main方法就会被调用,毕竟SceneManager.run有 @scene.main while @scene 这个循环嘛。

让我们看这张游戏主循环的示意图:


start是“开始”,主要是各场景创建窗口之类的对象的地方。
post_start是“开始之后”,一般是执行场景之间的渐变。
update是“更新”,所有游戏更新逻辑都在这儿。
pre_terminate是“结束之前”。一般用于呈现地图或战斗场景等结束时的淡出效果。
terminate是“结束”,主要用于释放图像对象(如Sprite和Window。这些会在图像呈现的部分说到)。

这些代表了一个场景的生命周期,而游戏会一直“存活”到没有场景(设置@scene = nil)了为止。
那么话说回来…为什么游戏结束不是直接调用exit(Ruby的系统方法之一,用于退出程序)而是要将SceneManager的@scene设置为nil啊?
很明显啊。如果直接退出的话,退出游戏的淡出效果不就没了吗?还有pre_terminate和terminate这两个方法还没执行啊!

在这章,我们暂时不提场景的其他内容(如start和terminate)。既然是要说主循环,我们就要说update这个方法。
Scene_Base里面定义的,最基本的update是这样的:(其他场景类就算要覆盖也必须调用update_basic)
[pre lang="ruby"]
  def update
    update_basic
  end
[/pre]
而update_basic的内容是:


Graphics.update和Input.update是啥?图像引擎的更新还有输入机制的更新。
如果没有这调用两个方法会发生什么事呢?要不就画面卡住不刷新,要不就输入卡住(键盘没反应)。
Graphics.update内置帧数限制器。如果调用Graphics.update的速度过快(多于帧率,默认是60次/秒)的话,系统会自动执行暂停。
也就是说,如果调用Graphics.update,就等于等待1帧。不管在哪儿调用都是这样。

我曾经看过有人用Graphics.update循环来实现等待效果(比如等待一秒用 60.times{ Graphics.update })。
结果发生什么事呢?各种谜之卡顿。天气卡了,事件踏步动作卡了,动态图块(比如水面)卡了,键盘也给卡住了…
为什么?因为你只更新了画面,而没有更新其他的游戏逻辑,尤其是关于画面的。于是正确的姿势是什么呢?
这要看个别场景。比如如果是Scene_Battle(战斗场景),你会发现这场景有准备update_for_wait(为了等待而刷新)这个方法。
Scene_Battle里的update_for_wait,其实就等于调用了该场景的update_basic。

Scene_Battle里的update_basic不止用super关键词来继承了Scene_Base的原方法之外,还增添了数个功能。
什么刷新$game_troop(敌群,主要是事件的运行)啦,$game_timer(计时器)啦,@spriteset(图像组,这个在下个部分会说到)啦…
想想,如果你只调用了Graphics.update,而忽略了其他重要刷新方法的调用,会是多么可怕的事呢?

那么要如何等待?至少也要调用该场景的upate_basic。如果是从窗口或者其他对象刷新的话,请用以下的代码:
[pre lang="ruby"]
等待帧数.times{ SceneManager.scene.update_basic }
[/pre]
Scene_Battle提供了update_for_wait而Scene_Map则提供了update_for_fade。当然请参考这些类的定义,看原本是怎么实现等待的。

那么现在我们来看看Scene_Base里面update_basic方法的第三行:update_all_windows (更新所有窗口)。
没错,只要是窗口(Window),它都会去调用它的update方法。关于instance_variables和is_a?的信息,请参考章节2.8的内容。
因此,只要是实例变量(前缀@的变量)里面存着引用的窗口,不需要另外在update方法里面执行调用。
如果这么做的话,会造成一个窗口的刷新在一帧里面被调用两次!这是很大的错误!会造成意想不到的BUG!
不止是update(更新),连dispose(释放)也是这样。因此我们也不需要在场景的terminate(结束)方法里面放窗口的释放语句。

好,现在我们有一个问题:为什么要知道游戏主循环的原理?有什么用途?
游戏主循环把游戏的所有部件都串在一起。不管是场景,窗口,事件,人物还是数据。它们都互相在调用着,互相在影响着。
如果不知道一个方法是拿来干啥的,得用CTRL+SHIFT+F(有些输入法会受影响,遇上的话就请暂时用别的输入法)也就是全局搜索来找。
我们找出这个的定义和使用的地方之后,就会明白它所存在的意义,和它的功能。

除此之外,我们需要知道RMVA游戏脚本的运行是从上到下的一条线。不管是脚本列表,还是代码。
我们要想象出那条“线”,判断分歧或循环时会这条“线”会怎么弯怎么绕,然后调用方法时会跳去哪儿再跳回来,这么一件事。
学会追溯代码运行的流程,会对理解脚本,甚至是游戏运作的逻辑帮到很多。哪个先哪个后,什么情况之下会发生什么事情。

最后,我们需要知道是什么造成了主循环的缓慢和堵塞。
好,现在我们想象下,假设从一帧到下一帧所需要执行的代码是一条路,有一辆车在上面跑动,跑完一圈就是一帧。
这车跑的速度啊,和它的引擎马力有关系。用它来比喻电脑的话,就是硬件的配置强度,如处理器的威力。
如果这车驶完了一圈却还没到下一帧开始的时间,那么这辆车会在休息站休息一下再出发。(比喻Graphics.update的帧率控制)

如果你写了会造成很大负担的代码,比如进行繁琐的位图绘制,这条路就变得很长很长了。
这车呢,跑完一圈需要的时间,变得更长。如果车本来就慢(比较旧的电脑),很明显,跑完一圈的时间会超过帧率限制(默认是1/60秒)。
帧率掉了呢,你就会发现,哎呀怎么画面卡卡的。卡得不行,甚至移动都困难了。这当然困难了啊,别忘了输入的更新(Input.update)是每圈调用一次的呢。

所以要表达的是?不要对主循环造成负担。繁重的图像绘制(如绘制文字)尽量在玩家看不到的地方(比如场景切换之间的黑幕)执行。
如果非得要在游戏进行到一半时绘制(比如战斗时的HP槽),也得加个判定什么的,在真正有需要的时候进行绘制而不是每帧都去搞。
每帧都去绘制一次,你不掉帧算你电脑强,那电脑较弱的就只能写个惨字了,对吧。

除此之外,确定会造成卡顿的还有太多的并行事件,屏幕上太多的图像,还有地图上太多的事件。
为什么事件和并行事件会导致卡呢?目前你只需要知道它们会对主循环造成负担就好,因为我们在下一章的时候会娓娓道来。
那要怎么办呢?很简单,就是别干鸟事情啊。别搞太多并行事件,别在屏幕上搞太多图像。
那么地图上如果有太多的事件要怎么解决呢?市面上有许多“防卡脚本”。
防卡脚本的运作原理基本上是让屏幕外一段距离的事件不执行刷新。这就减少了主循环的负担,要判定事件页条件啊什么的。

那地图本身大,会不会卡呢?答案是:可能会,也可能不会。
如果是默认地图的话,是不会卡的。这是因为RMVA的默认地图只绘制屏幕上的部分,并在地图移动的时候进行重新绘制。
不过,如果有使用Overlay,ULDS等以贴图或图像方式将整个图片如远景般显示在屏幕上的脚本…
这不卡才奇怪了耶!越大越卡,越多层越卡!!!

目前确定会造成卡顿的主要有四种原因:
1. 图像相关,如位图绘制和显示极大或者很多的图像。(基本上有用到图像的地方都要考虑下卡的问题)
2. 并行事件太多,或者是地图上的事件太多。
3. 频繁读取或写入文件,尤其是容量大的文件。
4. 繁重的循环,如每帧10000次的迭代。不卡都奇怪系列。
繁重循环的例子:
[pre lang="ruby"]
a = 0
1000.times do |i|
    1000.times do |j|
        a += i + j #循环1,000,000次!!!
    end
end
[/pre]
然后你想象下,如果这东西是每帧都运行的…(当然这例子可能有点夸张)

只要尽量避免这些事项,就不会有卡的问题。
  




3.3 声乐呈现

展开折叠
用过RMVA的我们都知道一件事:如果游戏窗口不在活跃状态(切窗),画面会暂停但是声音不会。
这是为什么呢?这是因为RMVA的声乐呈现是在另一个线程上的。也就是说,在另一个分开的游戏循环。

RGSS3里声音的控制模块是Audio(声音),内有用于播放声音的方法。详细的方法列表可以参考F1帮助文档。
我们知道,RMVA有四种声音:BGM(背景音乐),BGS(背景声音),ME(一次性音乐),SE(一次性音效)。
声音文件可以用mp3, ogg, wav等格式,但是建议一律使用ogg,因为优秀的压缩能力(容量小),支持自定义循环和指定播放位置,而且也避免版权问题。
要知道阿,mp3它臭名昭著的地方就是那奇葩的版权阿。你只要是用mp3格式都得付版权费阿。怎么不花点时间转档去ogg呢?
除此之外还有一种叫做MIDI的声音文件。不过,我不建议使用MIDI,因为在新版本(Vista之后的)Windows里效率不佳。
那么有MIDI文件怎么办?找些软件把那个声音录制成ogg档呗。啥,你担心容量问题?拜托喔都什么年代了,网速几十M你居然还在烦恼这个?

透过Audio所播放的声音是在另一个线程进行的。也就是说,声音的播放与RMVA的游戏主循环无关。(要知道线程是啥可以去百度搜索)
因此,暂停声音基本上是别想了。要解决音画因为切窗而不同步的问题,能做的只有防止画面在切窗时暂停。

那么现在我们来看看Audio模块里面内置了什么方法,还有各种声音类型有什么不同吧!
首先是BGM(Background Music,背景音乐)和BGS(Background Sound,背景声音)。
BGM和BGS其实是差不多一样的东西,都会循环。BGM就是…我们熟悉的音乐啊。BGS是雨声还是嘀嗒声之类的循环效果。它俩是可以共存的。
我们先来看如何在脚本内播放BGM和BGS。这里我们引用F1帮助文档的内容。
[pre lang="ruby"]
#Audio.bgm_play(filename[, volume[, pitch[, pos]]]) (RGSS3)
#开始播放 BGM。依次指定文件名、音量、音调和起始位置。
#起始位置 (RGSS3) 只对 ogg 和 wav 有效。
#RGSS-RTP 内的文件也会自动搜索。扩展名可以省略。

#Audio.bgs_play(filename[, volume[, pitch[, pos]]]) (RGSS3)
#开始播放 BGS。依次指定文件名、音量、音调和起始位置。
#起始位置 (RGSS3) 只对 ogg 和 wav 有效。
#RGSS-RTP 内的文件也会自动搜索。扩展名可以省略。
[/pre]
好,那么bgm_play和bgs_play,就是我们要调用的方法。参数依次是:文件名,音量,音调,起始位置。
文件名是必须附上的参数。音量和频率是可选的,默认是各100。位置只对ogg和wav有效。默认是0,也就是从头开始。
文件名不需要扩展名,比如如果你有 Music.ogg ,你不需要写ogg,只需要"BGM/Music"就好。
音量是0到100之间的一个数字。频率是50到150之间(越少越沉,越大越尖)。这两个,我们可以在编辑器的声音测试窗里调试。
起始位置,并不是秒,而是和声音文件的采样速率(sample rate)有关。起始位置其实是说解压后音源的第几个字节。

比如我们要播放BGM文件夹里面的Music.ogg,我们可以这么写:
[pre lang="ruby"]
Audio.bgm_play("Audio/BGM/Music") #播放音乐,音量默认(100),音调默认(100),从头开始
Audio.bgm_play("Audio/BGM/Music", 80) #播放音乐,音量80,音调默认(100),从头开始
Audio.bgm_play("Audio/BGM/Music", 80, 90) #播放音乐,音量80,音调90,从头开始
Audio.bgm_play("Audio/BGM/Music", 100, 100, 23450) #播放音乐,音量100,音调100,从音源的第23450个字节开始
[/pre]
BGS也是一样的,只是方法名换成了bgs_play。

如果现在已经播着了一个BGM或者BGS,我们再播放一个新的BGM或BGS,会发生什么事情呢?
很简单。前一个BGM/BGS会立刻停止播放,而新的BGM会开始播放。
那么如果播放的是同一个BGM/BGS呢?(比如我播放了Music.ogg这个BGM,10秒后我再调用这个方法一次)
如过只是音量不同,那么播放的位置是不会变的,也不被起始位置影响。只有音量会根据设置而改变。
频率不同的话,BGM/BGS就会从指定的起始位置开始播放。

说完了播放,我们来说暂停。暂停BGM的方法是bgm_stop而暂停BGS的方法是bgs_stop。
[pre lang="ruby"]
Audio.bgm_stop #停止BGM播放
Audo.bgs_stop #停止BGS播放
[/pre]

如果直接停止不太悦耳,那么我们可以将BGM/BGS缓缓地淡出。这个方法是bgm_fade或者bgs_fade。
[pre lang="ruby"]
#F1汉化文档里的说明:
#Audio.bgm_fade(time)
#开始淡出 BGM。time 以毫秒为单位指定淡出需要的时间。

Audio.bgm_fade(3000) #在3秒内淡出BGM,注意:3秒 = 3000毫秒
Audio.bgs_fade(5000) #在5秒内淡出BGS
[/pre]

除此之外,我们也可以用bgm_pos或者bgs_pos来获取ogg或wav档的当前播放位置。注意:这个播放位置指的不是时间而是字节。
假设我们要在播放BGM的后的10秒后停顿BGM,然后在5秒后从停顿的地方继续播放:
[pre lang="ruby"]
Audio.play("Audio/BGM/Music", 100, 100) #播放BGM 音量100 音调100
600.times{ SceneManager.scene.update_basic } #等待10秒
pos = Audio.bgm_pos #记录位置
Audio.bgm_stop #停止BGM
300.times{ SceneManager.scene.update_basic } #等待5秒
Audio.play("Audio/BGM/Music", 100, 100, pos) # 播放BGM, 从记录的位置继续
[/pre]

那么BGM和BGS除了用途之外,还有什么不同点吗?这个我们得先看看ME的用途。
ME(Music Effect, 音乐效果)是一次性播放的声音,也就是不会循环。战斗结束的胜利音效就是ME的一种。
播放ME要调用Audio模块的me_play方法。参数与BGM/BGS相似,但是不能指定播放的起始位置。
[pre lang="ruby"]
#Audio.me_play(filename[, volume[, pitch]])
#开始播放 ME,依次指定文件名、音量和音调。
#RGSS-RTP 内的文件也会自动搜索。扩展名可以省略。
#播放 ME 时会暂停 BGM。BGM 重新开始的时机与 RGSS1 稍有不同。

#例子:播放ME文件夹里的Victory.ogg文件,以80%音量和110%的频率
Audio.me_play("Audio/BGM/Victory", 80, 110)
[/pre]
ME和BGM/BGS一样,都是无法重复的。新的ME会取代旧的ME,而如果相同的话,原理和BGM/BGS的一样(关于音量和频率的)。

ME可以用me_stop来停止,也可以用me_fade来淡出。基本上这和BGM/BGS的非常相似。
[pre lang="ruby"]
Audio.me_stop #停止播放ME
Audio.me_fade(4000) #4秒内淡出ME
[/pre]

在播放ME的时候, BGM会淡出而暂停。在ME停止播放的时候,BGM会恢复播放。BGS不会受ME的影响。
这就是BGM和BGS的主要分别。

最后一种声音是SE(Sound Effect,声音效果/音效)。很明显,这是一次性播放的声音,而且还可以重复播放。
当然为了保护玩家的耳朵,RGSS3系统内置了爆音防护,也就是如果重复播放同一音效的频率过高,系统会自动添加延迟。
另外,如果调用停止播放的话,所有音效都会同时暂停。播放和停止音效的方法分别是se_play和se_stop。参数和ME相似。
[pre lang="ruby"]
#Audio.se_play(filename[, volume[, pitch]])
#开始播放 SE,依次指定文件名、音量和音调。
#RGSS-RTP 内的文件也会自动搜索。扩展名可以省略。
#若是在极短时间内重复播放同一个 SE,会自动延长间隔以防出现爆音。

#5秒内每隔1秒就播放SE文件夹内名为"Knock.ogg"的音效,音量60%,音调70%
5.times do
    60.times{ SceneManager.scene.update_basic }
    Audio.se_play("Audio/BGM/Knock.ogg", 60, 70)
end

#停止音效播放
Audio.se_stop #让所有SE都停止播放。
[/pre]  
  
虽然我们知道,呼叫Audio模块的这些方法可以达成效果,但是我们最好不要直接调用Audio模块的方法。
为什么呢?因为RGSS3内置了声音文件的封装类,也就是RPG::AudioFile(声音文件)与继承了它的子类。(RPG::BGM, RPG::BGS, RPG::ME, RPG::SE)
"RPG::"是什么意思?到现在还在问这个问题的你,可能需要再看看章节2.4后面的部分。这些类都是RMVA数据结构的一份子。
那么在这儿也不多做介绍,因为F1帮助文档里面有说明和定义!请自行查看。

于是,播放声音的正确姿势是啥呢?是这样的:
[pre lang="ruby"]
#播放BGM
bgm = RPG::BGM.new("Sound", 100, 100) #注意:不需要前缀 Audio/BGM/,因为系统播放时会自己加
bgm.play #注意:可以附上播放位置的参数
#一次性的话,RPG::BGM.new("Sound", 100, 100).play 也行
[/pre]

RPG::AudioFile的子类主要被用于储存声音文件的数据(名字,音量,音调)。这些数据主要在系统设置,动画,行动路径等数据结构里使用。
因此如果你检视(用p)系统和动画等数据(比如p $data_system),你会看到里面有RPG::BGM,RPG::SE这些类的实例被储存在里面。





3.4 输入处理与图像缓存

展开折叠
  
A. 输入处理

在游戏主循环内,每帧都需要调用一次的方法除了更新屏幕绘制(和等待)的Graphics.update之外,还有一个就是Input.update 。
Input(输入)是掌管键盘或手柄操纵的模块。它是内置在RGSS300.dll里面的,因此我们看不到它的实际定义,只能调用已知的方法。
我们每帧都需要调用一次update(更新)来处理键盘或手柄输入的状态。

如果我们要写鼠标脚本或全键盘脚本,我们需要拓展Input.update(用alias的方法,施与猴子补丁)来放我们自己写的操纵控制系统的更新方法。
当然,在这里我们并不会说到如何写鼠标脚本或全键盘脚本。市面上的鼠标脚本和全键盘脚本大概已经很足够使用了,除非你想挑战自己。

好,那么我们知道,在玩RMVA游戏时,如果要更换键位的话,需要按F1打开设置窗,然后更改键位(手柄或键盘)。


键位可以自定义来代表 :A, :B, :C, :X, :Y, :Z, :L, :R 这八个按钮。为什么是这八个呢?这我们得看早期的游戏机。
很多人都没玩过世嘉土星(Sega Saturn)这款主机。实际上,如果我没去科普的话我也不知道。它的手柄长这个样:


右手有六个按钮,下排是A B C,上排是X Y Z。L和R当然在侧边是给食指按的了。
不过现在的手柄呢,一般右手键盘是四个按钮十字架的摆设,不管是XBOX的还是PS的手柄。
因此如果要设计给适合手柄玩的话,L和R当然是侧边的这个不用说了。Y和Z代表手柄上的Start和Select,而A,B,C,X分别代表四方,叉,圈,三角。

除此之外,上下左右这四个方向键的辨识符号分别是 :UP, :DOWN, :LEFT 和 :RIGHT。这些是无法改动的。
此外,电脑键盘还有 :SHIFT, :CTRL, :ALT, :F5, :F6, :F7, :F8, :F9 。这些符号代表键位本身,也无法改动。

按钮/键位符号列表:
[pre lang="ruby"]
按钮 -  :A, :B, :C, :X, :Y, :Z, :L, :R (可用F1设置来自定义键位)
方向键 -  :UP, :DOWN, :LEFT 和 :RIGHT
键位 -  :SHIFT, :CTRL, :ALT, :F5, :F6, :F7, :F8, :F9

游戏内默认的按钮/键位:
:C - 确认键 / 快进键
:B - 取消键 / 菜单键
:A - 跑步键
:L, :R - 菜单翻页键
:CTRL - 测试模式时按着CTRL则无视通行度移动
:F9 - 测试模式时按F9打开变量/开关检视界面
[/pre]
这些符号在调用某些Input模块的方法时将作为参数递入。

Input模块所提供的方法总共有5个,分别是 dir4(四方向), dir8(八方向), press?(是否按下), trigger?(是否重新按下), repeat?(是否重复)
先来说说dir4和dir8吧。它们是用于判断哪个方向键按下了,并按照方向返回一个数字。如果没有按下,会返回0。
每个数字代表着一个方向,除了5。请看小键盘的布局:
[pre lang="ruby"]
电脑小键盘(Numpad)的布局:
7 8 9
4 5 6
1 2 3
[/pre]
8是上,4是左,6是右,2是下。7是左上,9是右上,1是左下,3是右下。
很容易明白吧。所以呢,调用Input.dir4的时候,如果现在是右键按下了,就会返回6。如果有重复的方向键,哪个比较后按就是哪一个。
Input.dir8则判定了斜向的输入。如果左键和上键同时按下,那么就会返回7。右键和下键同时按下,就会返回3。如果只有下键,就返回2。
Input.dir4用于判定角色行动的方向,而Input.dir8并未被默认脚本使用。如果要写八方向行走脚本,那你可能需要用到这个方法。

Input的另外三个方法用途都很相似,就是判定单个按钮/键位的状况。既然后缀是?,那么很明显,返回的是true或者false。有就是true,没有就是false。
用法是 Input.press?(符号) ,Input.trigger?(符号) 或 Input.repeat?(符号)。符号请参考上面的按钮/键位符号列表。

Input.press? 的用途很简单,就是判定一个键是否被按下。只要它一直被按下,这个方法每次调用都会返回true。
Input.trigger? 就有点复杂。它的判定是一个键是否被重新按下。根据F1帮助文档的定义:从没有按下的状态转变为按下的瞬间才被视为「重新按下」
Input.repeat? 判定按键是否按下,但是不会每帧都返回true,而是有一个等待延迟(大概是15帧)。使用例子:Window_Selectable的选项操纵。

范例:
[pre lang="ruby"]
Input.press?(:CTRL) #判断CTRL键是否按下
Input.trigger?(:B) #判断B按钮是否重新按下,也就是到放开按键之前它只会返回1次true
Input.repeat?(:DOWN) #判断下方向键是否按下,每隔几帧返回一次true
[/pre]

B. 图像缓存

一个RMVA制作的游戏一般会用到很多图像素材。什么地图图块,战斗图,角色行走图…
这些素材都是作为JPG,PNG或BMP档案被储存起来的。在系统使用这些素材的时候,需要读取文件然后把图像作为位图载入内存里面。
忘了位图是啥?请再看看 3.3 图像呈现 的部分。那么读取文件和载入文件的这个过程是挺费时间的。尤其是配置比较低下的电脑上运行。
那么为了避免需要重复地载入图像,RMVA准备了一个模块,名叫Cache(缓存)。它的定义可以在脚本编辑器内见到。

默认的Cache模块一共有18个方法和1个实例变量。这个实例变量叫@cache,是一个哈希表。里面当然是存着已载入系统的位图。
Cache的前面12个方法是用于读取图像素材。我们不需要放后缀(.png等等),而且系统会从RTP里面找文件。找不到?报错啊。
以下是读取方法:
[pre lang="ruby"]
Cache.animation(文件名字, 色调) #用于读取动画素材(Graphics/Animations 文件夹),可以选择色调
Cache.battler(文件名字, 色调) #用于读取战斗图素材(Graphics/Battlers 文件夹),可以选择色调
Cache.battleback1(文件名字) #用于读取战斗背景1素材(Graphics/Battlebacks1 文件夹)
Cache.battleback2(文件名字) #用于读取战斗背景2素材(Graphics/Battlebacks2 文件夹)
Cache.character(文件名字) #用于读取角色行走图素材(Graphics/Characters 文件夹)
Cache.face(文件名字) #用于读取脸图素材(Graphics/Faces 文件夹)
Cache.parallax(文件名字) #用于读取远景图素材(Graphics/Parallaxes 文件夹)
Cache.picture(文件名字) #用于读取事件图片素材(Graphics/Pictures 文件夹)
Cache.tileset(文件名字) #用于读取地图图块素材(Graphics/Tilesets 文件夹)
Cache.title1(文件名字) #用于读取标题1素材(Graphics/Titles1 文件夹)
Cache.title2(文件名字) #用于读取标题2素材(Graphics/Titles2 文件夹)
[/pre]
动画和战斗图是比较特殊的,因为可以附上色调(hue)来获取不同的图像。当然,原本的那份也还是会存在缓存里面。
如果呼叫任何一个方法时没有附上名字(空的字符串),那么系统会返回32 x 32大小的一个空位图。

Cache模块实现缓存的原理是使用一个哈希表(@cache)。如果读取了一个图片,它会被缓存在哈希表里面,键就是它的路径(比如 Graphics/Animations/Sword5)。
如果有色调改变的话,这个键会是一个数组([路径, 色调])。缓存后的位图因为有了常驻的引用(在模块的实例变量内存着)而不会被当成垃圾被自动处理掉。
如果系统侦测到早已缓存了这个图片,而且这位图未被释放(disposed),那么系统会返回它的引用。
因为它作为缓存的作用,存着的位图是常驻的。也就是说,一直到游戏结束为止,它都会留在内存里面。

如果担心加载的图片太多而内存爆满,你大可定时清理一下Cache。如果清理?呼叫Cache.clear。所有存着的位图将会清空,然后GC(垃圾回收)将立刻启动。
不过,这种情况是不怎么会发生的,除非你搞很大的地图的远景贴图,而且贴了很多层,每层占十几MB的内存的那种情况。

好,那么我们现在知道了RMVA有这么一个缓存的机制。我们要注意什么呢?
第一,不要随意用Bitmap.new来读取文件创建位图,尤其是很频繁读取的文件。原因不就很明显嘛,缓存就是为了避免频繁读取啊。
第二,不要随意释放用Cache来读取的位图。这会导致很多很多问题,比如你释放了一个位图,其他图像精灵(Sprite)所引用的位图就全都没了。因为都是同一个啊。
第三,有些比较大的图,比如IconSet(图标组)的加载时间会比较长。也许你可以试下预载它,在游戏开始的时候就把它载入到Cache里去了。(用Cache.system("IconSet"))

最后,如果你要自己读取任何文件夹里面的图片,那么请用load_bitmap方法。参数有三个:文件夹名字,文件名字,色调(可省略,默认0)
[pre lang="ruby"]
#加载图片:"Graphics/Characters/Actors1.png"
bitmap1 = Cache.load_bitmap("Graphics/Characters/","Actors1")
#加载图片:"Graphics/Animations/Sword3.png",色调55
bitmap2 = Cache.load_bitmap("Graphics/Anmations/","Sword3",55)
[/pre]
  




3.5 数据库与档案存取

展开折叠

我们知道,在游戏开始运行的时候(SceneManager.run),会加载数据库和创建游戏对象。
这些是调用DataManager(数据管理器)模块的init(初始化,initialze的缩写)方法而达成的。
除了处理游戏数据的初始化之外,DataManager还负责处理新建游戏和存档/读档的操作。
这一个部分将对RMVA默认脚本里数据库和档案存取的流程作出解析。

A. 数据库

RMVA编辑器中的数据库有很多个分页,如角色、职业、敌人等等。
这些数据将储存在对应的档案里,如角色数据会储存在Actors.rvdata2里,敌人会存在Enemies.rvdata2里等等。
那么我们来看看DataManager里面的load_database方法是长什么样子的。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 读取数据库
  #--------------------------------------------------------------------------
  def self.load_database
    if $BTEST #如果是战斗测试模式
      load_battle_test_database #加载战斗测试模式数据库
    else #否则
      load_normal_database #加载数据库
      check_player_location #检查初始地图和位置是否有设置,没有的话就报错
    end
  end
[/pre]
我们就不提战斗测试模式了,毕竟说真的,自定义脚本很多的情况下,还是通常地运行游戏比较好。
那么加载数据库的方法是load_normal_database。这个方法长这个样子:
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 读取普通的数据库
  #--------------------------------------------------------------------------
  def self.load_normal_database
    $data_actors        = load_data("Data/Actors.rvdata2") #角色
    $data_classes       = load_data("Data/Classes.rvdata2") #职业
    $data_skills        = load_data("Data/Skills.rvdata2") #技能
    $data_items         = load_data("Data/Items.rvdata2") #物品
    $data_weapons       = load_data("Data/Weapons.rvdata2") #武器
    $data_armors        = load_data("Data/Armors.rvdata2") #防具
    $data_enemies       = load_data("Data/Enemies.rvdata2") #敌人
    $data_troops        = load_data("Data/Troops.rvdata2") #敌群
    $data_states        = load_data("Data/States.rvdata2") #状态
    $data_animations    = load_data("Data/Animations.rvdata2") #动画
    $data_tilesets      = load_data("Data/Tilesets.rvdata2") #图块组
    $data_common_events = load_data("Data/CommonEvents.rvdata2") #公共事件
    $data_system        = load_data("Data/System.rvdata2") #系统
    $data_mapinfos      = load_data("Data/MapInfos.rvdata2") #地图资讯
  end
[/pre]  
我们知道rvdata2是RMVA数据的格式。但是…它是怎么储存数据的呢?load_data(读取数据)又是什么呢?
load_data是RGSS3的一个内置全局方法。有读取肯定就有储存。save_data(储存数据)是它的伙伴。

根据F1帮助文档,load_data和save_data的定义是这样的:
[pre lang="ruby"]
load_data(filename)
#加载由 filename 指定的数据文件并还原成对象。
#例子:
$data_actors = load_data("Data/Actors.rvdata2")

#这个函数基本上与下面的语句相同:
File.open(filename, "rb") { |f|
  obj = Marshal.load(f)
}
#不同之处在于,本函数可以从加密档案内加载数据文件。

#save_data(obj, filename)
#将对象 obj 写入名为 filename 的数据文件。
#例子:
save_data($data_actors, "Data/Actors.rvdata2")
#这个函数等价于:
File.open(filename, "wb") { |f|
  Marshal.dump(obj, f)
}
[/pre]
在这里我们看到了几个陌生的模块:File(文件)和Marshal(整顿)。它们都是Ruby的內建模块。
File主要是处理文件的读取和写入,而Marshal用于将Ruby对象打包成二进制数据。
我们先不探讨文件要怎么读取或者怎么写入(那是档案存取部分的事),而只需要知道一件事:
rvdata2文件里面存着的是Ruby对象。

那么到底存着什么呢?我们来试试看在load_normal_database的最后面p一下$data_actors里存着的东西。
[pre lang="ruby"]
module DataManager
        class << self
                alias :old_load_normal_database :load_normal_database
                def load_normal_database
                        old_load_normal_database
                        p $data_actors
                end
        end
end       
[/pre]
以上的代码作为插件脚本在新工程内运行的结果:(注意:为了方便,角色的数量已减为4个)


哇!好乱啊!不过仔细看的话,其实它是个数组。(如果用 p $data_actors.class 的话会获得Array,证实它是个数组)
它的第一个(索引为0的)元素是nil(空值),而后面的四个元素是RPG::Actor(角色数据结构)的实例。
RPG::Actor是什么呢?请打开F1帮助文档的“RPGVXAce 数据结构”部分查看。

从这个实验我们能知道什么呢?
第一,$data_actors是一个数组。它的大小是数据库内角色的最大数量 + 1(空的也算!)
第二,$data_actors的第一个元素(索引0)是nil,而从索引1开始的元素是RPG::Actor的实例,里面存着角色的资料。
第三,我们可以用角色ID作为$data_actors的索引来获取对应的数据对象。(如5号角色就$data_actors[5])

除了$data_system(它直接就是RPG::System的实例了)之外的数据全局变量都是如此的,装着数据对象的数组。
如果我们需要遍历这个数组的话,请绝对要记得要加nil检测,因为第一个元素是nil。
[pre lang="ruby"]
$data_actors.each do |actor|
        next if !actor #nil检测
        #要运行的代码
end       
[/pre]

那么既然我们知道数据库加载的原理了,有什么需要注意的东西吗?当然有!
我们不应该在游戏进行时对数据库(如$data_actors)做任何改动。
如果有什么需要改变的,比如额外素质加点什么的,都必须作为实例变量存在对应的对象里面(如Game_Actor也就是角色类的对象)。
这很明显,游戏数据库是作为一个“参考”。如果你更改了数据库,新建的游戏存档被这个改动影响,这就不好了。
那么什么情况下,我们可以改动数据库呢?就是游戏初始化的时候,透过alias别名对load_database施与猴子补丁来做出改动。

如果我们要增添或删除数据,该怎么办呢?删除的话,就算了吧。你没这个必要的。增添的话,是可以的。
比如我要给技能(RPG::Skill)增加一个属性:HP消耗。那么我就黑进去RPG::Skill给它增添这个attr_accessor。
[pre lang="ruby"]
class RPG::Skill
        attr_accessor :hp_cost #HP消耗
end       
[/pre]
但是这里有个问题。读取和写入方法建是建了,但是数据要怎么放进去啊?而且还没在initialize里初始化呢!读取时不会报错吗?
这个你就得靠你自己的想象力了。你要黑initialize来加个初始化(@hp_cost = 0),可以。你要用注释的方式来读取数据,可以。
本教程在这里的示范是用each迭代遍历加哈希表的方式来达成目标。
[pre lang="ruby"]
module DataManager

        SKILL_HP_COST = {5 => 20, 7 => 15, 8 => 30} #5号技能消耗20HP,以此类推
        SKILL_HP_COST.default = 0 #设置默认值为0,也就是如果哈希表内没有那个键,就返回0

        class << self
                alias :old_load_database :load_database
                def load_database
                        old_load_database
                        skill_hp_cost
                end
               
                def skill_hp_cost
                        $data_skills.each do |skill| #遍历$data_skills
                                next if !skill #nil检测
                                skill.hp_cost = SKILL_HP_COST[skill.id] #设置技能HP消耗
                        end       
                end
               
        end
end       
[/pre]

通过脚本,我们可以把数据库把玩在手中。什么新的变量,甚至新的数据类型(在脚本内直接定义)都能搞出来。
毕竟,学脚本就是为了把自己天马行空的想法实现出来啊。

B. 档案存取

存档和读档是RM游戏中非常重要的一环。
想当年8位游戏机还没有存档机制的时候啊…要继续游戏都是要输入一堆密码的。有够麻烦。
那么除了从菜单页面手动存档以外,我们还可以想出各种存档机制。什么自动存档啦,存档点啦,都行。
在这个部分,我们会了解到RMVA的默认档案存取机制,以便对它做出改动,也可以避免一些误区。

DataManager(数据管理器)模块除了处理数据库的加载之外,还负责新建游戏,存档和读档的操作。
我们先来看看新建游戏的机制是怎样的。从在标题菜单按下“开始游戏”的那一刻开始。
  
Scene_Title(标题场景)的command_new_game(开始游戏指令)方法,用途就如其名:
(注意:我们在这儿只讨论默认的机制,不考虑跳过标题页面直接进入地图的状况)
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 指令“开始游戏”
  #--------------------------------------------------------------------------
  def command_new_game
    DataManager.setup_new_game #初始化一个新的游戏
    close_command_window #关闭选项窗口
    fadeout_all #淡出画面与BGM
    $game_map.autoplay #自动切换BGM/BGS
    SceneManager.goto(Scene_Map) #移动到地图场景
  end
[/pre]
简单来说,开始游戏指令按下去后,DataManager模块的setup_new_game(初始化新游戏)方法会被调用。
在这之后的关闭选项窗和画面、音乐淡出都是装饰性的效果。
最后,我们需要让地图的BGM和BGS播放,然后移动到地图场景。

DataManager模块的setup_new_game方法长这个样子:
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 设置新游戏
  #--------------------------------------------------------------------------
  def self.setup_new_game
    create_game_objects #再次生成各种游戏对象
    $game_party.setup_starting_members #初始化队伍
    $game_map.setup($data_system.start_map_id) #设置开场地图
    $game_player.moveto($data_system.start_x, $data_system.start_y) #移动玩家角色到初始坐标
    $game_player.refresh #刷新玩家角色对象
    Graphics.frame_count = 0 #重置帧数(游戏时间)
  end
[/pre]
啥?游戏开始运行(DataManager.init)时生成了游戏对象,设置新游戏时又再生成各种游戏对象?
没错。这有几种原因。逻辑上来说,你创建新的游戏你当然是要有新的对象的吧。这没问题。
还有一个比较重要的原因就是,在刚返回标题之际,上一个游戏的对象都还留在内存里面。
如果不再次生成来初始化的话,会造成“继承”了上一个游戏的状态的巨大问题。

一般上我们是不怎么需要担心开始游戏的机制的,除非是跳过标题直接到地图画面(事件标题啧)。
我个人极度不支持事件标题的使用,除非真的有必要(角色在地图上行走,调查物件来作为选项)。
这种情况下,开始新游戏时,该事件里面的脚本指令需要参考上述的两个方法来安排。
不过最好是画面完全淡出之后才来调用DataManager.setup_new_game,以避免画面方面的问题。




RMVA的游戏存档机制非常直接,基本上就是直接刻录下去。
为什么这么说呢?我们直接来看看,在存档菜单里面,选好档位按下确认后,会发生什么事情。
以下的方法都是Scene_Save(存档场景)里面的:
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 确定存档文件
  #--------------------------------------------------------------------------
  def on_savefile_ok
    super #摆美用的
    if DataManager.save_game(@index) #如果存档成功 *注意:@index是档位,从0开始
      on_save_success #执行存档成功时的处理
    else #否则
      Sound.play_buzzer #播放失败音效
    end
  end
  #--------------------------------------------------------------------------
  # ● 存档成功时的处理
  #--------------------------------------------------------------------------
  def on_save_success
    Sound.play_save #播放存档音效
    return_scene #返回上一个场景(菜单或者地图)
  end
[/pre]
如果存档成功,就播放存档音效和返回上一个场景。如果失败则播放失败音效。
啥?存档会失败的啊?会的,这个我们待会儿会讨论到。

那么从以上的代码我们知道的是,DataManager模块里面的save_game方法是用于存档的。
它接受的参数是档位(从0开始,意思就是1代表二号档位,以此类推),返回的是存档成功与否。
我们直接来看看save_game是长什么样子的:
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 执行存档
  #--------------------------------------------------------------------------
  def self.save_game(index)
    begin #开始抓错
      save_game_without_rescue(index) #实际的存档方法(注:这个方法会返回true)
    rescue #如果出错,不报错而是执行以下的代码
      delete_save_file(index) #删除存档文件
      false #返回false(代表失败)
    end
  end
[/pre]  
哦,章节1.5里面提到的begin-rescue-end抓错机制用在这儿了啊。
没错,如果存档失败,游戏是不会报错退出的,而是只删除该档位的档案,然后告知失败。
这对玩家来说,也许是个好事,但是对制作者来说却不是。很明显,如果你没看到报错,你怎么修正呢?
所以啊,如果你发现你制作中的游戏出现了无法存档的情况,那么你该做的就是先关闭抓错机制。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 执行存档
  #--------------------------------------------------------------------------
  def self.save_game(index)
    #begin
      save_game_without_rescue(index)
    #rescue
    #  delete_save_file(index)
    #  false
    #end
  end
[/pre]
把关于抓错的部分用#给注释掉,就能恢复存档失败时的报错,得知到底是什么出了问题。

好,现在我们来看看实际的存档方法(save_game_without_rescue)是长什么样子的吧。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 执行存档(没有错误处理)
  #--------------------------------------------------------------------------
  def self.save_game_without_rescue(index)
    File.open(make_filename(index), "wb") do |file| #开启档案
      $game_system.on_before_save #存档前处理
      Marshal.dump(make_save_header, file) #储存档头
      Marshal.dump(make_save_contents, file) #储存档身
      @last_savefile_index = index #给存档菜单设置档位位置
    end
    return true #成功时返回true(失败就…直接报错了)
  end
[/pre]
哇咧!好复杂的样子!嘛,其实这不太复杂。一个个方法去看它们的定义就行了。

我们先来看看$game_system(Game_System的实例)的on_before_save方法是什么玩意儿。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 存档前的处理
  #--------------------------------------------------------------------------
  def on_before_save
    @save_count += 1 #给存档次数的记录喜+1
    @version_id = $data_system.version_id #记录游戏的版本标识码
    @frames_on_save = Graphics.frame_count #记录游戏进行的帧数(用于计算游戏时间)
    @bgm_on_save = RPG::BGM.last #记录当前BGM
    @bgs_on_save = RPG::BGS.last #记录当前BGS
  end
[/pre]  
啥?什么?存档次数?版本标识码?VA有这个功能?从来没听过啊!!是的,所以现在你就知道了。

有些RM游戏有不存档一命通关就有额外成就或者隐藏剧情或者什么好东西的。
这个功能实现的方法是检查Game_System的@save_count实例变量($game_system.save_count)。
此外,每当你在RMVA编辑器储存一次工程,系统数据(System.rvdata2文件,$data_system对象)的version_id实例变量会改变一次。
version_id实际上是一个乱数,而它的功能只在提醒此存档是别的版本(注意:储存工程就算更新版本了)造出来的。
至于要怎么用嘛…嗯,请继续读下去咯。

现在我们来看看什么是make_save_header(创建档头)。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 生成存档的头数据
  #--------------------------------------------------------------------------
  def self.make_save_header
    header = {} #创建空的哈希表
    header[:characters] = $game_party.characters_for_savefile #储存角色行走图资料
    header[:playtime_s] = $game_system.playtime_s #储存游戏时间
    header #返回哈希表
  end
[/pre]  
Game_Party的characters_for_savefile和Game_System的playtime_s这两个方法的定义可以自己去查询。
反正这些资料是用于在存档/读档场景里面显示的。
如果你要显示更多的资讯,比如第几章第几节,地图到哪儿,金钱有多少,储存脸图资料…
都可以更改这个方法来增添想加入的头数据。当然,对应的读取方法和档案窗上的显示你也得调整一下。

生成了档头,当然要生成档身。make_save_contents就是如此的方法。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 生成存档内容
  #--------------------------------------------------------------------------
  def self.make_save_contents
    contents = {} #创建空的哈希表
    contents[:system]        = $game_system #储存Game_System的实例(系统)
    contents[:timer]         = $game_timer #储存Game_Timer的实例(计时器)
    contents[:message]       = $game_message #储存Game_Message的实例(讯息)
    contents[:switches]      = $game_switches #储存Game_Switches的实例(公共开关组)
    contents[:variables]     = $game_variables #储存Game_Variables的实例(公共变量组)
    contents[:self_switches] = $game_self_switches #储存Game_SelfSwitches的实例(独立开关组)
    contents[:actors]        = $game_actors #储存Game_Actors的实例(角色组)
    contents[:party]         = $game_party #储存Game_Party的实例(队伍)
    contents[:troop]         = $game_troop #储存Game_Troop的实例(敌队)
    contents[:map]           = $game_map #储存Game_Map的实例(地图)
    contents[:player]        = $game_player #储存Game_Player的实例(玩家角色)
    contents #返回哈希表
  end
[/pre]
在这儿列出的是存档里会储存进去的数据。整个对象就这样直接刻录进去文件里面了。
为什么要知道这些呢?因为你如果要一个变量被存入存档内,它就必须存在上列的对象中的其中一处。
当然如果你要加入别的对象的话,你是可以更改make_save_contents来加入你要储存的对象的。
除此之外,我们也需要注意什么东西无法被储存所以不可以在上列的实例里面有它的引用。

生成了档头和档身之后,就是将它们存入文件内的时刻了。让我们再一次看看save_game_without_rescue的定义。
*重要的地方注释处被标上了箭头。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 执行存档(没有错误处理)
  #--------------------------------------------------------------------------
  def self.save_game_without_rescue(index)
    File.open(make_filename(index), "wb") do |file| #开启档案 <----------
      $game_system.on_before_save #存档前处理
      Marshal.dump(make_save_header, file) #储存档头 <----------
      Marshal.dump(make_save_contents, file) #储存档身 <----------
      @last_savefile_index = index #给存档菜单设置档位位置
    end
    return true #成功时返回true(失败就…直接报错了)
  end
[/pre]
什么是File.open?File就是文件。open就是打开。很明显,File.open就是打开文件。
make_filename(index)是什么?make_filename是生成档案的文件路径。递入的index参数是档位。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 生成文件名
  #     index : 文件索引
  #--------------------------------------------------------------------------
  def self.make_filename(index)
    sprintf("Save%02d.rvdata2", index + 1) #生成文件名/路径的字符串
  end
  #注意:如果忘了sprintf是什么,请参考章节1.7
[/pre]  
默认的存档名字是SaveXX.rvdata2(XX是档位)。比如如果index是0(一号档位),那么存档名就是Save01.rvdata2。
存档的位置默认是游戏文件夹。如果你想要有一个单独的文件夹(比如Saves文件夹),或者想要换下文件名,那么你可以改下这个方法。

那么File.open的第一个参数就是文件的路径。第二个参数,是打开的模式。
在这里就不一一介绍打开的模式了,因为你大可去百度搜索科普一下,或者去查RubyDoc之类的资料网。
"wb"指的是二进制模式写入。w是write(写入)的缩写,b是binary(二进制)的缩写。合起来就是wb。
所以我们可以知道,File.open(make_filename(index), "wb") 是以二进制(而不是字符串)写入的模式打开存档文件。
注意:写入模式时如果文件不存在,就会创建新的文件。而且,使用w(写入)模式打开文件会清空文件内的数据。

那么我们打开了一个文件,用完了就得关闭它啊。有open(打开)总得有close(关闭)是吧。
这是没错的,不过我们可以用代码块来省略这个步骤,让系统自动关闭打开中的文件。
[pre lang="ruby"]
#以下的这段代码
file = File.open(文件名, 打开模式)
执行操作
file.close #关闭文件

#和以下这段代码基本上是一样的
File.open(文件名, 打开模式) do |file|
        执行操作
end        #代码块执行完毕后会自动关闭文件

#注意:还记得 { } 形式的代码块吗?这也是行的!
File.open(文件名, 打开模式){|file| 执行操作 }
[/pre]

那么知道了打开文件的机制之后,是时候看看写入的数据是什么了。
执行存档的方法最重要的是这两行:
[pre lang="ruby"]
      Marshal.dump(make_save_header, file) #储存档头 <----------
      Marshal.dump(make_save_contents, file) #储存档身 <----------
[/pre]          
我们在说到数据库时看过Marshal.load(被RGSS3包装成load_data全局方法)。现在,我们要看的是它的相反,Marshal.dump。
Marshal是“整顿”模块。它的功能是把一个对象以一种特殊的方式打包成一段二进制数据,和从那段数据解包成对象。
当然实际上我们可以自定义打包的方式,但是此教程不会深入地讨论这个部分。有兴趣的话可以去科普marshal_dump这个自定义方法。

Marshal.dump接受两个参数。第一个就是要打包的对象,第二个是要写入的文件。这非常明显。
Marshal.dump打包对象的时候会把它所有的实例变量的实例变量的实例变量的…反正会追溯的底,全部都打包成数据就对了!
如果遇到无法打包的事物呢?报错咯。什么东西不能打包呢?在本部分的最后我们会说到的。别急别急。

那我们看到执行存档的方法里面有两次Marshal.dump。啥?这…可以的啊?不会出问题吗?不会。
你Marshal.load的顺序就等于你Marshal.dump的次序。请看以下的范例:
(注意:因为关系到文件读取/写入,请在电脑上的解释器,如VA游戏本身测试运行)
[pre lang="ruby"]
a = 444
b = 666
c = 888
#写入
File.open("Test.rvdata2","wb") do |file|
        Marshal.dump(a, file)
        Marshal.dump(b, file)
        Marshal.dump(c, file)
end
File.open("Test.rvdata2","rb") do |file|
        p Marshal.load(file) #444
        p Marshal.load(file) #666
        p Marshal.load(file) #888
end
[/pre]

看到这儿,你大概已经明白RMVA的默认存档机制了吧。存的都是些啥,要如何去存。




既然存了档,那肯定要读取的啊。储存的英文是save,读取的英文是load。
让我们先来看看,在读档场景选好档位按下确认键时,会发生什么事情。
Scene_Load(读档场景)里面的on_savefile_ok(其实这是读档)方法如下:
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 确定读档文件
  #--------------------------------------------------------------------------
  def on_savefile_ok
    super #摆美用
    if DataManager.load_game(@index) #如果读档成功
      on_load_success #执行读档成功时的处理
    else #否则
      Sound.play_buzzer #播放失败音效
    end
  end
  #--------------------------------------------------------------------------
  # ● 读档成功时的处理
  #--------------------------------------------------------------------------
  def on_load_success
    Sound.play_load #播放读档音效
    fadeout_all #淡出画面和音乐(定义请看Scene_Base)
    $game_system.on_after_load #执行读档后的处理
    SceneManager.goto(Scene_Map) #移动到地图场景
  end
[/pre]  
这和存档场景的类似,只是读档成功之后的处理稍微不同。
Game_System的on_after_load方法如下:
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 读档后的处理
  #--------------------------------------------------------------------------
  def on_after_load
    Graphics.frame_count = @frames_on_save #设置游戏进行帧数(对应on_before_save的 @frames_on_save = Graphics.frame_count)
    @bgm_on_save.play #播放BGM (对应on_before_save的 @bgm_on_save = RPG::BGM.last)
    @bgs_on_save.play #播放BGS (对应on_before_save的 @bgs_on_save = RPG::BGS.last)
  end
[/pre]

DataManager里面的档案读取分为两个部分。一个是读取档案头(load_header),另一个读取档案身(load_game)。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 执行读档
  #--------------------------------------------------------------------------
  def self.load_game(index)
    load_game_without_rescue(index) rescue false
  end
  #--------------------------------------------------------------------------
  # ● 读取存档的头数据
  #--------------------------------------------------------------------------
  def self.load_header(index)
    load_header_without_rescue(index) rescue nil
  end

与存档相似,index就是档位索引。0代表第一档位,1代表第二档位,以此类推。
它们也有rescue的功能来防止在出问题时报错退出,所以如果要检查报错讯息,得注释掉rescue和之后的代码。

我们先来看看load_header_without_rescue方法的定义是怎样的。
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 读取存档的头数据(没有错误处理)
  #--------------------------------------------------------------------------
  def self.load_header_without_rescue(index)
    File.open(make_filename(index), "rb") do |file| #以rb(二进制打开)模式打开文件
      return Marshal.load(file) #第一次Marshal.load存档时,获得的是档头
    end
    return nil #读档出错:返回nil
  end
[/pre]
wb是write binary(写入,二进制)的意思。那么rb就是read binary(读取,二进制)。
读取和写入不同的地方是,如果文件不存在,读取可是会出错的!毕竟文件不存在的话你读啥嘛你说是吧。
这个方法的功能很明显,就是打开文件,然后Marshal.load读取存档的头数据。
获取之后会怎样呢?请自己去看调用它的地方。给个提示:Window_SaveFile(档案窗口)。

第一次Marshal.load,获得的是档头。那么第二次,获得的就是档身了。第三次…哪儿有第三次?当然是报错啊。
所以load_game_without_rescue(和其调用的方法)是这样的:
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 执行读档(没有错误处理)
  #--------------------------------------------------------------------------
  def self.load_game_without_rescue(index)
    File.open(make_filename(index), "rb") do |file| #打开存档
      Marshal.load(file) #跳过档头(因为Marshal.load第二次才是档身)
      extract_save_contents(Marshal.load(file)) #读取并展开档身
      reload_map_if_updated #如果数据已更新则重载地图
      @last_savefile_index = index #设置上次存档的索引
    end
    return true #成功则返回true
  end
  #--------------------------------------------------------------------------
  # ● 展开存档内容
  #--------------------------------------------------------------------------
  def self.extract_save_contents(contents) #嘛,基本上就是存档的相反咯
    $game_system        = contents[:system]
    $game_timer         = contents[:timer]
    $game_message       = contents[:message]
    $game_switches      = contents[:switches]
    $game_variables     = contents[:variables]
    $game_self_switches = contents[:self_switches]
    $game_actors        = contents[:actors]
    $game_party         = contents[:party]
    $game_troop         = contents[:troop]
    $game_map           = contents[:map]
    $game_player        = contents[:player]
  end
  #--------------------------------------------------------------------------
  # ● 如果数据已更新则重载地图
  #--------------------------------------------------------------------------
  def self.reload_map_if_updated
    if $game_system.version_id != $data_system.version_id #如果存档版本标识码不同于系统数据
      $game_map.setup($game_map.map_id) #重新初始化地图
      $game_player.center($game_player.x, $game_player.y) #设置显示位置为地图中央
      $game_player.make_encounter_count #设置下一次遇敌的步数
    end
  end
[/pre]  
为什么版本标识码不同就要重载地图呢?原因很简单。比如你改了一些事件,要重开游戏来测试下。
读取存档的时候,如果不重载地图,而角色又在那个地图内的话,事件的改动是不会奏效的。你必须再次进入地图才能看到更改后的事件。
所以就有了这个重载地图的机制来解决这个问题。




了解了新建游戏,存档和读档的机制之后,就可以进入进一步的阶段了。
我们将了解如何巧妙地运用存档、读档机制,和要避免的一些问题。

之前一直强调过,有些对象是无法实用Marshal.dump来打包进文件的。什么对象不行呢?很简单。自己去测试不就知道了吗?
好好好,开玩笑的。我个人解决过的,无法存档的问题,大部分牵涉到图像相关的对象。(如Bitmap, Sprite)

以下是确认无法被Marshal.dump处理的对象类型:
[pre lang="ruby"]
Module Class MatchData IO File Enumerator ThreadGroup Mutex Fiber
Proc Method UnboundMethod Bitmap Viewport Sprite Tilemap Plane Window Font
[/pre]
尝试Marshal.dump这些类的实例时,会出现TypeError类型错误,一般讯息为:no marshal_dump is defined for class 类的名字
(意思就是marshal_dump方法未定义)

因此,我们必须避免这些类的实例出现在会被储存的对象的引用链(就是实例对象的实例对象的实例对象的…直到尽头)里面。
我看过有人把Sprite(图像精灵)给存在Game_Battler(战斗人员)实例的其中一个实例变量里面。说真的,这一点也不好。(说的就是某RTAB CP制脚本!)
如果正好Sprite因为一些因素而没被dispose(释放)掉呢?(其实这也是个巨大的问题)爆裂吧!!世界!!!就是这样。

不过我们需要特别注意RMVA其中一个游戏对象类的定义,就是Game_Interpreter(事件解释器)。
Game_Interpreter里面使用到了Fiber(纤程)的实例。关于Fiber,我们在下一个章节会认识到,不过我们现在只需要知道,它无法被Marshal.dump处理。
那么默认脚本是怎么样排除无法储存的问题呢?那就是自定义打包/解包。我们直接来看Game_Interpreter内的定义:
[pre lang="ruby"]
  #--------------------------------------------------------------------------
  # ● 储存实例
  #    对纤程进行 Marshal 的自定义方法。
  #    此方法将事件的执行位置也一并保存起来。
  #--------------------------------------------------------------------------
  def marshal_dump
    [@depth, @map_id, @event_id, @list, @index + 1, @branch]
  end
  #--------------------------------------------------------------------------
  # ● 读取实例
  #     obj : marshal_dump 中储存的实例(数组)
  #    恢复多个数据(@depth、@map_id 等)的状态,必要时重新创建纤程。
  #--------------------------------------------------------------------------
  def marshal_load(obj)
    @depth, @map_id, @event_id, @list, @index, @branch = obj
    create_fiber
  end
[/pre]
Marshal.dump在处理一个对象的时候,如果该对象有定义marshal_dump,那么系统会转为处理这个方法返回的对象。
Marshal.load则是对应marshal_load方法。如果没有定义呢?当然是默认的打包算法了。
等等等等,这就奇怪了。既然没有自己定义marshal_dump就是用默认的打包法,那为啥Bitmap啊Sprite这些类会说找不到方法呢?
这也许是因为它们用了undef(注销方法,请参考章节2.9)来告诉系统,我们就是不要这个方法。
当然如果你想要的话,你也可以自己再定义一次这两个方法。截图存档脚本就用了这个技巧来储存Bitmap和Font。

等等等等,我没看错吧?“此方法将事件的执行位置也一并保存起来"??
是的,你没看错。如果你是在事件里面调用脚本指令来进行存档的(比如储存到一号档位是DataManager.save_game(0))…
那么会发生什么事呢?让我们来看看范例:


实际运行并触发这个事件,嗯存档,是没问题的。就…存档啊。成功了就显示“存档成功”啊。没问题。
然后,我们来重开游戏读取一下…what the…嗯!咳!为啥啊?为啥突然跳出来一个“存档成功”啊?

这是因为Game_Interpreter(作为Game_Map, Game_Troop, Game_Interpreter的实例变量)在储存的时候…
它是会记录事件的执行位置的!what the 嗯!什么鬼!那要怎么办才不在读取后出现这鬼东西啊!
解决方法嘛,请直接看范例:


当然你熟悉了原理的话,也许你可以弄一个更加精辟的也说不定。
简单来说,就是用一个不会被存档储存的全局变量来作为暂时的开关。(范例里的$save)
通过判断这个“开关”,我们可以决定是否要显示存档成功的讯息。
为啥要跳转标签呢?原因嘛,你自己移除掉测试下不就知道了呢。
不过这还是有点问题,就是如果该事件在对话时改变朝向,那么读取的那一刻,它是还朝着玩家的(然后转回去)。
这会显得非常奇怪,所以啊,能避免就避免呗。我个人是将这个机制用在公共事件(自动存档后通知玩家)。

除了要注意事件运行位置之外,地图上事件的位置是不被储存的。
也就是说呢…重开游戏再次读档后,甚至是换个地图再回来(都不用存档读档啊啊啊!)…什么?事件又回到本来的位置了!
解决的方法嘛…就是储存目标事件的坐标,然后在进入地图的时候设置坐标。事件或脚本都是行的。

很多人问过,怎么防止玩家使用傻瓜存档修改器来作弊啊?嘛,说真的,作弊的话,玩家想要作弊他什么方法都能想得出。
而且嘛,不管你做了多少防护措施,只要玩家可以拆到你的脚本出来看,你什么加密什么防护都是假的。
不过…假设你真的可以把脚本保护好…要保护所有rvdata2相关的数据是非常简单的事情。
加密的方法有很多。什么AES加密啦,XOR加密啦…(不懂就科普)不过这里就给一个虽然很挫但是至少可以防傻瓜存档修改器的法子。
我们知道存档Marshal.dump了两次。第一次是存档头,第二次是存档的内容。
反过来要读取的话,Marshal.load第一次是存档头,第二次是存档内容。
如果我们在存档机制里面增加几次Marshal.dump,然后在读档机制里加几次Marshal.load呢?
[pre lang="ruby"]
module DataManager
  #存档
  def self.save_game_without_rescue(index)
    File.open(make_filename(index), "wb") do |file|
      $game_system.on_before_save
          Marshal.dump("解包穷三代",file) #额外的Marshal.dump
      Marshal.dump(make_save_header, file)
          Marshal.dump("作弊毁一生",file) #额外的Marshal.dump
      Marshal.dump(make_save_contents, file)
      @last_savefile_index = index
    end
    return true
  end
  
  #读取存档内容
  def self.load_game_without_rescue(index)
    File.open(make_filename(index), "rb") do |file|
          Marshal.load(file) #额外的Marshal.load
      Marshal.load(file)
          Marshal.load(file) #额外的Marshal.load
      extract_save_contents(Marshal.load(file))
      reload_map_if_updated
      @last_savefile_index = index
    end
    return true
  end

  #读取存档头
  def self.load_header_without_rescue(index)
    File.open(make_filename(index), "rb") do |file|
          Marshal.load(file) #额外的Marshal.load
      return Marshal.load(file)
    end
    return nil
  end  
end
[/pre]
当然,这只是个范例。我们可以运用想象力来做各种不同的存档打包/解包机制,从而抵御存档修改器。
不过最好的方法嘛…还是用心地做游戏,少为这种破玩意儿费心神吧。




我们已经知道了如何读取文件和将数据写入文件。现在,要知道的是如何删除文件和检查文件是否存在。
File类提供了两个类方法:delete和exist?。这两个方法都接受一个参数:文件路径。
[pre lang="ruby"]
#检查文件是否存在
a = File.exist?("ABC.rvdata2") #如果找到就是true,没有就是false
#删除文件
File.delete("ABC.rvdata2") if a #如果文件存在就删除文件
[/pre]
如果文件不存在,删除文件是会报错的,就和读取文件差不多。(未找到文件的错误)
因此,在读取和删除文件前,请务必用File.exist?来确认文件是否存在,或者用rescue来抓错/避免报错退出。

如果我们要批量检查一系列的文件是否存在,我们可以用Dir.glob方法。(Dir类的glob类方法)
Dir.glob接受一个文件配对的字符串,并以数组的方式返回所有找到的文件名。如果没有呢?数组就是空的了。
我们可以参考DataManager里面的save_file_exists?方法:
[pre lang="ruby"]
  def self.save_file_exists?
    !Dir.glob('Save*.rvdata2').empty? # *就是任意字符串
  end
[/pre]

知道了File.open/delete/exist?和Marshal.load/dump这些方法,我们可以自创数据文件,来放一些全局性的数据。
全局变量脚本知道吧?学着自己写啊亲。不过嘛,有些判断性的机制,只需要创建空文件就可以了的。
比如我自家的坑曾经有过一个机制,是判定是否通关然后开启特典地图的。这是使用了一个名为"clear.rvdata2"的空文件,在游戏通关时创建。
在标题场景的时候,如果侦测到了这个文件,就开启特典的选项。所以啊,脚本,就是那么地神奇。
  
[/fold]


回复 1 1

使用道具 举报

梦石
0
星屑
6891
在线时间
7027 小时
注册时间
2013-11-2
回帖
1263

开拓者剧作品鉴家

 楼主| 发表于 2016-12-31 22:55:18 | 显示全部楼层
本帖最后由 RaidenInfinity 于 2017-5-5 12:52 编辑

第四章 - RGSS3图像系统


图像可以说是RM游戏脚本里非常非常重要的一环。
我们收集到的背景,战斗界面,事件行走图等图像素材,要把它“贴”到游戏里实装应用。
那么在这个部分,我们将过目一下RM中图像呈现的概念和尝试在游戏内显示想要的图像。
这也许是整个教程里最长最复杂的部分,毕竟这是非常非常重要的一环,也会影响接下来的章节。
不过,只要仔细阅读并理解其中的概念,这会对相关脚本的理解和创作起到很大的帮助。

4.1 颜色

展开折叠
  
什么是颜色?颜色是我们的眼睛对光的一种视觉感应。红橙黄绿蓝靛紫粽黑白灰…各种颜色。
在现实,我们可以用天然或合成的化学物质来调配出各种颜色。比如红色颜料加上蓝色颜料就能调配出紫色。
电脑的显示屏幕则是用红,绿,蓝这三种“原色”来混合成各种颜色的差分。
*维基百科内对三原色的解释:光的三原色是红色、绿色和蓝色,三种光相加会成为白色光。
  

因此,在程序内,每个颜色都可以由 红,绿,蓝 这三种原色比例的多寡混合出来。
这种颜色混合的模式叫做“三原色光模型”,简称RGB颜色模型或红绿蓝颜色模型。
一般我们会用0到255为刻度以决定原色的比例。在这里用Paint Tool SAI的调色盘来演示:
  
从上图我们可以看到,三原色都为255(满)的时候,混合出来的颜色是纯白色。
三原色都为0(空)的时候,就是纯黑色。三原色比例一致但未满,就是不同幅度的灰色。

  
那么红色比例满,其他为空的时候,很明显是纯红色。红绿满,蓝色空,就是纯黄色。
用不同的颜色比例,可以混出不同的颜色。这是非常有趣的事情。

除了红绿蓝三种原色的比例之外,颜色还可以有一个“不透明度”(opacity)的数值。
什么是“不透明度”?字面上的意思。如果这个数值满,就是完全地不透明。
如果不透明度为空的话(完全透明),就根本连显示都不显示出来了。

不透明度的用途是指定当两个颜色(背景色,前景色)相叠的时候,该如何混合成实际显示的颜色。
  
不透明度在英文里还有一个专用名词,叫做alpha值。以后看到"alpha"的时候别慌,这就是不透明度。
再提醒一下:不透明度/alpha值在满的时候完全不透明,空的时候完全透明。

在PS和SAI等绘图软件里面,不透明度一般是以百分比来显示和调整的。50%是一半,100%是满。
但是在RM里面,不透明度和颜色一样,是用整数来表示的。0到255。255是满,0是空。

因此,我们可以得出,在RM里面,记录颜色的数据是由:红,绿,蓝,不透明度 这四个数值组成的,简称RGBA格式。
R是Red(红色), G是Green(绿色), B是Blue(蓝色), A是Alpha(不透明度)。每个数值用0到255之间的整数来表示。

  
如果我们要把上图里面的颜色(包括不透明度)输入RM,怎么办呢?
我们来看看啊…红是176,绿是80,蓝是200,不透明度是80%。80 / 100 * 255 =  204,所以不透明度是204。
那么我们知道,这个颜色用RGBA格式来表示的话是:176, 80, 200, 204。

RM里面储存颜色数据的类叫Color。Color就是颜色的英文嘛。(注:这是美式英文的拼法。英式的是Colour)
创建一个Color类的对象,需要3或是4个参数,依序是RGBA(红,绿,蓝,不透明度)。
完全不透明的颜色(A为255)则可以省略第四个参数。低于0或是超出255的数值会被修正成0或是255。
假设我们有两个颜色:一个是完全不透明的纯红色(255, 0, 0, 255),还有一个就是刚才的例子(176, 80, 200, 204)。
[pre lang="ruby"]
#Color的创建方法是:Color.new(红, 绿, 蓝) 或者 Color.new(红, 绿, 蓝, 不透明度)

@color1 = Color.new(255, 0, 0) #不透明度255的话可以选择省略这个参数
@color2 = Color.new(176, 80, 200, 204)

p @color1 #输出:(255.000000, 0.000000, 0.000000, 255.000000)
p @color2 #输出:(176.000000, 80.000000, 200.000000, 204.000000)

#注1:在RGSS3,Color.new可以不需要附上任何参数以创建数值为(0, 0, 0, 0)的颜色。
p Color.new #输出:(0.000000, 0.000000, 0.000000, 0.000000)

#注2:使用p显示的颜色数值后面有6位小数点,这是因为这个类的inspect方法被修改成了这个样子。
#关于inspect方法的说明请参考本教程的章节1.7和2.5。
[/pre]

在创建了一个Color对象之后,我们可以对颜色的数值作出读取和修改。
红色是red, 绿色是green, 蓝色是blue, 不透明度是alpha。
[pre lang="ruby"]
@color = Color.new(176, 80, 200, 204) #创建颜色对象
p @color.red #输出:176.0
p @color.green #输出:80.0
p @color.blue #输出:200.0
p @color.alpha #输出:204.0

@color.blue = 99 #修改蓝色值
p @color #输出:(176.000000, 80.000000, 99.000000, 204.000000)
[/pre]

嗨呀,如果要修改颜色的话,一个个设置是不是太麻烦了些啊?是的。很麻烦。
因此,Color类提供了set(设置)这个方法,可以一次过设置完所有数值。
[pre lang="ruby"]
@color = Color.new(176, 80, 200, 204) #创建颜色对象
p @color #输出:(176.000000, 80.000000, 200.000000, 204.000000)
@color.set(255, 255, 255, 128) #设置所有数值
p @color #输出:(255.000000, 255.000000, 255.000000, 128.000000)

#除此之外,我们也可以提供一个Color对象作为参数,复制它的数值
@color1 = Color.new #创建所有数值为空的颜色对象
p @color1 #输出:(0.000000, 0.000000, 0.000000, 0.000000)
@color1.set(@color) #将@color的数值复制进去@color1
p @color1 #输出:(255.000000, 255.000000, 255.000000, 128.000000)
[/pre]

Color类到底有什么用呢?非常有用。字体颜色,HP条的颜色,甚至是窗口皮肤的颜色啊!
了解RM里颜色的概念之后,我们就可以继续看下去了。










回复

使用道具 举报

梦石
0
星屑
6891
在线时间
7027 小时
注册时间
2013-11-2
回帖
1263

开拓者剧作品鉴家

 楼主| 发表于 2016-12-31 22:56:46 | 显示全部楼层
本帖最后由 RaidenInfinity 于 2017-5-5 13:05 编辑

施工现场 - 4.2 位图,图像精灵,矩形

位图(Bitmap),又称点阵图,是使用像素阵列(Pixel array)来表示的图像。
什么是像素?像素就是图像显示的基本单位。一个点,一个颜色。
像素的英文叫Pixel。这是由图片(Picture)和元素(Element)的英文单词混合而成的。
那么我们可以将RM里面的位图理解成一个存着像素的二维数组。
如果不记得什么叫二维数组,那就请拉上去再看一次章节1.6。

像素也是位图大小的单位。假设我们有一个宽32像素,高32像素的图片,那么这个图片里面就有1024个像素。(32 * 32 = 1024)
再提醒一次:每一个像素也就是每个点都有一个颜色。
  

为什么我们要知道这个呢?这是非常重要的基础概念。图片越大,图片内像素的数量就越多,占的内存也就越大。
当然,对这个位图的操作(会在另一个部分里解说),需要的运算时间也就越长。
不过,现在我们暂时不深入讨论这些关于效率的问题。

在RMVA,创建位图(Bitmap)的方式有两种:
1. 从文件中读取,在解码后载入内存
2. 创建空的位图,全部像素的颜色值都是 (0, 0, 0, 0)
目前,我们只看前者。后者将在“位图操作”的部分进一步说明。

RMVA的F1帮助文档对Bitmap创建的说明是这样的:
[pre lang="ruby"]
Bitmap.new(filename)
载入由 filename 参数所指定的图像,生成一个 Bitmap 对象。

RGSS-RTP 和加密档案中的文件会自动搜索。扩展名可以省略。
[/pre]

简单来说就是用 Bitmap.new(文件名字) 来执行位图载入。
系统会自动寻找RTP文件,而且会读取加密档案。如果找不到怎么办呢?当然是报错啦!找不到文件的错误没看过吗?
还有一种情况下会报错,就是内存不足。(错误讯息是位图创建失败)内存不足怎么办呢?
一就是电脑该换了,二就是你载入了太多图片,内存撑不住了。

比如如果我要载入RTP里史莱姆的战斗图,要怎么办呢?首先,游戏的图像素材是存在Graphics文件夹里的。这我们都知道。
图像文件夹里又区分很多个文件夹。战斗图是Battlers。史莱姆是Slime.png。文件格式不需要加,所以Slime就好了。
因此,要载入这个图片,我们要这么写:
[pre lang="ruby"]
@bitmap = Bitmap.new("Graphics/Battlers/Slime")
[/pre]
如果游戏未脱离RTP的话,系统会自动读取RTP的文件,所以能正常运行。

位图既然是图片,那么当然有宽度和高度这两个属性。分别用width和height这两个方法来获取。
啥?你想要修改?不行。位图创建了之后,宽和高是不能再修改的。
[pre lang="ruby"]
@bitmap = Bitmap.new("Graphics/Battlers/Slime")
p @bitmap.width #输出:136
p @bitmap.height #输出:47
[/pre]

如果我不想要这个位图了,也没有重复使用的可能性,怎么办?放着让它被垃圾回收机制处理吗?
RGSS3的Bitmap,说白了就是一个包裹着一段载入内存的位图的对象。
垃圾回收机制(参考章节2.7)的确可以处理它,但是我们不知道这个机制什么时候才会工作。
如果放任不管的话,什么时候内存满出来,出什么问题也说不定。
因此,Bitmap类提供了一个方法:dispose(释放),来手动清除位图所占用的内存,并弃用这个对象。
在释放之后,我们可以用disposed?(是否已释放)方法来检查这个位图的状况。
[pre lang="ruby"]
@bitmap = Bitmap.new("Graphics/Battlers/Slime")
p @bitmap.disposed? #输出: false
@bitmap.dispose
p @bitmap.disposed? #输出: true
[/pre]
如果一个Bitmap对象已经被释放,那么再次呼叫dispose是没有效果的。




现在你可能在纳闷:图片是加载了啊,可是怎么在屏幕上显示呢?
这么形容吧。如果屏幕是个墙壁,位图是个相片,那么我们需要一个相框来把相片挂上去展示。
RGSS3自带的这个“相框”名叫Sprite。我个人将它译名为“图像精灵”。(Sprite在英文的意思是精灵)
维基百科对Sprite的解释是:包含于场景中的二维图像或动画。“包含于场景中”是个重点。
假设现在你有一个Bitmap,存着你要在屏幕上显示的图片。你总得告诉系统,你要把这个图片挂到屏幕上的哪个地方啊,对吧?
Sprite的用途就是给系统指定如何显示一个Bitmap,提供显示的坐标,比例,角度,色调等参数。

创建Sprite的方式也有两个,不过目前我们只介绍第一种,没有参数的创建方法:Sprite.new 。
在创建一个Sprite之后,我们要修改它的bitmap变量,给它指定一个图片(Bitmap对象)让它显示。
注意:在下面的大部分关于图像显示的范例里面,我们会调用RGSS3內建的rgss_stop函数来定住画面,以免受到默认游戏系统的影响。
F1帮助文档对rgss_stop的解释:
[pre lang="ruby"]
rgss_stop
停止脚本的执行,只保留画面的刷新。

等效于下面这一句:
loop { Graphics.update }
[/pre]

让我们直接尝试吧!将史莱姆的战斗图显示到屏幕上!
[pre lang="ruby"]
@bitmap = Bitmap.new("Graphics/Battlers/Slime") #载入史莱姆的战斗图
@sprite = Sprite.new #创建新的Sprite对象
@sprite.bitmap = @bitmap #指定要显示的Bitmap

rgss_stop #停止所有脚本的运行,画面定格
[/pre]

效果:
  

哇哦哦!显示出来了!不过,为啥史莱姆在左上角啊?这当然啊。又没指定坐标,默认的位置就是左上角。

RGSS3(和大部分2D游戏)所使用的坐标系统是从左上角开始的。啥,你不知道什么是坐标?问问百度吧。
  
画面的左上角是原点。在这个位置,X(横)轴和Y(直)轴的值都是0。嗯,得记住了!横的叫X!直的叫Y!
X值越大,图片就越靠右,反过来就是靠左。Y值越大,图片就越靠下,越小则靠上。

Sprite的显示坐标调整的方式是给x和y这两个变量赋值。
我们直接来看范例和其效果:
[pre lang="ruby"]
@bitmap = Bitmap.new("Graphics/Battlers/Slime") #载入史莱姆的战斗图
@sprite = Sprite.new #创建新的Sprite对象
@sprite.bitmap = @bitmap #指定要显示的Bitmap

@sprite.x = 200 #设置X值
@sprite.y = 100 #设置Y值

rgss_stop #停止所有脚本的运行,画面定格
[/pre]

效果:(附上说明)
  
X和Y都可以是负值,也可以超出屏幕的宽度和高度。想要知道效果的话,可以自己尝试调整。
回复 1 0

使用道具 举报

梦石
0
星屑
6891
在线时间
7027 小时
注册时间
2013-11-2
回帖
1263

开拓者剧作品鉴家

 楼主| 发表于 2017-5-5 10:17:20 | 显示全部楼层
本帖最后由 RaidenInfinity 于 2017-5-5 12:47 编辑

未完成,编辑中




第五章 - 事件指令与解释器





回复

使用道具 举报

梦石
0
星屑
6891
在线时间
7027 小时
注册时间
2013-11-2
回帖
1263

开拓者剧作品鉴家

 楼主| 发表于 2017-5-5 10:17:52 | 显示全部楼层
本帖最后由 RaidenInfinity 于 2017-5-5 10:18 编辑

附录:常见词汇


这里列出的词汇,不需要全部都背起来。遇到不懂的词语时,可以把这个部分当做字典来科普。当然,更好的方法还是去网上搜索。

基本词汇说明
点击打开
运算符(operator) - 加减乘除之类的符号,带有一定的意义。
变量(variable) - 装着数值的一个容器。有数种类型。
常量(constant) - 同上,但是一经定义则不可再改动。也指直接写在脚本内的数值。
关键字(keyword) - 在编程中有重要意义的单词。不能直接做为变量/常量的名字。
表达式(expression) - 一组运算符,变量,或常量,最后会被运算而得到一个值。
语句(statement) - 交给程序运行的一行指令。Ruby内默认为1行为1句。用;(分号)可以将几个语句放在同一行里。
函数(function) - 一个大型程式中的部分程式码,负责完成一项特定的工作。一般可以传特定的值给函数处理,以获得传回来的结果。
呼叫(call) - 字面上的意思,就是呼叫一个函数,把参数递过去让它处理了,等它把结果传回来。
参数(parameter) - 呼叫函数时传递过去的值。
实际参数(argument) - 函数成功获取并处理的值。
类(class) - 蓝图/模板一般的数据构造,描述着所创建的对象共同的属性和方法。
方法(method) - 类里面的函数一般被称为方法。
调用(method call) - 方法的呼叫一般被称为调用。
对象(object) - 类的实例,可以说是一个“根据蓝图(类)创造出来的物件”
参考(reference) - 对象的记忆体地址,可以理解成“这个物件放在什么地方”
实例(instance) - 指根据蓝图(类)创造出来的一个独一无二的“例子”。
模块(module) - 方法和常量的组合。


常见关键词
点击打开
class (类定义)
module (模块定义)
def (方法定义)
alias (别名定义)
if (如果)
else (不然)
elsif (不然如果)
end (终止)
then (那就)
unless (除非)
true (真值)
false (假值)
nil (空值)
not (不是)
and (和)
or (或者)
begin (开始)
rescue (抢救)
ensure (确保)
case (事例)
when (等于时)
do (做)
for (迭代/对于)
break (跳出)
next (下一个)
redo (重来)
retry (重试)
while (循环/...的时候)
until (一直到)
return (返回)
super (替代)



常见运算符
点击打开
+ (加/正值)
- (减/负值)
* (乘)
/ (除)
% (取余)
** (乘方)
= (赋值)
<< (左位移)
>> (右位移)
~ (位辅数)
& (位和)
| (位或)
^ (位异或)
== (逻辑等于)
=== (特殊等于测试)
<=> (联合比较运算)
!= (逻辑不等于)
=~ (正则表达等于)
!~ (正则表达不等于)
< (小于)
> (大于)
<= (小于或等于)
>= (大于或等于)
! (逻辑否)
&& (逻辑和)
|| (逻辑或)
( ) (括号)
[ ] (元素引用)
[ ]= (元素赋值)
? : (条件表达)
:: (常量解析)
. (属性解析)
.. (范围包含)
... (范围不包含)
注意:这里没有列出优先度,想知道的话请自行科普。


内建函数,数据库结构
请自行阅读F1帮助文档。

以下为默认脚本里面常见到的词汇,就是类或者模块的名字,和常用到的全局变量。

模块
点击打开
Vocab (用语) – 存着部分游戏文字的模块。
Sound (声音) – 存着数据库音效定义的模块。
Cache (缓存) – 掌管图像素材导入和保存的模块。
DataManager (数据管理) – 数据库和游戏实例的管理器,掌管游戏初始化和存档/读档。
SceneManager (场景管理) – 场景切换的管理器。游戏的“心跳起搏器”。
BattleManager (战斗管理) – 战斗过程的管理器。掌管战斗胜负奖励,战斗BGM播放等事物。


游戏对象
点击打开
Game_Temp (暂存管理器) – 处理临时数据的类。主要处理公共事件运行,和储存场所移动时的淡出类型。
Game_System (系统管理器) – 处理系统附属数据的类。处理菜单禁用,储存地图BGM,游戏进行时间等资料。
Game_Timer (计时器) – 处理事件“计时器操作”指令的类。
Game_Message (信息管理器) – 处理信息窗口状态、事件文字显示、选项指令等的类。
Game_Switches (公共开关组) – 处理公共开关的类。本质是个套了壳的Array(数组)。
Game_Variables (公共变量组) – 处理公共变量的类。本质是个套了壳的数组。
Game_SelfSwitches (独立开关组) – 处理地图事件独立开关的类。本质是个套了壳的Hash(键值表)。
Game_Screen (游戏画面) – 处理地图/战斗场景的更改色调,画面闪烁,天气,图片等操作。
Game_Picture (图片) – 主要管理事件“显示图片”指令中使用的图片。
Game_Pictures (图片组) – 就如其名,本质是储存图片的数组。在Game_Screen内部使用。
Game_BaseItem (物品基底) – 技能、物品、武器、护甲的统一类。主要用于角色装备,菜单/战斗指令记忆。
Game_Action (行动) – 处理战斗中的行动的类。主要处理战斗人员在该回合内的选用的指令和目标等资讯。
Game_ActionResult (行动结果) – 战斗行动结果的管理类。主要处理行动的命中,造成的伤害,增减状态等资讯。
Game_BattlerBase (战斗人员基底) – 管理战斗人员的基本类。处理能力值计算和状态效果。
Game_Battler (战斗人员) – 处理战斗人员的类。主要处理战斗图,位置,技能伤害,HP/MP/TP等数据。
Game_Actor (己方角色) – 管理己方角色的类。处理己方角色的装备,技能,职业,等级,经验值等数据。
Game_Enemy (敌方角色) –管理敌方角色的类。主要处理敌方角色的行动(使用的技能)。
Game_Actors (己方角色组) – 包装己方角色数组的外壳。基本上和其他的“组”都差不多。
Game_Unit (队伍基底) – 管理游戏单位(队伍)的类。主要处理列出队员,随机选择目标之类的操作。
Game_Party (己方队伍) – 管理己方队伍的类。金钱,物品,队伍特效是在这儿处理的。
Game_Troop (敌人队伍) – 管理敌群和战斗相关资料的类,也可执行如战斗事件管理之类的功能。
Game_Map (当前地图) – 管理地图的类。地图的卷动,通行度之类的都是在这里处理的。
Game_CommonEvent (公共事件) – 管理公共事件的类, 主要处理并行公共事件的运行。
Game_CharacterBase (人物基底) – 管理地图人物的基本类。处理坐标,图片,通行等数据。
Game_Character (人物) – 管理地图人物的类。主要处理人物的移动路径。
Game_Player (玩家人物) – 处理玩家人物的类。处理玩家操纵的人物相关的事物,比如移动指令,事件启动。
Game_Follower (跟随角色) – 管理跟随角色的类。处理跟随角色的显示、跟随的行为等。
Game_Followers (跟随角色组) – 包装跟随人物的数组的外壳。基本上和其他的“组”都差不多。
Game_Vehicle (载具) – 管理载具的类。如果载具不在搭乘中,它会被移动到地图外面。
Game_Event (事件) –处理事件的类。拥有条件判断、事件页的切换、并行处理、执行事件等功能。
Game_Interpreter (事件解释器) – 事件指令的解释器。里面定义着各事件指令的脚本实现。


图像类
点击打开
Sprite_Base (基础动态精灵) – Sprite(图像容器,译为精灵)的拓展,主要处理动画播放。
Sprite_Character (角色精灵) – 主要处理角色(玩家角色,事件,载具)的行走图显示。
Sprite_Battler (战斗图精灵) – 主要处理敌人战斗图的闪烁等效果。默认己方角色的战斗图是空的。
Sprite_Picture (图片精灵) – 主要处理事件“显示图片”指令中所使用的图片。
Sprite_Timer (计时器精灵) – 主要处理计时器启动时在地图右上角的显示。
Spriteset_Weather (天气精灵组) – 当前地图场景所使用的图像组,处理下雨等天气效果的绘制。
Spriteset_Battle (战斗精灵组) – 战斗场景所使用的图像组,处理战斗背景,战斗图等的生成和更新。
Spriteset_Map (地图精灵组) – 当前地图场景所使用的图像组,处理地图,远景,角色,事件等的生成和更新。


窗口类
点击打开
Window_Base (窗口基底) – 所有窗口的基底(父类)。处理描绘文字,显示图标等显示内容的方法。
Window_Selectable (选项窗) – 所有和选项(光标移动,卷轴滚动)有关的窗口的基底类。
Window_Command (指令窗) – 带有指令选择功能的窗口的基底类。
Window_HorzCommand (横向指令窗) – 同指令窗,但是是横向的。
Window_Help (帮助文字窗) – 显示技能和物品等的说明、以及角色状态的窗口。
Window_Gold (金钱显示窗) – 显示金钱数量的窗口。
Window_MenuCommand (主菜单指令窗) – 就如其名。
Window_MenuStatus (主菜单队伍状态窗) – 主菜单场景中显示队伍成员状态的窗口(右边)。
Window_MenuActor (主菜单选择目标窗) – 显示物品使用或技能使用的选择目标的窗口。
Window_ItemCategory (物品类型选择窗) – 在道具,商店场景内,选择道具类型的窗口。
Window_ItemList (物品列表窗) – 在道具场景内显示持有物品的窗口。
Window_SkillCommand (技能类型选择窗) – 技能画面中,选择指令(特技/魔法等)的窗口。
Window_SkillStatus (技能场景角色状态窗) – 技能画面中,显示技能使用者状态的窗口。
Window_SkillList (技能列表窗) – 技能画面中,显示技能列表的窗口。
Window_EquipStatus (装备场景角色状态窗) – 装备画面中,显示角色能力值变化的窗口。
Window_EquipCommand (装备指令窗) – 技能画面中,选择指令(更换装备/最强装备/全部卸下)的窗口。
Window_EquipSlot (角色当前装备窗) – 装备画面中,显示角色当前装备的窗口。
Window_EquipItem (装备列表窗) – 装备画面中,显示可替换装备的窗口。(下面)
Window_Status (角色基本信息窗) – 状态画面中,显示角色基本信息的窗口。
Window_SaveFile (档案窗) – 存档画面和读档画面中显示存档文件的窗口。
Window_ShopCommand (商店指令窗) – 商店画面中,选择买入/卖出的窗口。
Window_ShopBuy (商店购买窗) – 商店画面中,买入时显示所有商品的窗口。
Window_ShopSell (商店贩卖窗) –商店画面中,卖出时显示所有商品的窗口。
Window_ShopNumber (商店数量窗) – 商店画面中,输入“物品买入/卖出数量”的窗口。
Window_ShopStatus (商店角色状态窗) – 商店画面中,显示“物品持有数”和“角色装备”的窗口。
Window_NameEdit (名字编辑窗) – 名字输入画面中,上面的,编辑名字的窗口。
Window_NameInput (名字输入窗) – 名字输入画面中,下面的,选择文字的窗口。
Window_ChoiceList (选项列表窗) – 使用于事件指令中的“显示选项”的功能的窗口。
Window_NumberInput (数值输入窗) – 使用于事件指令中的“输入数值”功能的窗口。
Window_KeyItem (关键物品选择窗) – 使用于事件指令中的“选择物品”功能的窗口。
Window_Message (信息窗) – 显示文字信息的窗口,俗称“对话框”。
Window_ScrollText (滚动文字窗) – 使用同名事件指令后,显示滚动文字的窗口。
Window_MapName (地图名字显示窗) – 在屏幕左上角,显示地图名称的窗口。
Window_BattleLog (战斗信息记录窗) – 在战斗画面上方,显示战斗信息记录的窗口。
Window_PartyCommand (战斗队伍指令窗) – 战斗画面中,选择“战斗/撤退”的窗口。
Window_ActorCommand (战斗角色指令窗) – 战斗画面中,选择角色行动(攻击,防御等)的窗口。
Window_BattleStatus (战斗状态窗) – 战斗画面中,显示队伍成员(HP,MP等)状态的窗口。
Window_BattleActor (队友选择窗) – 战斗画面中,选择队友目标的窗口。
Window_BattleEnemy (敌人选择窗) – 战斗画面中,选择敌人目标的窗口。
Window_BattleSkill (战斗技能列表窗) – 战斗画面中,选择“使用技能”后显示的窗口。
Window_BattleItem (战斗物品列表窗) – 战斗画面中,选择“使用物品”后显示的窗口。
Window_TitleCommand (标题指令窗) – 标题画面中,选择“开始游戏/继续游戏”的窗口。
Window_GameEnd (结束指令窗) – 游戏结束画面中,选择“返回标题/退出游戏”的窗口。
Window_DebugLeft (左调试窗) – 调试画面内,显示开关和变量编号的窗口。
Window_DebugRight (右调试窗) –调试画面内,调节开关和变量值的窗口。


场景类
点击打开
Scene_Base (场景基底) – 所有场景的基底类/父类,有着场景的主要处理方法,如开始和更新。
Scene_Title (标题场景) – 描绘标题画面的类。游戏一开始进入的场景。
Scene_Map (地图场景) – 描绘地图画面的类。地图上的一切在这儿进行。
Scene_MenuBase (菜单场景基底) – 所有菜单画面的基底类,主要是处理截图背景。
Scene_Menu (菜单场景) – 描绘主菜单画面
Scene_ItemBase (物品菜单场景基底) – 道具和技能菜单的基底类,主要是处理道具/技能的使用。
Scene_Item (道具菜单场景) – 描绘道具菜单画面的类。
Scene_Skill (技能菜单场景) – 描绘技能菜单画面的类。
Scene_Equip (装备菜单场景) – 描绘装备菜单画面的类。
Scene_Status (状态菜单场景) – 描绘状态菜单画面的类。
Scene_File (档案场景基底) – 存档和读档画面的基底类。毕竟存档和读档画面非常相似。
Scene_Save (存档场景) – 描绘存档画面的类。
Scene_Load (读档场景) – 描绘读档画面的类。
Scene_End (结束选项场景) – 描绘结束选项画面的类,也就是在主菜单选择结束游戏出现的画面。
Scene_Shop (商店场景) – 描绘商店画面的类。
Scene_Name (取名窗场景) – 描绘取名画面的类。
Scene_Debug (调试场景) – 描绘调试画面(在试玩模式中按F9时出现)的类。
Scene_Battle (战斗场景) – 描绘战斗画面的类,有着战斗相关的重要方法和定义。
Scene_Gameover (游戏结束场景) – 描绘游戏结束画面的类,过后会返回标题场景。


主进程
点击打开
Main (主进程) – 各种定义结束后,游戏开始运行的地方。因此,插件脚本要加在这个脚本的上面。


全局变量 (数据库)
点击打开
$data_actors (角色数据列表) – RPG::Actor(角色数据)的数组。
$data_classes (职业数据列表) – RPG::Class(职业数据)的数组。
$data_skills (技能数据列表) – RPG::Skill(技能数据)的数组。
$data_items (道具数据列表) – RPG::Item(道具数据)的数组。
$data_weapons (武器数据列表) – RPG::Weapon(武器数据)的数组。
$data_armors (防具数据列表) – RPG::Armor(防具数据)的数组。
$data_enemies (敌人数据列表) – RPG::Enemy((敌人数据)的数组。
$data_troops (敌队数据列表) – RPG::Troop(敌队数据)的数组。
$data_states (状态数据列表) – RPG::State(状态数据)的数组。
$data_animations (动画数据列表) – RPG::Animation(动画数据)的数组。
$data_tilesets (图块组数据列表) – RPG::Tileset(图块组数据)的数组。
$data_common_events (公共事件数据列表) – RPG::CommonEvent(公共事件数据)的数组。
$data_system (系统数据) – RPG::System(系统数据)的变量。
$data_mapinfos (地图资讯数据列表) – RPG::MapInfo(地图资讯数据)的数组。只供编辑器使用,在默认脚本内没有用处。


全局变量 (游戏对象)
点击打开
$game_temp (暂存管理器实例) – Game_Temp(暂存管理器)的实例。
$game_system (系统管理器实例) – Game_System(系统管理器)的实例。
$game_timer (计时器实例) – Game_Timer(计时器)的实例。
$game_message (信息管理器实例) – Game_Message(信息管理器)的实例。
$game_switches (公共开关组实例) – Game_Switches(公共开关组)的实例。
$game_variables (公共变量组实例) – Game_Variables(公共变量组)的实例。
$game_self_switches (独立开关组实例) – Game_SelfSwitches(独立开关组)的实例。
$game_actors (己方角色组实例) – Game_Actors(己方角色组)的实例。
$game_party (己方队伍实例) – Game_Party(己方队伍)的实例。
$game_troop (敌人队伍实例) – Game_Troop(敌人队伍)的实例。
$game_map (当前地图实例) – Game_Map(当前地图)的实例。
$game_player (玩家人物实例) – Game_Player(玩家人物)的实例。



回复 2 0

使用道具 举报

梦石
0
星屑
692
在线时间
31 小时
注册时间
2009-1-20
回帖
13
发表于 2018-6-18 19:10:56 | 显示全部楼层
神贴啊!膜拜

评分

参与人数 1赞 +1 收起 理由
guoxiaomi + 1 删前留名?

查看全部评分

回复

使用道具 举报

梦石
10
星屑
39550
在线时间
1919 小时
注册时间
2010-11-14
回帖
3214

R考场第七期纪念奖

发表于 2018-6-20 09:29:18 | 显示全部楼层
看到图像篇竟然没有后续了……
不过前面的一些东西还是比较有用的,虽然现在还没有足够的动手能力去改造。。。
用头画头像,用脚写脚本
回复

使用道具 举报

梦石
0
星屑
341
在线时间
27 小时
注册时间
2013-11-2
回帖
15
发表于 2018-7-8 22:37:15 | 显示全部楼层
哇。。。看到一半突然没了。。。。


不过貌似看了也几乎没懂。。。。先消化消化吧
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部