加入我们,或者,欢迎回来。
您需要 登录 才可以下载或查看,没有帐号?注册会员
x
本帖最后由 RyanBern 于 2015-8-24 12:15 编辑
[box=RoyalBlue]目录
[/box]
第0章节:预备知识 1L(主楼) 第1章节:类 7L √热心朋友的相关补充 23L 第2章节:解密RGSS系统 11L 第3章节:改动游戏对象 14L 第4章节:窗口的使用 19L 第5章节:场景的使用(一) 20L 第6章节:场景的使用(二) 21L 第7章节:尾声 22L
写在前面
一位论坛的朋友和我说,他看过很多RMXP脚本的教程,但是感觉没得到什么帮助,脚本也总写不好。我想这也是很多论坛朋友的共同问题吧,想自己弄个脚本,却无从下手;想看教程,却一头雾水(尤其是游戏里面的F1,感觉要有一定的基础才能理解最主要的部分)。这些天在论坛里面逛了逛,有不少尝试着自制脚本的朋友,但写出的脚本却总也通不过。我也看到了一些朋友写的代码,不得不说,代码中很多错误都是源于对教程的误解和对范例脚本(也就是游戏默认的脚本)的错误移植,游戏默认内置脚本其实是个很好的参考,但是如果不加分析胡乱利用一通,当然是不行的。于是我想到写下这个帖子,帮助那些渴望写出自己脚本的朋友们,成为一个真正的“脚本党”。我其实也算是个写脚本的业余爱好者,同时也在不断挖掘RGSS1更深层次的东西,本贴介绍的,仅仅是RGSS1的冰山一角,但却是我们编写脚本最常用的知识和基础。发出这个东西,不敢说能让大家都成为脚本高手,至少能让大家对RGSS1有个更清楚的认识。以下教程中,大家可能对Ruby和RGSS1有所混淆,Ruby是一门程序设计的语言;而RGSS1是基于Ruby编写的脚本系统,有很多特定的功能。
这里写的是我对写脚本的一些理解,希望各位高手能积极提出意见,有哪里写得不对的地方,还请大家帮忙批评指正。另外大家如果遇到什么相关的问题,可以随时在帖子中询问,只要我有时间,我会立即为大家解答的。(当然求脚本之类的提问我在这里就不处理了,请移步RMXP 提问区)
配套脚本教学视频:https://rpg.blue/thread-381015-1-2.html
【第二版序】
半年前这个教程杀青了,半年后再翻出来,感觉不是很满意,有些地方都没有说清楚。而且后面脚本解读那里,根本就是在利用“抄脚本——写分析”的模式,让人很难有欲望看下去。因此第二版会有一些改动。这半年来又接触了不少编程的技术,对一些问题的看法也和从前大大不同了,因此都写在这里,大家好好探讨一番。这次的改动,注重细节的解说,再加上理论与实践结合的部分(虽然还没有做,大家不要来BS我),想必能比第一版更好吧。
[box=RoyalBlue]第0章节:预备知识
[/box]
很多人觉得计算机很聪明,实际上,它是十分天真的。我们现在所看到它实现的强大功能,其实就是通过有限次的计算来实现的。我们说计算机很傻,是因为我们告诉它什么,它就做什么,因此,我们必须要好好和它沟通,它才能更好地为我们服务。
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/3*3和1*3/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)
a = 2
b = a
a = 3
print b #print 是系统内部的输出函数
a = 2
b = a
a = 3
print b #print 是系统内部的输出函数
(2)
a = [ 1 ,2 ,3 ]
b = a
a[ 2 ] = 4 #这是给数组第2号单元赋值为4
print b
a = [ 1 ,2 ,3 ]
b = a
a[ 2 ] = 4 #这是给数组第2号单元赋值为4
print b
在第一个例子里面,屏幕上将会打印2,在第二个例子里面,屏幕上会打印124。
细心的朋友会发现,在(1)中改变了a的值,但是b的值没变;但是在(2)中就不同了,对a进行的某种操作也会在b那里反映出来。但是,无论是哪种情况,在执行b=a之后,a和b表示的是同一块数据 (指向内存中的同一片区域,即地址),而不是相同数据的不同拷贝 ,或者说,b是a的一个别名,你要找这片内存区域,说a也行,说b也行。
那么,我们应该如何去理解“变量”?在这里我们应该把“变量”都理解成“引用”,它们代表的并不是该数据内容的本身,而是该数据所在的内存地址。把变量的重新赋值理解为指针的指向改变,而数据内容的本身是没有变化的。(这点对Integer之类的东西貌似也是对的,因此你不能说把某一片用于表示“1”的内存区域修改,使其表示“2”,你只能把变量指针的指向从“指向表示'1'的内存区域”改变成“指向表示'2'的内存区域”)
在后面定义函数的时候,也会发生类似的现象。写过程序的朋友知道,函数上面的参数(我们叫做形式参数,简称形参)和实际的变量(我们叫实际参数,简称实参)没有什么关系,对形参的改变丝毫不影响实参的变化。举个例子来说,假如有下面的程序:
def swap( a,b)
t = a
a = b
b = t
end
def swap( a,b)
t = a
a = b
b = t
end
函数的作用是交换a和b两个变量的值,但是如果运行下面的程序:
a = 3
b = 4
swap( a,b)
p a,b
我们会发现a和b的值并没有发生交换,原因就是计算机只是把实际参数的值拷贝 给了形式参数,之后函数内部对形式参数进行的操作与实际参数无关。
但是,我们刚才说过,变量实际表示的就是地址,而我们知道,相同的地址必定指向相同的内存空间,对同一块内存空间进行操作,变量的值当然会发生改变。例如:
def f( a)
a[ 1 ] = 3
end
a = [ 2 ,2 ,3 ]
f( a)
print a
def f( a)
a[ 1 ] = 3
end
a = [ 2 ,2 ,3 ]
f( a)
print 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中,实变量以@开头,通常是跟具体对象关联的。
例如:
class Person
def initialize
@name = "XX"
end
def pr_name
print @name
end
end
print @name
class Person
def initialize
@name = "XX"
end
def pr_name
print @name
end
end
print @name
在这里,@name进入到了函数pr_name的内部,对@name进行访问时,访问的应当是“这个对象的@name”。但是如果在这个类之外写print @name,那么你一定看到的是nil,因为这时@name已经不是Person中的@name。
(3)局部变量
在Ruby中,局部变量就是没有前缀的变量,比如level,x,等等。这一类的作用范围更窄,只是在定义函数内部有效,作为块参数的局部变量只在当前块内有效。在函数外面则是无效的。因此,我们在函数临时需要一个变量,函数结束后完全不需要的时候,就应该用这种变量。另外,函数的形式参数也要用局部变量表示。在这里面说明的一点就是,这种类型的变量是没有默认值的。例如:
def fun
a = 2
return 2 * a
end
def fun2
b = a + 1
return b
end
def fun
a = 2
return 2 * a
end
def fun2
b = a + 1
return b
end
在这里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方法,系统不会在内存中开辟区域。
下面是一个例子:
class A
attr_accessor :x
attr_accessor :y
attr_accessor :z
def initialize( x,y,z)
@x = x
@y = y
@z = z
end
end
class A
attr_accessor :x
attr_accessor :y
attr_accessor :z
def initialize( x,y,z)
@x = x
@y = y
@z = z
end
end
执行: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存储对象的机制。
我们先看下这个脚本:
class Game_Party
#--------------------------------------------------------------------------
# ● 同伴成员的还原
#--------------------------------------------------------------------------
def refresh
# 游戏数据载入后角色对像直接从 $game_actors
# 分离。
# 回避由于载入造成的角色再设置的问题。
new_actors = [ ]
for i in [ email] 0 ...@actors.size [ /email]
if $data_actors [ @actors[ i] .id ] != nil
new_actors.push ( $game_actors[ @actors[ i] .id ] )
end
end
@actors = new_actors
end
end
class Game_Party
#--------------------------------------------------------------------------
# ● 同伴成员的还原
#--------------------------------------------------------------------------
def refresh
# 游戏数据载入后角色对像直接从 $game_actors
# 分离。
# 回避由于载入造成的角色再设置的问题。
new_actors = [ ]
for i in [ email] 0 ...@actors.size [ /email]
if $data_actors [ @actors[ i] .id ] != nil
new_actors.push ( $game_actors[ @actors[ i] .id ] )
end
end
@actors = new_actors
end
end
这个方法出现在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对象,这样一来,同样的数据要写入两遍。读取的时候自然也要读两遍。我们在这里做个测试:
class Scene_Load
#--------------------------------------------------------------------------
# ● 读取存档数据
# file : 读取用文件对像 (已经打开)
#--------------------------------------------------------------------------
def read_save_data( file)
# 读取描绘存档文件用的角色数据
characters = Marshal .load ( file)
# 读取测量游戏时间用画面计数
Graphics.frame_count = Marshal .load ( file)
# 读取各种游戏对像
$game_system = Marshal .load ( file)
$game_switches = Marshal .load ( file)
$game_variables = Marshal .load ( file)
$game_self_switches = Marshal .load ( file)
$game_screen = Marshal .load ( file)
$game_actors = Marshal .load ( file)
$game_party = Marshal .load ( file)
$game_troop = Marshal .load ( file)
$game_map = Marshal .load ( file)
$game_player = Marshal .load ( file)
# 加上这个
p $game_actors [ 1 ] .to_s ,$game_party.actors [ 0 ] .to_s
# 魔法编号与保存时有差异的情况下
# (加入编辑器的编辑过的数据)
if $game_system .magic_number != $data_system .magic_number
# 重新装载地图
$game_map .setup ( $game_map.map_id )
$game_player .center ( $game_player.x , $game_player .y )
end
# 刷新同伴成员
$game_party .refresh
end
end
class Scene_Load
#--------------------------------------------------------------------------
# ● 读取存档数据
# file : 读取用文件对像 (已经打开)
#--------------------------------------------------------------------------
def read_save_data( file)
# 读取描绘存档文件用的角色数据
characters = Marshal .load ( file)
# 读取测量游戏时间用画面计数
Graphics.frame_count = Marshal .load ( file)
# 读取各种游戏对像
$game_system = Marshal .load ( file)
$game_switches = Marshal .load ( file)
$game_variables = Marshal .load ( file)
$game_self_switches = Marshal .load ( file)
$game_screen = Marshal .load ( file)
$game_actors = Marshal .load ( file)
$game_party = Marshal .load ( file)
$game_troop = Marshal .load ( file)
$game_map = Marshal .load ( file)
$game_player = Marshal .load ( file)
# 加上这个
p $game_actors [ 1 ] .to_s ,$game_party.actors [ 0 ] .to_s
# 魔法编号与保存时有差异的情况下
# (加入编辑器的编辑过的数据)
if $game_system .magic_number != $data_system .magic_number
# 重新装载地图
$game_map .setup ( $game_map.map_id )
$game_player .center ( $game_player.x , $game_player .y )
end
# 刷新同伴成员
$game_party .refresh
end
end
在读取数据完毕之后,我们来看看这两个对象的地址。注:所有对象都有to_s方法,如果不加重设的话,返回的是引用的地址。
由于$game_actors实际有意义的数据是从下标[1]开始,因此$game_actors[1]和$game_party.actors[0]是一样的,但是它们的地址却不一样。这就要调用$game_party.refresh,让这两部分引用保持一致性。
大家可以想想,如果不调用$game_party.refresh,会出现什么后果,而此时的内存地址模型又是怎样的?
预备知识就说到这里吧,虽然比较繁琐,但是我觉得在帮助上还是看不到的。这个帖子是连载,有时间我会写后面的内容,那些才是重头戏,大家一起期待之后的帖子吧。