【转载】RGSS1脚本入门参考
57自己想说的话
对于学习rgss1很有帮助,是先前的RMXP脚本教程的作者RyanBern所写。
我怕它随时成为lostmedia,所以我把它转载过来。侵删
原文完成于2014.7.5,以下开始为原文内容。
原帖链接:https://rpg.blue/forum.php?mod=viewthread&tid=335455&extra=&authorid=356383&page=1
写在前面
一位论坛的朋友和我说,他看过很多RMXP脚本的教程,但是感觉没得到什么帮助,脚本也总写不好。我想这也是很多论坛朋友的共同问题吧,想自己弄个脚本,却无从下手;想看教程,却一头雾水(尤其是游戏里面的F1,感觉要有一定的基础才能理解最主要的部分)。这些天在论坛里面逛了逛,有不少尝试着自制脚本的朋友,但写出的脚本却总也通不过。我也看到了一些朋友写的代码,不得不说,代码中很多错误都是源于对教程的误解和对范例脚本(也就是游戏默认的脚本)的错误移植,游戏默认内置脚本其实是个很好的参考,但是如果不加分析胡乱利用一通,当然是不行的。于是我想到写下这个帖子,帮助那些渴望写出自己脚本的朋友们,成为一个真正的“脚本党”。我其实也算是个写脚本的业余爱好者,同时也在不断挖掘RGSS1更深层次的东西,本贴介绍的,仅仅是RGSS1的冰山一角,但却是我们编写脚本最常用的知识和基础。发出这个东西,不敢说能让大家都成为脚本高手,至少能让大家对RGSS1有个更清楚的认识。以下教程中,大家可能对Ruby和RGSS1有所混淆,Ruby是一门程序设计的语言;而RGSS1是基于Ruby编写的脚本系统,有很多特定的功能。
这里写的是我对写脚本的一些理解,希望各位高手能积极提出意见,有哪里写得不对的地方,还请大家帮忙批评指正。另外大家如果遇到什么相关的问题,可以随时在帖子中询问,只要我有时间,我会立即为大家解答的。(当然求脚本之类的提问我在这里就不处理了,请移步RMXP 提问区)
配套脚本教学视频:https://rpg.blue/thread-381015-1-2.html
【第二版序】
半年前这个教程杀青了,半年后再翻出来,感觉不是很满意,有些地方都没有说清楚。而且后面脚本解读那里,根本就是在利用“抄脚本——写分析”的模式,让人很难有欲望看下去。因此第二版会有一些改动。这半年来又接触了不少编程的技术,对一些问题的看法也和从前大大不同了,因此都写在这里,大家好好探讨一番。这次的改动,注重细节的解说,再加上理论与实践结合的部分(虽然还没有做,大家不要来BS我),想必能比第一版更好吧。
很多人觉得计算机很聪明,实际上,它是十分天真的。我们现在所看到它实现的强大功能,其实就是通过有限次的计算来实现的。我们说计算机很傻,是因为我们告诉它什么,它就做什么,因此,我们必须要好好和它沟通,它才能更好地为我们服务。
第0章节:预备知识
0.1 几个重要的概念
0.1.1 数字计算&运算符&表达式
这个我相信大家都明白,数字计算是计算机最基本的功能,游戏里面的F1已经说得很详细了,在这里我只想说一些大家容易忽略的。
赋值运算符“=”:
虽然很不起眼,但是,我们要注意的是,一定要把它和数学上的等号’=’区分开,在这里赋值运算符的作用是把它右边的值赋给左边,左边通常是一个变量(它的概念我们即将会讲到)。不能给常量或者伪变量再次赋值。例如不能写3=2这样的式子。另外,赋值运算符的优先级是最低的一个,因此一般把所有表达式都计算完毕后再进行赋值。
除法’/‘,取余数’%’:
这两个运算符的用处十分广泛。大家一定要弄清整数的除法。在绝大多数编程语言中,整数的除法不会发生除不尽的问题,得到的结果,其实是两个数的商值。例如7/2应该得到3,我们可以把它理解成7 = 2 * 3 + 1因此结果是3,而不是3.5。取余数也就是7%3=1。我们要注意,余数的正负和除数的相同(或者0),绝对值比除数小,因此,(-7)/3=-3,(-7)%3=2。这要提醒大家注意的是,不要随便交换乘除法的顺序,否则会造成一些不可预料的错误。
例如,1/33和13/3最后算出的结果是不一样的,如果到这里你没有发现它们之间的不同,请回顾一下除法的意义。
另外,如果想得到小数形式的商,就要用7.0 / 2,这样得到的是浮点数类型的3.5。注意,在计算机中,所有的浮点数都是不准确的,也就是说会有浮点误差,这是计算机精度有限造成的,因此,不能比较两个浮点数是否相等,因为通常你得到的都是“伪值”(即二者不相等)。所以,大家一定要充分利用整数,除非无法避免,尽量不要使用浮点数。
条件表达式’? :’:
具体的使用方法是:表达式1 ? 表达式2 : 表达式3
意思就是系统先算表达式1,如果表达式1成立,则计算表达式2,否则计算表达式3。并且整个条件表达式的值就是表达式2或表达式3的值(取决于表达式1是否成立)。
这个语句因为比if语句简单,所以用途十分广泛,大家一定要熟练掌握。
例如,max = x > y ? x : y,这里的意思就是先比较x和y的大小,如果x比y大,则再计算表达式2(也就是x),否则计算表达式3(也就是y),再把计算后的表达式赋值给变量max。
注:条件表达式有短路原则,如果表达式1成立,那么计算表达式2,而不去考虑表达式3(此时如果计算表达式3甚至可能发生错误)。同理,如果表达式1不成立,那么计算机也不会考虑表达式2的值。
0.1.2 变量
其实在第一部分已经用到了变量的概念,不过我相信大家对变量或多或少都有个了解,因此就没有再次引入变量的概念。但是,近期在论坛发现有一位朋友在讨论区提出了自己对变量的新看法,我跑去看了看,觉得还是比较深刻的,因此我在这里还要把它拿出来。
首先,变量是什么?一种很粗浅的理解就是,变量就是随着程序进行而有能力发生改变的量。它改变与否当然是服从编程者的意愿。但是,这个理解在我看来,并不是很深刻,因为很多人会把类似于a,b,这样的东西叫做变量,甚至有些人根据变量名称的汉语意思,来默认这个变量的作用。其实不然,类似a,b,x,level等等,我们应该把它看作一个符号,看作我们和计算机沟通的语言,而不是变量本身,而变量本身,则是存储在计算机内存中的一块数据。而就像我看的那篇帖子中说的,变量名其实就是告诉你,在内存的某处,存储这一堆数据,而这些数据代表的值是什么。因此,x = 3的意思就是,内存中存储着一段表示整数3的数据,而x,就是所谓的“标签”,我把它换成小猫小狗什么的完全可以。
学过C语言的朋友可能会发现,Ruby里面是没有指针的,这个机制其实为写程序的人提供了很大的方便。Ruby并不是真的没有指针,而是在使用的时候,指针和变量的区别变得模糊起来。下面我们来重点说说Ruby的变量机制。
Ruby中,任意的符号都可以看作一个变量,并且不加声明就可以直接引用,未经过初始化的全局变量和实变量的值为nil。这个nil到底是何方神圣?其实nil是个伪变量,表示的是“无”,属于Ruby的一种抽象数据实例。程序运行前,在内存的某处的一堆数据来表示nil,所有没有被初始化的全局变量和实变量,都代表它。从这个角度来讲,Ruby中的变量和指针似乎是等价的。但是有时候会发生很多费解的事情,我们来看看下面的两个例子。
(1)
1 |
|
(2)
1 |
|
在第一个例子里面,屏幕上将会打印2,在第二个例子里面,屏幕上会打印124。
细心的朋友会发现,在(1)中改变了a的值,但是b的值没变;但是在(2)中就不同了,对a进行的某种操作也会在b那里反映出来。但是,无论是哪种情况,在执行b=a之后,a和b表示的是同一块数据(指向内存中的同一片区域,即地址),而不是相同数据的不同拷贝,或者说,b是a的一个别名,你要找这片内存区域,说a也行,说b也行。
那么,我们应该如何去理解“变量”?在这里我们应该把“变量”都理解成“引用”,它们代表的并不是该数据内容的本身,而是该数据所在的内存地址。把变量的重新赋值理解为指针的指向改变,而数据内容的本身是没有变化的。(这点对Integer之类的东西貌似也是对的,因此你不能说把某一片用于表示“1”的内存区域修改,使其表示“2”,你只能把变量指针的指向从“指向表示’1’的内存区域”改变成“指向表示’2’的内存区域”)
在后面定义函数的时候,也会发生类似的现象。写过程序的朋友知道,函数上面的参数(我们叫做形式参数,简称形参)和实际的变量(我们叫实际参数,简称实参)没有什么关系,对形参的改变丝毫不影响实参的变化。举个例子来说,假如有下面的程序:
1 | def swap(a,b) |
函数的作用是交换a和b两个变量的值,但是如果运行下面的程序:
1 | a = 3 |
我们会发现a和b的值并没有发生交换,原因就是计算机只是把实际参数的值拷贝给了形式参数,之后函数内部对形式参数进行的操作与实际参数无关。
但是,我们刚才说过,变量实际表示的就是地址,而我们知道,相同的地址必定指向相同的内存空间,对同一块内存空间进行操作,变量的值当然会发生改变。例如:
1 | def f(a) |
得到输出的结果应该是233,这就意味着函数真正地对a进行了操作。因此,我们得出结论,传递到方法中的参数(实际是地址)会被复制一份,不会对实际的参数发生改变,而可以按照这个地址参数对其他区域进行操作,总之,实际参数的地址是不会变的。总之,一句话,这里函数参数的传值方式为“值传递(Pass By Value)”,记住这点也就不难理解上面的现象了。
最后我们要说Ruby中最常见的三种变量,这三种变量起作用的时机不同,用法不同,因此要分情况进行使用。
(1)全局变量
在Ruby中,全局变量以$开头,例如$t,$game_party,等等。它们是在程序的任何地方都有效的变量,也就是说,如果变量名字相同,那么必定就是同一个全局变量。因此,只有我们要创建共享范围比较大(在程序的不同地方都要用)的变量时,才能用到它,否则一般不用全局变量。最常见的例子,就是跨类进行全局变量调用,如果你定义了一个类,使用的过程中需要调用别的类的内容,就要用全局变量来帮忙。例如在Window_Item类中就调用了Game_Party类的实例$game_party,试想,如果$game_party不是全局变量,在Window_Item中,计算机不认识Game_Party中的符号,那么当然会发生访问错误。这个地方,到了我说类(class)的时候,大家会有更加清楚的认识。
(2)实变量
在Ruby中,实变量以@开头,通常是跟具体对象关联的。
例如:
1 | class Person |
在这里,@name进入到了函数pr_name的内部,对@name进行访问时,访问的应当是“这个对象的@name”。但是如果在这个类之外写print @name,那么你一定看到的是nil,因为这时@name已经不是Person中的@name。
(3)局部变量
在Ruby中,局部变量就是没有前缀的变量,比如level,x,等等。这一类的作用范围更窄,只是在定义函数内部有效,作为块参数的局部变量只在当前块内有效。在函数外面则是无效的。因此,我们在函数临时需要一个变量,函数结束后完全不需要的时候,就应该用这种变量。另外,函数的形式参数也要用局部变量表示。在这里面说明的一点就是,这种类型的变量是没有默认值的。例如:
1 | def fun |
在这里fun中的a和fun2中的a一毛钱关系都没有,它们是两个符号。所以我们明显看到,fun2中,对a进行的操作时非法的,因为此时a没有初始化,所以系统不能把它看做一个变量,在后面我们要说到,函数名字(方法)也是采用这种无前缀符号形式表示,系统会寻找与之同名的方法,如果还找不到方法,那么运行的时候,系统会提示错误信息No method error。
但是,自动变量的好处就是用完能及时回收,保证内存空间,但是对于全局变量,如果创建出来,在写程序的人没有下达命令的时候,系统是不敢轻易回收它的。试想如果程序里面的变量都是全局的,那么用不了多长时间,内存就塞满了,这对运行程序来说是非常致命的。因此大家务必要清楚什么时候该用什么变量,才能做一个合格的“准脚本党”。
0.1.3 常量和伪变量
在这里顺便介绍一下常量和伪变量。
常量,顾名思义就是这个量代表一个特定的值,一般不能被改变。Ruby中常量的表示方法是用首字母大写的标识符表示,例如Icon,MAXNUM等等,因此常量必须要赋予初始值。当然,常量也有作用的范围,使用常量时,建议把它们放到命名空间中去,这样能够突出常量的作用域,避免发生混淆。常量的作用是为了编写程序的方便,例如,论坛上有很多这样的脚本,显示一个窗口,但是在40号开关打开的情况下,窗口是不显示的。我们可以在外面利用常量WINDOW = 40来表示控制其是否显示的开关ID,这样如果要改动,只需要改动一处即可。
伪变量,是一类特殊的变量。Ruby中的伪变量常见的有4个,分别是self,true,false,nil。下面我们分别来说一下。
self:被处理对象的本身,这个概念在我们讲到类的地方会详细说明,初学者会比较难懂(说实话我在接触Ruby初期就完全不懂得self的含义)
true/false:表示一种逻辑值,实际是TrueClass/FalseClass的唯一实例。true是恒真,false是恒假,一般作为if的条件判断来使用,之间的运算符合逻辑运算。
nil:Ruby中的特殊数据类型实例,表示“无”,注意,它并不能代替不同类所谓“空”的概念。在数组类型中,空数组用[]表示,不用nil,在字符串中,空字符串用””表示,也不用nil。nil本身没有多少方法,大家可以认为它也表示一个恒假的值。
值得一说的是,表示伪的方式有很多,大家一定要记住2种,false,nil,这两个都表示伪,和C语言不同的是,数字0以及其他的值都表示真。[此处感谢无脑之人的宝贵意见]
0.2 几个建议
在本章节的最后,给大家提几个建议。
0.2.1 写任何脚本都要有良好的书写规范,脚本中要注意缩进,要有层次感。变量和运算符之间,最好用空格隔开。例如x = a + b这样。
0.2.2 变量名字要取得适当,尽量取一些有含义的名字,这样能让写程序和看程序的人知道这个变量代表什么。例如表示等级,就用level,而不是用简单的m或者n。循环变量一般都用i,j表示,这点大家养成习惯就好。另外,短下划线“”看做一个字母,如果要分隔变量之间的单词,请用,例如icon_size,中间的“_”当然不能换成空格。大家使用标识符时,不要使用中文字符,以免发生错误。
0.2.3 要培养自主纠错能力,不要提示个什么错误就茫然不知所措。系统弹出那个小小的对话框经常会包含重要的错误信息,这样能协助你改正错误。出现的错误,可能是Syntax Error(语法错误,可能是少打end或者是捏造了不存在的写法),可能是No Method Error(未定义方法错误,可能是对nil调用方法或者是类的概念模糊),可能是Name Error(命名错误),一切都要具体情况具体分析。
0.3 有关变量/指针/地址的重要补充(第二版更新)
这个是补充的内容,有兴趣的初学者可以看看,当然大神什么的就免了。
写这里的原因是时隔半年,突然发现自己的教程里面有很多东西没有说清楚。当然半年前还没有学过Java基础课,对有些东西的猜测也不敢随便写上去,现在把它补上。
在变量机制方面,Ruby和Java非常相似。不同的是,Ruby使用变量前无需声明,因此不必告诉编译器各种变量的类型,Java中使用变量之前还是要声明的。还有一点不同就是Ruby是“万物皆对象”,即所有的数据都是一个对象,而Java除了对象以外,还有类似于int,char等基本数据类型。我们下面说的就是对象这一方面。
如果想要创建一个对象的实体,就必须对类调用new方法,这样系统会在内存中动态地开辟一块区域,然后用调用构造方法initialize,最后把对象的引用返回给变量。如果不调用new方法,系统不会在内存中开辟区域。
下面是一个例子:
1 | class A |
执行:a = A.new(0,0,0),效果如图

执行:b = A.new(1,1,1),效果如图

注意到变量a和b的地址不同
执行:b = a,效果如图

执行完毕后,a和b变成了一个地址,指向了同一片内存区域。这就相当于为同一片内存区域建立了两个标签,或者说起了个别名,无论对a还是对b访问都会访问到这个区域。也就是说,a.x = 2和b.x = 2的效果完全相同。
那么,原来b代表的内存空间去哪了?即上面图的最右的部分,它不被任何指针变量所拥有,也不可能通过其他变量访问到它。像这种不被任何指针变量所拥有的内存区域,就应当被回收,以供别的变量使用。Ruby和Java相同,都有一套GC机制(Garbage Collection),它会定期检查内存空间是否已经不被任何一个指针变量所引用,如果没有任何指针指向它,GC就会把它回收再利用。注意,GC执行不是实时的,否则的话效率会变得很低。
说这里有什么用呢?是为了解释RGSS1中一处不太好理解的脚本(至少个人认为是这样),要解释这个地方,还需要了解一下Ruby存储对象的机制。
我们先看下这个脚本:
1 | class Game_Party |
这个方法出现在Game_Party中,我第一次看这里的时候,注释就没有看懂。什么叫“游戏数据载入后角色对象直接从$game_actors中分离,回避由于载入造成的角色再设置问题”?
而后全局搜索$game_party.refresh,发现它只出现在一个地方:Scene_Load(101)。可见真的是为了处理载入方面的问题。那么,既然载入了,为什么还要多次一举这样设置一番呢?
稍微懂点RGSS1的人都知道,$game_actors是存储所有主角角色的变量,编写的时候利用了类Array的外壳。而$game_party.actors是存储当前队伍中所有角色的数组,当然这个集合是所有角色集合的子集。那么,这两个地方都有指向Game_Actor类的引用,它们必须要保持一致。下面的图形能够说明这一问题。
但是,如果涉及到对象的存储问题,实际就不是这样了。我们在Scene_Save中可以看到,系统对$game_actors和$game_party都做了存储。因为写入文件的是对象,所以不但要把对象本身写入,而且要把对象内部的引用也要写入,如果对象内部的引用还有引用,那么也要写入……说白了,要写入$game_party本身,也要写入$game_party.actors这个数组对象,注意,写入$game_party本身只写入了$game_party.actors数组的引用,数组本身的内容没有写入。由于$game_party.actors是对象数组,因此还要把数组中的每一个Game_Actor对象都写入进去。而刚才我们提到,$game_actors中也含有所有Game_Actor对象,这样一来,同样的数据要写入两遍。读取的时候自然也要读两遍。我们在这里做个测试:
1 | class Scene_Load |
在读取数据完毕之后,我们来看看这两个对象的地址。注:所有对象都有to_s方法,如果不加重设的话,返回的是引用的地址。
由于$game_actors实际有意义的数据是从下标[1]开始,因此$game_actors[1]和$game_party.actors[0]是一样的,但是它们的地址却不一样。这就要调用$game_party.refresh,让这两部分引用保持一致性。
大家可以想想,如果不调用$game_party.refresh,会出现什么后果,而此时的内存地址模型又是怎样的?
预备知识就说到这里吧,虽然比较繁琐,但是我觉得在帮助上还是看不到的。这个帖子是连载,有时间我会写后面的内容,那些才是重头戏,大家一起期待之后的帖子吧。
有了预备知识,我们就可以进入Ruby中运用最广泛的部分——类(class)了。我在这里要说的是,在介绍类之前,我认为大家应该对Ruby的语法有了大致了解,对一些基本的概念也都掌握了,例如控制语句,函数,所以就没有再重复这些东西。如果大家有不明白的,请关注下RMXP的F1,那里面已经说得很清楚了。
第1章节:类
1.1 类&实变量&实例方法
1.1.1 类的概念
类其实是一个比较难说的概念,Ruby是面向对象的编程语言,所谓对象,就是计算机中的抽象数据,而且数据之间有一定的逻辑和结构关系。所谓类,不严格地来讲,就是抽象数据类型。翻开RMXP的F1,我们会发现,所有的类,竟然也都是一个对象,这就是我们所说的“万物皆对象”的观点。打比方来说,人类可以看作是一个类,那么人类的每一个个体就可以看作是一个对象,不知道这样是不是能好理解些。从感性的角度上来说,类应当是对象的一个集合。所有的人就构成了一个人类,而人是人类的一个实例(Instance)。类的上面定义了“属性(Property)”和“方法(Method)”。属性我们可以理解为描述(类生成)对象的性质,比方说人的身高,体重等等;方法可以理解为对象的某种操作,比方说吃饭,喝水,说话等等。对于类本身,还有专属于类的类变量和类方法。另外,类之间有继承关系,有多态性(Polymorphism)。如果你能理解这些,那么理解类也就不难了。
1.1.2 实变量&实例方法
每一个类的对象(实例)都有属于自己的变量和方法,所谓实例的变量,就是定义在该对象中的,只限于该对象自己使用的变量,实例方法,也就是只有属于这个类的对象才能使用的方法。我们举了例子来具体说明:
1 | class Person #定义一个叫Person的类 |
这个名叫Person的类就定义好了。
首先说一下,Person是上面我们所说那个“类的类(Class)”中的一个实例,Class类中的实例只有一个方法叫做new,调用方法要用圆点运算符“.”表示。格式是a.method(…),
其中a是该类的一个实例,method是方法的名字(函数名字,必要的时候要带参数)。
【关于initialize方法】每个类都能定义一个名叫initialize的方法,这个方法比较特殊,是类的构造方法,或者成为“构造器”。当你使用Xxx.new生成一个类的实例时,便自动调用了这个initialize方法,’.new’后面跟的参数会原封不动地传递到initialize中去。
刚才我们说过了,Person是Class中的一个实例,那么Person.new的意思就是生成一个Person类的实例。我们接下来插入下面一个代码:
R
1 | ryan = Person.new |
这两句的意思是生成一个Person类的实例,储存在一个变量中,然后调用该类的方法。因此执行完这两句之后,屏幕上将会打印出I am Ryan!的字符串。
在上一章节我们已经说过,如果一个变量是对象的一部分数据,那么要把它定义成实变量的形式(即@开头的变量),这样定义出来的变量在整个类的内部都是有效的。试想如果把上面的语句中所有的@都去掉,那么后果将会是……
给initialize方法添加参数
我们回头再来看看这个程序片段。
1 | class Person #定义一个叫Person的类 |
我们会发现,这个类别建立好了之后,所有的实变量的值都是固定的,这就导致我们只能建立相同内容的实例,因此我们要稍稍改装一下,让建立的实例拥有不同的初值。
1 | class Person #定义一个叫Person的类 |
这里的initialize是所有类一般要定义的方法,在一些面向对象的语言中,把这个方法叫做“构造方法”或者“构造器”。实际上,Person.new这个语句已经在调用initialize,所有类的初始化函数名字必须是initialize,这也是Ruby的规定。这里的initialize带有2个参数,我们回忆一下函数参数的概念,就会发现@name和name根本不是一回事。这样定义好了之后,我们可以写下面的语句了。
1 | ryan = Person.new("Ryan",'M') |
这样和刚才的效果相同,但是我们可以随便设置实例的初值。
实变量公开化
虽然我们可以用上面定义的类生成一个个实例,但是我们想一下这种情况:有一天Ryan出门了,见到了一个漂亮的女孩,因为我们定义了hello方法,所以Ryan很轻松地向她打了个招呼。女孩于是问:”Ryan”这个名字怎么拼啊?Ryan这时却抓耳挠腮,因为Ryan还没学会怎么让对方知道自己的名字拼写。再比如,Ryan觉得这个名字太简单了,想改成”RyanBern”(依然很矬),但是依然没有办法改变自己的名字。解决上面两个问题,就要用我们的实变量公开化。
1 | class Person #定义一个叫Person的类 |
我们加入了两个方法,第一个函数是返回实例中@name的值,第二个函数的作用恰恰是修改实例中@name的值。
我们注意到,这两个方法的名称比较奇特,和@name这个实变量很相似。实际上,第一个方法叫做’name’,第二个方法叫做’name=’,并且带一个参数。你可能会问了,怎么会有名字这么奇怪的方法?这种方法在C++/Java中都是没有的(除非你想操作符重载,但是即使重载了,效果也不如Ruby里面的这个好),而Ruby之所以这样做,是出于对实变量保护隐藏的目的。我们在类对象的外面,是没有办法修改对象内部的变量的,如果真的需要修改,那么只能通过定义方法的形式。注意,第二个函数开头的两个name是不一样的,前面那个是方法名字,括号里面的是形参名字。
有了这两个函数,我们可以写(假设Ryan已经定义好了):
1 | nam = ryan.name #将ryan的@name赋值给变量nam |
这里注意,上面的例子中第二句同时调用了’name’方法和’name=’方法,请仔细思考一下。
这种通过定义方法来公开变量,是Ruby中最常见的。实际上,定义这样的函数之后,可以把被定义的变量看作是类的一个属性,我们可以获取属性的值或者修改属性的值(但严格来讲,【属性】这个概念并不存在于Ruby当中)。一种简单的理解,就是把’name’,’name=’,‘@name’联系在一起,’name’方法返回‘@name’的值,’name=’方法修改‘@name’的值。将原点运算符’.’理解为“的”,那么a.name就可以理解为某人的名字。但是我们要知道,其实圆点运算符表示的就是类方法的调用,加上属性的概念是为了突出这种方法的特殊地位。这样,相关的实变量便可以通过外部访问。
当然,这样编写可能会浪费大量的篇幅,我们在这里有比较简单的形式来代替上面的两个方法。
1 | attr_accessor :name |
其中,attr_accessor是后面两个的合并,同时具备两种功能。
而attr_reader只定义了’name’方法;attr_writer只定义了’name=’方法,表示只允许读或者只允许写。
在这里我们注意一个问题,’attr_’开头的这一串文字,不是所谓关键字,也不是所谓变量声明。它们本身就是方法(隶属于Module),而它们的作用,就是生成其他的方法。而’:name’就是它们的参数(这是一个Symbol对象,也可用字符串对象代替)。它们生成的方法有最简单的形式,即’name’方法是单纯返回@name的值,’name=’方法是单纯修改@name的值。如果你想要让这两个方法变得复杂,那么就不要使用’attr_xxxx’。
因此,我们打开脚本编辑器,映入眼帘的Game_Temp,里面就是这些东西,仔细看看也没什么了不起的是吧?
1.1.3 父类&子类&类扩充&方法重写(Override)
在这里我们引出父类和子类的概念。假设我们定义好了一个类Person,现在我定义它的子类Student,那么我们要写:
class Student < Person,表示Student是Person类的子类。那么Student这个类所有的实例的性质,都会具有Person类的所有性质。即如果student是Student的一个实例,那么在Person类中定义的实变量和方法,student都能继承下来。这有点类似于“遗传”的概念。因此我们可以写student.name,student.hello等等这样的语句。
引入这一概念的原因,也是为了编程的方便。在计算机所有的对象中,虽然很多对象都属于不同的类,但是他们也同时具有很多的共同性质。那么我们可以先把它们的共同性质定义出来,做成一个父类,然后再分别定义它们特有的性质。在RGSS中,最明显的就是Window类,窗口类的父类是Window,然后Window_Base是它的一个字类,表示一般的窗口;Window_Selectable则是Window_Base的一个字类,表示具有光标的滚动窗口类。这两个类会衍生出一系列的子类,总体说分两种,不带光标以及滚动功能的,基本都是Window_Base的子类,具有光标以及滚动功能的,基本都是Window_Selectable的子类。有人会说,既然Window_Selectable是Window_Base的子类,那么我们岂不是可以把下面所有窗口类都归到Window_Selectable下?这是不行的,Window_Selectable比Window_Base更丰富,但是需要的内存空间也就更多,如果你要一个不带滚动功能的类,却设置到了Window_Selectable下,那么很多内存空间都没有被利用上,这显然是我们不需要的。
第二版新增
值得一提的是,类之间的结构定义体现了一个人编程素养的高低。在制作程序时,如果要新增某种功能,一定要引入新的类。但是怎么去引入?是直接引入所需类,还是从父类做起,以便有更好的适应性?这都是问题。对于两个不同的类,是做成父类——子类比较好,还是做成两个平行毫不相关的类比较好?答案并非十分明确。在这里举一个Java中的例子,我觉得很有参考价值。在Java的AWT包中,有Menu(菜单类)和MenuItem(菜单项类,即菜单拉开中的各个选项)。如果让你制作,你会把它们的关系定义为:
(1)父类——子类?
(2)子类——父类?
(3)两个平行类?
在Java中,这两个类的关系是第二种,Menu(菜单类)是MenuItem(菜单项类)的子类?啥?搞错了吧?菜单项包含在菜单的里面,怎么反而菜单项是父类?这个问题稍加思考,便可以赞叹编写Java-AWT包人员的高明之处。我们点开一个菜单,里面有各种菜单项,当然也包括一些可展开的菜单(即二级菜单),从这个角度上来讲,菜单就是一种特殊的菜单项,只不过它可以展开而已。因此MenuItem是Menu的父类是没问题的。
提到了父类——子类,那么还有一个东西就不得不提,那就是抽象类(abstract class)
尽管在Ruby中,并没有看到类似于抽象类的字眼,但是我们还是能够深刻地体会这种编程思想。面向抽象的编程思想也是编写这样程序的重要思想之一。抽象类是高度概括的一个类,它包含着它所有子类的一般行为。正因为它的高度概括性,它就必须作为一个父类出现,等待着别的类去继承它,扩充它。而自己本身,由于内容过于概括,因此不适合实例化一个对象。你问我这种现象的例子有没有?很明显,Object类就是一个抽象类。它定义了对象的一般行为,Object类作为所有类的祖宗,有着无可替代的高度概括性。定义出一个好的抽象类能够让你的代码更加简洁,易懂,而且显得有水平。
我们再看一个例子,RGSS1中的Game_Battler,它就是一个抽象类。它的两个已知子类分别是Game_Actor和Game_Enemy。啥?为啥说它是抽象类?我们看看下面的代码。
1 | class Game_Battler |
这是定义MaxHp的一段代码,比较简洁。我们注意def下面那句n = base_maxhp + @maxhp_plus ….
问,base_maxhp为何物?
它并不是局部变量,那就是一个方法了。
在Game_Battler搜索def base_maxhp,结果啥也没找到。
也就是说,Game_Battler里面的maxhp引用了一个没有在类内部定义的一个方法?这怎么可以?
这怎么不可以??
我们再次搜索def base_maxhp,结果在Game_Actor和Game_Enemy中均找到了它的定义。原来,这个方法不在父类中定义,而是在子类中定义。父类的方法却要用到子类的方法,这不是差辈儿了么?不是这样的,由于父类的高度概括性,导致无法在父类中描述base_maxhp具体的执行过程,而它的两个子类中,描述base_maxhp的执行过程是可以知道的。因此,父类就弄出这么个方法放到这儿,表示我不在这里定义,而是在子类中进行定义。这样只是起说明作用却没有实体的方法叫做“抽象方法”,而在Ruby中,你甚至都不用声明一个抽象方法。
那允许我再问个问题,如果对Game_Battler类的一个对象调用maxhp方法,则又如何?岂不是要出现No method error?
要记住,既然是抽象类,一般就不用它去生成一个实例,不生成实例,何谈调用一说啊?RGSS1中出现过Game_Battler.new吗?显然没有。
第二版更新·完
当然,如果一个类的方法和属性定义不能满足我们的需求,而我们又不想再定义一个子类,这时候我们就要对原有的类进行扩充。当然,你可以在原来的类上面进行修改,不过,如果是系统整合的话,我们会采用以下的方式:
1 | class Person |
注意,Person我们已经在前面定义好了,但是你完全可以再写一遍class Person,表示对该类进行追加定义。
方法重写(Override)
如果一个方法已经在父类被定义过,在子类再次对它定义(通常利用父类已有的方法)就叫做方法的重写。例如:
1 | class Student < Person |
这里我们重写了方法hello,那么在调用student.hello的时候,屏幕上显示的就是新定义叙述的内容(即后面多了and I am a student)。
不过,这顶多算是把原来的方法覆盖掉了,重写的味道还不是很浓。我们在定义子类方法的时候,通常是对父类的同名方法的扩充,这就要用到关键字super,例如:
1 | class Student < Person |
在这里,我们调用了Person类的initialize的方法,利用的是关键字super,当然参数什么的不能少。为什么不能写initialize(name,gender)呢?如果这样的话,系统会认为这是一种递归定义,即函数调用自身的过程(Ruby中允许递归定义函数),而并不是调用父类的同名方法,因此我们必须采用这样的形式。
注意,super关键字的使用有一定特殊性,在使用super的时候,要格外注意参数问题。在这里推荐,即使是被调用的方法没有参数,也要跟一对空括号’()’来表示没有参数,而不能什么都不带直接写super。这是因为,如果super后面什么都不跟,那么默认传进去的参数和正在定义的方法相同,这有可能引起错误。例如:
1 | # 定义父类 A |
如果有上面的定义,那么在使用B.new时,就会发生Argument Error。
方法的覆盖
如果一个方法在一个类中被定义,再次对其定义就叫做方法的覆盖。例如
1 | class A |
在这里 A 类的方法’test’被定义了两次,那么后定义的会覆盖之前定义的。如果调用A.new.test,实际调用的方法取决于这句话的位置,如果在第一个test后第二个test前使用,那么实际调用的就是覆盖前的方法;如果在第二个test后使用,那么实际调用的就是覆盖后的方法。
在这一小部分的最后,我想买个关子,在方法的定义和重定义中,还有一个重要的“别名”机制alias,这个东西的存在,为我们写脚本带来了更大灵活性,那么关于alias,我们要放到后面的章节进行讲解,这里就先不说了。
应大家的要求,我们在这里布置一道小练习题。
练习:请定义一个表示水瓶的类,水瓶有两个属性,一是最大容积(用一个正整数表示),二是当前水瓶中盛放水的量(也用一个非负整数表示,不得大于最大容积)。在这个类上面定义三个类方法:1.将一个水瓶装满水;2.清空一个水瓶里面的水;3.将这个水瓶自身里面的水倒入另一个水瓶,注意,不是随机地倒,倒完之后,保证自身是空的或者对方是满的。
定义类class Bottle并验证你定义的方法。
第二版新增
*1.1.4 模块(module)简介
首先,什么是模块?模块是用于实现某些特定功能的代码的组合。它与类不同,和类相比较,模块内部定义了一些常量和方法,从完整性看,模块不如类完整。不过模块内部可以定义内部类,这样定义的好处是让类的作用更加清楚。
如何定义一个模块?
利用关键字module可以定义一个模块。
1 | module Action |
这就是一个简单模块的定义。从这几句代码来看,定义了两个字符串常量和两个(普通)方法,而这两个方法,从名字来看是“打开”,“关闭”。可是这有什么用?我们知道,我们可以打开收音机,可以打开电脑,也可以打开煤气灶。那么,对于这几个类,都有相应的“打开”和“关闭”方法。一个个定义显然不妥,我们想到了定义父类。收音机和电脑可以归入到“家用电器(Appliance)”类中,但是煤气灶无论如何也不是什么家用电器。这可咋办?那就定义到更高的父类中,把家用电器和“煤气灶所属的类”归到一个类上去。那我们叫它“家庭用品(Utensils)”类。但是在这个类定义turn_on和turn_off方法,问题就更多了。家庭用品不但包括家用电器,煤气灶,还包括床,被单啥的,难道它们也可“打开关闭”?显然不行。那咋办?
我们经过观察,可以发现,turn_on和turn_off只是两个特定的功能,和类什么的关系不是很大。于是我们把它定义到模块当中,然后在类中把这个模块糅合进去(Mix-in)。这样,类中便有了模块中实现的方法。
要在类中糅合模块,要用include方法。
1 | class Radio < Appliance |
这样Radio对象就可以使用Action模块里面的功能。
啥?不会用?那么我再说个简单的用法好了。Module可以当作命名空间(namespace)使用,主要是各种参数和常量的定义。啥?直接定义在外面?这可不好,万一别人定义的和你自己定义的重复了就麻烦了,还是放到命名空间里面好。我们看看著名的Fuki对话框脚本:
1 | module FUKI |
Fuki脚本把这些常量都定义到module当中了,他为啥不定义在最外面?显然是考虑冲突的问题。因此我们写脚本也要有这样的习惯,把常量都定义到模块中,不要定义在外面,更不要定义成全局变量。例如(某升级提示脚本):
1 | $不显示升级窗口 = 88 |
开始觉得这样没什么,后来越看越觉得不好。
想要在别的类引用模块内部的常量,要用到’::’运算符。
1 | p FUKI::Head_PIC_DIR |
当然如果一个模块已经被include在类内,就可以在类内部直接用了。
我又忍不住联想了,其实module这个东西吧,跟接口(Interface)比较相似,都是实现某些特殊功能的语句组合。他们还有一点不谋而合:由于Ruby中无法多重继承(实际上多重继承并不十分合理,个人认为,但是多重继承有可取之处),因此module便可以实现某种意义上的“多重继承”。这点利用接口(Interface)也可以做到。
第二版更新·完
1.2 两个重要的类
在这里我们要介绍两个我们经常用的类。
1.2.1 数组Array
数组可以看作是一些对象的有序集合,在内存中占据一块连续的区域。在C语言中,数组有固定长度,而且数组内包含的元素类型必须是相同的。但是在Ruby中,数组运用就灵活了很多。这里的数组不但没有固定的长度,而且内部的元素类型也不必相同。例如:[0,2,nil,[1,2]],这个数组包含4个元素,从左到右分别是两个整数,nil,还有另一个数组。对数组成员进行访问,直接用下标表示,假如a是一个数组,第0号单元就是a[0],也就是物理位置上的第一个。下标从0开始而不是从1开始,这个为处理问题提供了很大方便(我一开始也不懂为什么要从0开始,编了四年之后发现这是很方便的)。所以大家还是尽量熟悉它吧。下面我们说下数组常用方法,这个在F1中输入array搜索就能找到。
- 初始化
a = []或者a = Array.new
别小看这个东西,很多时候因为少了这句话,会引发NoMethodError for nil : NilClass - 将一个元素x放在数组的最后面
a.push(x)
将数组里面添加元素的常用方法,类似于进栈操作。当然push可以跟很多参数,表示把参数依次添加到数组末尾。 - 删除数组中最后一个元素,并返回它
element = a.pop
注意这个函数有两个功能,一是删除,二是取值,类似于出栈操作。 - 删除数组中值为val的所有元素
a.delete(val)
注意,删除之后,所有元素依然是相邻的,下标的位置可能改变。 - 删除数组中位置为nth的元素
a.delete_at(nth)
同上,删除之后其他的元素位置会移动。 - 判断数组中是否有元素val
include?(val)
注意,Ruby中,以’?’结尾的函数的功能约定为判断(当然只是一个约定),返回值要么是true,要么是false。我希望大家也把这个“传统”延续到自己的脚本编写中。 - 将数组排序
a.sort
a.sort!
a.sort!{|a,b|…}
注意,Ruby中,以’!’结尾的方法称为“重磅方法(Bang Method)”,告诉你这是一个比较危险的方法,很可能破坏原始数据。
其中第三个为带块的排序,即按照一定标准排序。例如:a是一个数组,里面元素是我们刚才定义好的Person的实例,现在要将所有元素按照年龄大小排序,那么就要写a.sort!{|a,b| a.age – b.age}。注意,花括号里面的a和b是形式参数,意思就是取好了数组中的两个元素后,再取他们的age属性,和外面的表示数组的a无关。 - 遍历整个数组(第二版变更)
each{|item| …}
each_index{|i| …}
each_with_index{|item, index| …}
具体块中的操作自己设定。
例如:
1 | a = (1..100).to_a |
each方法就是按照次序取出数组中所有的元素,然后根据元素进行某种操作。
each_index实际就是对数组的索引进行遍历,作用等同于(0…a.size).to_a.each{|i| …}
each_with_index每次把索引和相对应的单元都取出来,以便在块内使用。
注意:熟悉RGSS1的人喜欢用for item in a~end的形式,其实for是一个语法糖,具体调用的方法还是each,这不过是照顾那些C++的人而设置的。
注意:使用each迭代器时,不要使用类似于delete这样对原始数组有破坏的方法,这是因为在迭代过程中要尽量保持原有数组的不变性(这种行为在C#里面是不能通过编译的)。
1.2.2 Hash表
Hash表又称关联数组,它相当于在集合A和集合B上建立了一个映射。也就是说,它将A中的每个元素映射到B中。A中的元素成为主键(key),B中的元素称为值(value)。每一个主键都对应唯一一个值。由于主键之间无法排序,所以Hash表也是没有顺序的。
具体的操作大家就自行F1,输入Hash查找一下吧。这里就不多加叙述了。
类的基础知识我们就说完了,肯定不能说的很详细,数组和Hash表的用法,大家如果想知道更多,请参考帮助文件,那说的应该比我要全。在下一章节我们要全局地分析一下RMXP默认的系统,了解一下整个程序究竟是怎样工作的,大家就敬请期待吧!
第2章节:解密RGSS系统
在这一章节我们来全局地了解一下我们的RMXP到底是怎么工作的。在这我要说一点题外话,四年前我刚刚接触RMXP,当时年少无知,画了一张地图放上去,就点击那个开始测试按钮,心想会运行出来什么东西,当时的想法是,我除了画了一张地图,什么都没做,大概会运行错误吧,但是实际上,RMXP中已经自带了一个默认的系统。千万别小看这个系统,虽然功能还不够强大,但是作为我们学习的例子,肯定是十分合适的。
注意啦,从这节之后,我们要具体学习如何解读RMXP的脚本和改造它,甚至我们可以自行编写脚本,这就是我写这个帖子的目的所在。
2.1 预置脚本
打开RMXP的脚本编辑器,我们就会看到一堆乱七八糟的代码。心里不禁有一些发虚,我相信,即使有些朋友看了很多次F1,对着这些脚本也会一头雾水。没关系,我们一点点梳理一下它们的层次。
2.1.1 Game_Xxxx游戏对象脚本

上面图片显示的就是游戏对象的脚本,游戏对象,就是在游戏进行过程中,和玩家互动的对象。比方说游戏地图,地图上的事件,发生打斗的敌人等等。这些对象都随着游戏进行而变化。与之相对的是数据脚本,这就是我们所说的数据库,它们是游戏开始之前就已经设置好的数据,一般是不可更改的。我们打开RMXP,很长时间都会花在游戏数据编辑上面。(比方说道具,武器,职业,状态,都是我们自己定义的,并且游戏进行中不会更改)
那了解这部分内容,我们想想在整个游戏中都会遇到什么东西。
首先是游戏的系统对象,比方说计时器,比方说各种音乐音效,这些对象的管理在Game_System中,我们打开这个脚本,就可以清楚地看到,这个脚本执行的是什么功能。当进入游戏画面中,系统会载入一个Game_System类的实例$game_system,这个全局变量是游戏中一直存在的,直到你退出游戏。而使用Game_System类的方法,就要利用$game_system这个实例。
例如,要让游戏播放一个BGM,就要输入:
$game_system.bgm_play(bgm),其中,bgm是代表一个音频对象(并不是指BGM的文件名),我们实际运用的时候,通常会调用这样的语句:
Audio.bgm_play(filename,volume,pitch),三个参数表示文件名,音量,节拍,后面两个参数可以省略。Audio其实是一个模块,我们初学还暂时用不到它的概念,我们只需要记住这样用就可以了。
然后就是各种游戏开关和变量了,这些东西,就是我们在编写事件的时候用的,实际上游戏在运行时,也会创建它们。不过,这个东西可不是游戏一打开就有的,只是在开始新游戏或者载入存档之后才有的。原因也很简单,因为无论是开关还是变量,都是在玩家真正开始游戏的时候才有存在的意义,而在标题处是没有它的实际作用的。换言之,如果你想用某个开关的变换来改变标题画面的一些东西(比方说打开1号开关读档按钮无效,关闭1号开关读档按钮有效),这样的思路是行不通的。
- Game_Switches——游戏开关的类
- Game_SelfSwitches——游戏独立开关的类
- Game_Variables——游戏变量的类
这三个类就是表示开关,独立开关,变量。对应生成的实例是:
- $game_switches
- $game_self_switches
- $game_variables
当然,这些东西只有真正进行游戏时才会有的。下面我们来一个个说该怎么用。
$game_switches和$game_variables都是类似于数组的结构,我们可以像数组那样使用它。不过仅限于读取和改写两种操作,其余的类似于删除之类的操作则无法进行。
因为数组是从0号单元开始,但是游戏设定里面,ID是从1开始的,因此这两个全局变量的0号单元都是nil,我们用的时候,不必担心这方面的问题。$game_switches[1]就是指的1号开关(事件编译器中的1号开关),$game_variables[1]就是指的1号变量。
$game_self_switches是类似于Hash表的结构,我们可以像Hash表那样使用它。当然,我们也不能使用Hash表的全部功能,仅限于读取和改写两种操作而已。
这个Hash表中的主键的类型,是一个含有3个元素的数组(Hash表的若干概念可以自行F1),[map_id,event_id,switch_tag],其中,第一个元素指的是地图ID,第二个元素指的是事件ID,第三个元素指的是开关编号(因为同一个事件的独立开关有4个,分别是A,B,C,D),这样某一个独立开关的信息就会完全确定。因此,让1号地图ID为2的事件的独立开关A打开,就要写:
1 | $game_self_switches[[1, 2, 'A']] = true |
注意,最后一个参数是单引号引出的大写字母。
我们知道,如果利用事件编辑器,只能对本事件的独立开关进行操作,但是,有了上面这个语句,我们便可以在一个事件中对另一个事件的独立开关进行操作,这样能解决很多问题。
那么在这里我们留下一个小小的作业。
【踩冰机关】请尽量用事件编写一个小游戏,主角踏入一片奇怪的冰地,冰地上有一个入口和一个出口,只有主角把规定区域内所有的地面都踩过一次(规定不允许重复踩踏规定区域内的任何一个地面,即所有地面必须踩一次而且只能踩一次),出口才会打开,否则出口不会打开。当主角踩踏某一块地面两次时,宣布失败,利用场所移动将主角传送到入口,并且重置地面(如果主角没有把地面全踩一遍就走到出口,按照规则,出口是封闭的,这时候主角只能第二次踩出口的那块地面,因此游戏也会失败)。允许使用的脚本仅限于对独立开关的操作。小提示:实际上,在地面块数比较少的情况下,利用开关也是可以做到的,但是开关的数量实在是太少,会造成很多浪费,在这里我们会看出独立开关的优越性。
最后要说的是,这三个全局变量,每次存档后,都会写入存档文件里面,利用这个特性,我们可以做出很多新的脚本,具体的还要到后面我们再说。
还有就是队伍,角色,敌人队伍,敌人。
- Game_Battler——游戏战斗者的类,分成3部分定义,是Game_Actor和Game_Enemy的父类
- Game_Actor——角色的类
- Game_Enemy——敌人的类
- Game_Troop——敌人队伍的类
- Game_Party——角色队伍的类
- tors——角色排列的类
- Game_BattleAction——打斗行动的类,在Game_Battler内部使用
其中,拥有全局变量实例,并且全局变量能写入存档的是
- $game_party #表示角色队伍
- $game_actors #表示角色排列
- $game_troop #表示敌人队伍
这些内容,我们会在第3章节重点讲解,因为这里是大家对游戏的改动涉及最多的地方,因此我们肯定会详细说的,在这儿只是让大家了解个大概。
注意,Game_Actors和Game_Actor是两个不同的东西,Game_Actors是类似于数组的类,$game_actors里面是按照角色ID存放各个角色的信息,而Game_Actor则是活生生的角色类,因此使用的时候不要弄混了(话说我刚写脚本的时候就经常弄混)。
再有就是游戏地图,角色和事件,公共事件了。这些都是在地图上要处理的,而且非常直观。
- Game_Map——游戏地图的类
- Game_Character——角色和事件公用的父类,分成3部分定义
- Game_Player——角色(在地图上)的类
- Game_Event——事件(在地图上)的类
- Game_CommonEvent——公共事件的类
其中,拥有全局变量实例,并且能被写入存档的是
- $game_map #表示游戏地图
- $game_player #表示地图上玩家的角色
这里要说明的是,游戏的各种事件是地图里面附属的一个东西,因此只需要放在$game_map里面就可以了,至于为什么把玩家单单提出来做成一个类,是因为玩家和事件有着不同性质。
另外是游戏画面,游戏图片。
- Game_Screen——游戏画面的类
- Game_Picture——图片的类
其中$game_screen会被写入存档数据。
Game_Screen是负责游戏画面闪烁,震动,色调变换,或者是天气设置的,这个肯定和地图不同,因此和Game_Map是分开的。
Game_Picture是在$game_screen内部使用,事件编辑器里面的显示图片什么的,其实就是这个类的方法。这里我们改脚本改动较少,所以就在这里说一下而已。
最后,游戏需要什么临时数据,比方说是否在战斗中啦,是否由事件调用存档啦,都是临时数据。这些临时数据统统放在一个类的实例里面。
Game_Temp——游戏临时数据的类
对应实例是$game_temp
因为是临时数据,当然不会写入存档中,这个大家一定要注意。很多人写了新脚本,却把需要存储的数据放到$game_temp里面去,结果可想而知。
2.1.2 Sprite_Xxxx Spriteset_Xxxx精灵 活动块 活动块组
![]{2-2.png}
实际上,游戏中显示到屏幕上的图片,都是由这些类生成的,不要以为$game_map表示地图,那么地图就是$game_map,实际上,负责生成并且显示图片的工作,是交给精灵(Sprite)完成的。
- Sprite_Character——角色行走图活动块类,包括主角和事件的行走图
- Sprite_Battler——战斗图活动块类,包括主角和敌人的战斗图
- Sprite_Picture——图片活动块类
- Sprite_Timer——计时器活动块类
- Spriteset_Map——地图元件活动块组,是一些活动块的集合体,例如地图元件,角色行走图,远景图,雾图形,计时器
- Spriteset_Battle——战斗画面活动块组,是一些活动块集合体,例如战斗图,战斗背景,计时器
在这里我们只讲一个事情,很多人会发现,数据库中能够更改敌人战斗图的位置,却不能更改主角的战斗图位置,这是非常不方便的事情。那么怎样改变角色战斗图的位置呢?我开始以为答案在Sprite_Battler或者Spriteset_Battle里面,后来一看根本就不是,其实角色战斗图的位置在Game_Actor中的第567行(如果脚本是默认的):
这下就可以改脚本了,这是竖版战斗改横版战斗或者45度角战斗的第一步啊!
2.1.3 Window_Xxxx窗口类

这也是游戏中非常重要的一个类了,以后我们经常会跟它打交道。游戏里面所有带框框的基本都是这个类的实例,其中第三个Window_Selectable改,是我优化了Window_Selectable之后的脚本,原来的默认脚本是没有的。当然,窗口我们也是重点讲,不过还是不在这一章,因此大家翻开看看就好,起码知道每个脚本都是负责什么窗口的。
2.1.4 Arrow_Xxxx光标类脚本
这个图我就不截了,如果是没有鼠标脚本的话,这个地方大家应该不会动(实际上我也没怎么动过)。这个类的内容,就是负责游戏里战斗场面指敌人或者指主角的光标,而不是窗口中的光标矩形,这个大家注意下就好。
2.1.5 Interpreter事件解释器脚本

这个大概是脚本编辑器最庞大的脚本了吧,分割定义就有7个之多。而他们的作用,我不用多说,是事件党的最爱了吧。事件编辑器中所有的指令,都是这里的方法,说白了,RMXP把我们最常用的命令放到事件编辑器中,可以进行“傻瓜式”操作。
当然,有些脚本就是对这些事件指令进行优化,比如我们熟知的物品得失提示脚本,就是更改了Interpreter类里面的内容。这个我们后面也要作详细说明。
2.1.6 Scene_Xxxx场景类脚本

这也是我们经常会遇到的脚本,它们的作用就是处理一个个组合的场景。简单来说,一个场景包括很多个窗口,精灵,以及对所有输入的回应。注意,特别是对输入的回应,是在场景中进行的,也就是说,Window_Selectable定义的时候,并没有说输入空格或者回车后,窗口该怎么怎么变。真正对输入的反应,是在Scene中进行,因为一个场景的很多“元件”都是相关联的的,用一个场景统一处理他们,才是最好的选择。
同样,我们在后面的章节,要重点讲场景的制作。
2.1.7 Main游戏脚本的入口
在脚本的最后我们看到一个叫Main的脚本,翻开它,我们会看到只有短短的几行。这就相当于RGSS的主函数,整个程序就是从主函数出发来向下进行的。
另外,在Main之前的位置,是我们插入各种外挂脚本的地方,我们不能随便地插入到脚本编辑器的任意位置,只能插到Main组之前,Scene组之后,这点大家一定要注意。
2.2 RGSS的工作过程
实际上,脚本编译的顺序就是从上至下。注意,此时程序还未开始运行,Main组前面所有的脚本,都是进行各种变量和方法的定义,因此它们也只是静静地躺在那里。真正开始的是Main组中的脚本。
那我们先看看Main组脚本里面都有什么:
1 | begin |
一开始先简短设置系统字体,准备画面过渡。
然后就直接进入场景画面。
注意,那个$scene是伴随这程序始终进行的全局变量,表示的就是当前场景本身。中间那个while循环是说,如果$scene的值不是nil,那么就调用$scene的main方法。也就是说,如果想退出这个循环,直接输入$scene = nil即可,这样的话,不但场景会退出,主函数也会跟着结束,因此整个游戏就退出了。
在这里$scene一开始被赋予Scene_Title.new,也就是说我们即将进入标题画面,调用的也是标题画面的main方法。
我们于是打开Scene_Title,在这里我就不放全部代码了。可以看到有下面的语句:
1 | $data_actors = load_data("Data/Actors.rxdata") |
这就是在游戏一开始,载入所有游戏数据库内所有数据,并保存在相应的全局变量里面。这些全局变量不会被更改,一直存在直到游戏退出。
1 | def command_new_game |
以新游戏命令为例,当玩家选择“新游戏”时,系统会做以上工作。
其中我们看到直到这里才会生成各个游戏对象,如果是载入,那么就会载入各种游戏对象。
最后一步将场景切换到地图场景中。这里把$scene变成Scene_Map.new,在所有场景的main函数中,都有一个无限循环loop do,其中有一句:
1 | if $scene != self |
这里的self是指被处理对象实例本身,刚才我们说$scene = Scene_Map.new,这就是所谓的$scene不再是被处理对象本身了,变成了其它的东西,表达式不满足,跳出main的主循环loop do。而后,我们回到Main组那里,$scene.main这个语句已经执行完毕,但是不满足循环终止的条件,即$scene还不是nil,因此Main组里面继续调用新的$scene的main方法,这时候$scene已经是Scene_Map的实例了,因此画面就会转到地图画面。
整个游戏就是这样工作的。直到$scene == nil,游戏便终止了。
第2章节的内容就到这里,接下来的第3章,我们要系统学习如何DIY游戏对象脚本Game_Xxxx,大家就敬请期待吧。
第3章节:改动游戏对象
从这一章节开始我们要具体说说如何在原脚本的基础上改动脚本,甚至你可以自己做出一个脚本。如果你前面的概念不是很明白的话,没有关系,不过只要你关注这里的话,相信你一定能够成功写出一个脚本的。
3.1 战斗者(Game_Battler) 角色队伍(Game_Party)
3.1.1 Game_Battler Game_Actor (角色类)Game_Enemy(敌人类)
作为可以出现在战斗中的对象,RGSS将角色和敌人分别定义,再根据他们的共同性质(比方说都有HP,都可以进行技能伤害等等)定义出一个父类Game_Battler。那我们先翻开Game_Battler,看看这里面的结构究竟是什么。
1 | attr_reader :battler_name # 战斗者 文件名 |
我们先看attr属性定义的部分。
battler_name和battler_hue是战斗图的文件名和色相,这个大家一般不用管,都是在数据库里面设置好的。
states指的是当前战斗者被附加上的状态的ID,是一个数组。
hidden和immortal指的是敌人的专有属性,分别是是否隐藏(就是中途出现)和不死之身标志,这个也是在数据库定义的。虽然定义在了Game_Battler里面,但是如果是Game_Actor的实例,使用这两个属性是无效的。
damage是别的战斗者对自身造成的伤害,注意,并非自己的某种战斗行动给他人造成的伤害。
我们再看initialize方法部分。
我们会发现,这里面多了几个没有声明成属性的实变量。证明这些变量是没有公开的。
@states_turn是一个Hash,里面存放的是当前具有的状态距离自动解除还需要的回合数。这里用Hash表而不用数组是因为考虑到一个战斗者身上的状态毕竟是少数,用Hash可以更加灵活。比方说1号状态经过3回合才能进行自动解除(注意,自动解除并不是真正解除,而是数据库中的这个部分:
),
那么@states_turn[1]的值就是3。这样每过一个回合,所有状态的剩余回合都-1,达到0的进行自动解除。另外,如果这里的一个主键的值是-1,那么它是一个自动状态(就是防具附加的状态),这种状态是不进入自动解除的,除非你卸下防具。
@maxhp_plus,@maxsp_plus等6个属性值的是各种能力值的加成,这些变量也是针对角色来说的,主要是在用事件增加角色的能力值或者用道具增加能力值的时候,储存增加的量。注意这些值和等级是没有关系的。
我们来看看下面的几个方法。
细心的朋友可能发现,在这个类别中,没有写类似于attr_accessor :maxhp这样的东西,这是因为attr_accessor只是定义简单读取和写入操作,并没有其它的功能。而我们知道,一个角色的maxhp,取决于他的等级,状态,能力值加成等等,如果只是简单定义attr_accessor :maxhp,这样对maxhp进行更改就会变得很复杂,倒不如直接一次性取得所有的因素。
1 | def maxhp |
这里我们可以看到,获取maxhp的第一步,是计算基础maxhp和加成maxhp的和,再进行范围修正。第二步是进行状态的修正,我们知道,在数据库中的状态一栏,我们可以通过状态来改变maxhp值。最后那个n = [[Integer(n),1].max,999999].min表示先把n变成整数,然后再进行属性修正。
在这里,我们发现在第二行有个base_maxhp,很多人会好奇这是个什么东西,不是实变量,在Game_Battler里面也找不到相应的方法,更不可能是局部变量。其实,这个东西确实是一个方法,但是不是在Game_Battler中,而是在它的两个子类Game_Actor和Game_Enemy中,因为两个子类对应的方法不一样,所以要分开定义。这就意味着虽然这个方法看似在这个类中没出现,但是如果我们在其子类中定义了它,则仍然可以在子类中使用。在后面我们会看到它的庐山真面目。
R
1 | def make_action_speed |
这个方法的作用是决定战斗者的行动速度,即速度越大的越先行动。agi是战斗者的速度,后面那个是上下浮动的随机分散值。这就说明了一个战斗者速度对行动先后的重要性。这也能说明,速度大的战斗者行动的顺序很可能排到前面(受到后面浮动随机分散的影响)。因此我们如果要更改这种排序模式,改变这个方法就OK。
然后就是这个增加状态的方法。
1 | def add_state(state_id, force = false) |
这是将战斗者自身增加状态的两个方法,第二个方法调用了第一个方法,而第一个方法是增加单个状态ID为state_id的状态,先是判断状态是否无效(即定义到了$data_states之外),然后是判断战斗者身上有没有抵抗这种状态的另一种状态(例如,“战斗不能”状态会解除所有其他的状态,如果给一个“战斗不能”的战斗者附加别的状态,不会成功),如果有则附加无效,然后是附加这个状态,并且产生一定的效果,然后对@state进行排序,排序的依据是状态的优先级,也就是下图:
第二个方法是真正的增加状态方法,可以增加一组状态,并且有成功率判定的条件。先是判定是否防御了这个状态(如果角色穿上了相应装备就会进入这一过程),如果不能防御,看是否状态是“不能抵抗”,否则按照ABCDEF的有效度进行判定。A是1,B是2,等等(注意,F1中对此处的解释有误!)。因此,如果想改ABCDEF代表的成功率,就要改下面这一行:
1 | if rand(100) < [0,100,80,60,40,20,0][self.state_ranks[j]] |
比方说把A改成90%就把数组第二个数100改成90,就这么简单。
接下来我们看Game_Battler的分割定义3,这个脚本整体说的是攻击效果的定义,包括技能能否使用的判断,技能效果,普通攻击效果,物品效果。因此,论坛上很多人问到,如何进行自定义攻击效果设置,其实改的都是这里。在这里我们举两个例子:
例1:技能需要消耗一定的道具
这个问题已经很老了,相信大家也知道怎么处理,不过还是提一下。Game_Battler分割定义3的第一个函数就是判断技能能否使用,里面列出了各种情况,我们只要增加我们自定义的判定模式即可。比方说1号技能要消耗3个2号道具,就在函数的第一行写下这一句话就可以:
1 | if skill_id == 1 and $game_party.item_number(2) < 3 |
这样,1号技能就有了个特殊判断,当然,要在1号技能的公共事件上设置使用后消耗3个2号物品即可,这个相信大家都会做。
例2:技能伤害加成
这个就是修改skill_effect方法就可以了,注意它的两个参数分别是使用者和特技,进行相应调整就可以。比方说,如果角色持有含属性[火]的武器,释放含有属性[火]的技能,则获得20%的伤害加成,就可以写(假如火属性的ID为1):
1 | if self.is_a?(Game_Actor) && $data_weapons[self.weapon_id].element_set.include?(1) && skill.element_set.include?(1) |
这里的第一个条件self.is_a?(Game_Actor)不能省略,因为对于敌人类(Game_Enemy)来讲,是没有weapon_id这个属性的,会引发NoMethodError。这个is_a?是判断某个实例是否属于某个类的函数,对任意的实例都有效。例如:
1 | class A |
这个语句希望大家能够熟练使用,很多判断没有这句话,似乎是很难完成的。后面的那些语法,参考F1就基本能够写出。
接下来我们介绍一下Game_Actor,作为角色的类,有它的特殊性。
我们看一下专属于Game_Actor类的变量。
1 | attr_reader :name # 名称 |
中间那个character_name和character_hue指的是角色行走图的文件名和色相,而不是战斗图的文件名和色相。在这里看出,默认系统里面,角色类才有装备,等级,经验的刻画。
我们再向下看,就会看到属于Game_Actor的各种方法。前面说到的base_maxhp也在其中。具体的我就不领着大家一个个说了,只举一个例子。
例:简单的装备附带技能系统
这又是一个热门话题,不过很多人都解决过它。实际上,如果我们学习了这里,我们也能比较轻松地解决它,下面请看。(以下提供的解决办法还有很大提升空间,但是这个办法是最容易想到的办法)
步骤1:明确我们要做什么,如果给角色装备相应的装备,就让它学习相应的技能,卸下装备就遗忘相应的技能。注意:装备中的技能最好不要交叉(即两种不同种类的装备含有相同的技能),这样能让我们的系统简单一些,而且装备技能不可从其他渠道学习(即只能通过换上这种装备才能使用)。
步骤2:先要设定装备和技能的对应关系,有点像新数据库的构造,我们可以采用Hash表的方式构造一个数据库中无法设定的新数据。考虑到这个数据在全局都可以使用,应该定义成全局变量,而且定义在最外面的结构里面就可以了。
1 | $equipment_skill_table = {} #在这里定义的是Hash表 |
步骤3:增加装备的同时增加技能。翻开Game_Actor,我们看到了下面的方法:
1 | def equip(equip_type, id) |
这样,以武器为例,我们在when 0这个结构层次上做如下改动:
1 | when 0 |
如法炮制剩下的几个when分支就OK了。
再次提醒一下,$equipment_skill_table一定要先定义再使用,定义位置随意,但是最好定义在最外部的层次上(class保留字的外面)。
关于Game_Enemy类的,没有什么多说的,但是我们也可以自定义一些内容。
例如:能力值破限。在数据库中无法设置敌人的力量(str)为999以上,但是在这里我们就可以做到。如果要设置1号敌人的力量为1500,则找到base_str那一个函数,改写成:
1 | def base_str |
利用条件表达式就可以轻松完成。不过要定义多个敌人能力值破限,就要定义一个自定义的数据库,然后再进行函数值返回,这里就不说了。
我们向下看,可以知道,敌人的普通攻击是没有属性的,也没有附加状态的变化,用类似的方法,我们可以修改这部分的内容,这里就不说了。
在这里我们留下第一个作业题目。请设定一个被动技能,它的效果是,学习该技能的角色会降低所有状态(不能抵抗的除外)的命中可能性(默认是20%),即如果原来角色对状态1的有效度是C(60%命中),则学习该技能后降为D(40%)。
3.1.2 角色队伍Game_Party
这个也是我们经常修改的类别,下面是Game_Party的脚本开头。游戏里面Game_Party的实例是$game_party
1 | #-------------------------------------------------------------------------- |
我们注意到,角色,金钱,步数是整个队伍共有的东西。而我们看到,道具,武器,防具却没有用attr来定义属性,因为物品之类的东西,RGSS中设置了上限(默认99),如果直接用attr来定义,则还需要进行范围修正的处理。由于没有定义相应的方法,因此用$game_party.items[1]这样的方法取得1号物品的个数是不可以的。也无法通过$game_party.gold = 100这样的语句修改金钱(因为gold属性只是attr_reader定义的,只能读取而不能写入)。
$game_party.actors指的是当前队伍中所有的队员,注意,$game_party.actors可以看作是$game_actors的一个子集,随着角色信息的变化,二者的数据时同步的。由于系统限制,默认$game_party.actors中最大人数为4,如果要做一个队伍脚本的话,首先就要突破这个限制。
我们向后看,定义的方法包括角色的添加和删除,各种队伍物品的添加和删除,最后还有确定角色目标的方法,这个都不难理解,需要的时候,甚至都可以翻过来参考,不过我还是建议大家把这些方法熟练记住。
例如,现在要清除队伍中所有的物品。如果要是不知道脚本的话,那就麻烦大了,用事件的话是会累死人的。但是知道了队伍脚本,就不一样了,于是我们可以写:
1 | def clear_items |
把这个方法定义在Game_Party的内部,然后在事件中插入脚本$game_party.clear_items即可。
注意@items没有被外部化,因此用$game_party.items[id] = 0这样的语句是不可以的,重申!
大家可能看到Game_Party里面的方法简单易懂,因此涉及到物品,金钱等变量时,一定要参考Game_Party。
1 | def add_actor(actor_id) |
上面的脚本说明,$game_party.actors中最多只能容纳4个成员,如果队伍人数已满,再添加角色是无效的,有很多人认为用事件的“添加队员”指令,就一定会成功,其实不然。如果想让队伍中的人数突破限制的话,就必须首先在这里做文章。当然改成5人战斗脚本光改这里肯定是不够的,我们必须要进行大量的整合,这里就不细说了。
3.1.3 地图角色Game_Character及它的两个子类Game_Player(角色)和Game_Event(事件)[/size]
这些脚本都是处理地图画面上角色以及NPC移动的类,我们先从Game_Character看起。
1 | attr_reader :id # ID |
在这里我们看到了Game_Character的一些属性,这里的x和y相当于我们地图编辑器中的这个:
![]{3-3.png}
是理论的坐标值,实际坐标值和理论坐标值有个换算关系,实际坐标 = 理论坐标 * 128,这个记住就好。我们写脚本的时候,理论坐标用得最多。Character_name和character_hue表示的是角色行走图的文件名和色相,这个和我们刚才讲Game_Actor里面的属性几乎是一样的,不过Game_Character作为角色行走图类,这个属性和Game_Actor的还是有本质区别。
pattern指的是角色行走图的图案序号,这是个什么东西呢?我们知道角色走动的时候,会有踏步动画,一个角色行走图文件是4*4个图形,不同横行代表不同方向,同一横行的四个图案就是用来做成动画的,这个pattern就是指同一横行的图形号码。
direction指的是行走图的朝向,在RGSS中,数字2、4、6、8分别代表下,左,右,上。这个怎么记忆呢?我们看看电脑上的数字小键盘,这个对应关系,正好是小键盘数字和方向的对应。
接下来我们看下这里面的各种方法。RGSS有一套默认的角色移动规则,这类规则我们也不用做太多改变,已经是比较合理的了。因此下面的函数我们看看形式即可。
1 | def screen_z(height = 0) |
例如,我们看一下这个画面z坐标的方法。因为我们是调查的行走图在画面上的z坐标,而不是元件,因此我们不要随便引入优先级这个概念。首先,我们知道,事件选项里面有一个“总在最前面显示”,这种事件的z坐标是最高的。另外,我们要注意,表示角色的Game_Player是没有这个属性的,因此角色永远也不能成为画面最上层的东西。下面我们来看其他情况的z坐标。这里先计算了一下画面的y坐标,这是为什么呢?因为我们知道表示角色和事件的图形都有一定的高度,而在地图上,一个格子的大小是3232,超出的部分就要向上下左右扩张,而我们知道,一个NPC图形的大小是3248,RGSS中,默认把图形的中下方的32*32区域放在指定坐标的格子中,因此角色头部就会覆盖他上方的格子。

比方说上图这种情况,我们要让y坐标大的角色z坐标也稍微大一点,这样可以覆盖他上方的角色图形。当然我们知道,角色图形可以是元件,那么元件的z坐标就和它的优先度有关。在最后一行我们可以看到,1个优先度=32个z坐标,也就是说,角色和事件图形的优先度是大于0小于1的,因此只要优先度超过1的元件,都会把事件盖住。
Game_Character下面两页几乎都是写移动和跳跃规则的方法。因此,我们除了在事件中运用“设置移动路线”来让角色移动外,还可以直接用脚本来让角色运动。例如写$game_player.move_left就可以让主角左移动一格。
然后我们看Game_Player。
因为在主角移动时,画面会始终跟随主角移动,因此Game_Player里面多了个center(x,y)方法。这个大家看看就行了,没必要完全搞懂。
1 | def increase_steps |
在这个方法中,我们终于知道了游戏为什么要记录主角的步数。很多人认为新工程里面主菜单显示“步数”完全是个多余之举,确实我也认为没有显示的必要。步数是用来生成遇敌计数和检查连续伤害用的,这里,地图用的连续伤害是每走2步就减去1%的HP,当然你可以对此进行更改。
1 | def refresh |
在这里我们明白了,地图上角色的图形和队伍中第一名角色的图形是一致的。因此,我们完全可以把它改成别的。在Game_Party中我们可以增加“领队”这一属性,然后把中间的actor = $game_party.actors[0]换成actor = $game_party.leader即可。当然Game_Party的leader还需要定义。另外,我们经常遇到主角变更交通工具的情况,比方说航海的时候,主角图形则要显示为一艘船。这个我们纯用事件是无法做到的(我们可以通过改变队伍中第一个角色的行走图来获得这个效果,但是你打开菜单,会发现角色的脸谱也变成了一艘船,这个显然很不合理),我们仍然可以在这里进行改动。例如,设置一个航海中的标志,如果队伍在航海中,则直接设置@character_name和@character_hue即可。
在Game_Player的update方法中,我们看到了输入的处理。
1 | case Input.dir4 |
这是对4方向输入的处理,用Input.dir4,游戏中还有一个对8方向的输入处理Input.dir8,增加了4个斜向的移动,1、3、7、9分别表示,左下,右下,左上,右上,还是跟小键盘的方位是一样的(注:F1再次出现错误,F1原来写的是数字1到8表示8方向输入,这是不对的,应该是1、2、3、4、6、7、8、9才对)。
1 | case Input.dir8 |
这个是修改后8方向是输入,大家可以试一下。
关于Game_Xxxx类脚本我们就要说这些,大家有兴趣的话可以多看看脚本,会有更多意想不到的发现。
3.2 自制游戏对象
看了这么多游戏对象的脚本,是不是有自制游戏对象的念头了呢?那我们在这一小节就说说如何自制属于你自己的游戏对象脚本。
3.2.1 起步
首先要明白,你自己想自定义的游戏数据到底属不属于游戏对象,如果不属于(比方说你要建立一个数据库的类,这就不属于游戏对象),那么就不应该用游戏对象的思路。总的来说,随着游戏进行,随时都有改变可能,并且跟玩家进行直接互动的对象,大多都是游戏对象。下面我们来看一个例子:
例如,制作一个真实商店,要求是商店尽可能还原真实生活中的商店,有库存限制,你卖给这个商店的物品,商店也会原封不动显示出来。这就是一个游戏对象。
3.2.2 分析属性和方法
下面我们要分析一下,我们建立的游戏对象应该有哪些内容和方法。
还是以真实商店为例。
我们需要的是商店的最大库存(所有商品的最大数量,为了方便起见,对于同一个商店,所有商品的最大库存都是一样的),以及商店里面各种商品的数量。除此以外,暂时还未想到其他的属性。
真实商店的方法很简单,就是出货和进货,不过考虑到物品分为道具,武器,防具3种,我们可能要定义很多方法。
暂时想到这么多,然后我们就可以开工了。
3.2.3 写出游戏对象脚本
我们现在,就可以根据我们的分析,来写出真实商店脚本的游戏对象部分了。先是定义各种属性,考虑到道具,武器,防具的不同性质,我们把它们定义在三个表中。
1 | class Game_VisualShop |
接下来定义初始化:
1 | def initialize(max_number,items,weapons,armors) |
这个初始化就完成了,我们注意,传进去的参数有4个,分别是库存最大数量,各种物品的数组,数组里面是物品的ID,我们这里用ID而不是用物品的实例,是为了模仿游戏主角背包的存储模式,这个一定要注意。然后就是把商店各种物品数量初始化为库存最大值。这个也是看个人需要。我们用的是Hash表来表示现有的库存,这个也是借鉴了Game_Party中对于队伍中各种物品武器防具的刻画。写到这里,我们可以考虑给真实商店增加一个变量,表示商店中可以显示库存为0的物品(其余的库存为0的物品就直接不显示)。这是因为你卖给这个商店的物品和这个商店原有的物品本质是不一样的。这是一个二维数组,含有3个一维数组,分别表示总显示的道具、武器、防具。因此在initialize的最后,加入
1 | @always_shown = [] |
这个显然我们不用把它公开化,放在这里就可以了。
然后是各种方法,也是非常简单的。
1 | def import(type,id,number) |
这是一个通用的商店进货的方法,type表示种类,0为道具,1为武器,2为防具。id是对应类型的id,number是数量。添加的时候,考虑到某物品的Hash个数可能不存在,因此要进行一步判断。然后再进行增加操作,并修改范围。如果最终个数是0并且不是永远显示的话,直接从相应Hash表中删除。这里只给出了道具部分的定义,武器和防具部分定义是类似的。
有了这个东西,出货的方法就很容易写了。
1 | def export(type,id,number) |
直接调用import方法,并将number取成负值即可。
到这里我们所有的基本方法就定义完了,Game_VisualShop也告一段落,不过,现在它只是一个游戏对象,如何将它和玩家互动起来,我们还需要学习后面的内容。
第3章的讲解就到这里,在下一章我们会将到窗口的使用,大家敬请期待吧。