设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 25043|回复: 112
打印 上一主题 下一主题

[原创发布] 【14.7.5第二版更新】RGSS1脚本入门参考

[复制链接]

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

跳转到指定楼层
1
发表于 2013-10-12 22:24:57 | 显示全部楼层 |只看大图 回帖奖励 |倒序浏览 |阅读模式

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

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

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)
RUBY 代码复制
  1. a = 2
  2. b = a
  3. a = 3
  4. print b #print 是系统内部的输出函数

(2)
RUBY 代码复制
  1. a = [1,2,3]
  2. b = a
  3. a[2] = 4 #这是给数组第2号单元赋值为4
  4. print b

       在第一个例子里面,屏幕上将会打印2,在第二个例子里面,屏幕上会打印124。
       细心的朋友会发现,在(1)中改变了a的值,但是b的值没变;但是在(2)中就不同了,对a进行的某种操作也会在b那里反映出来。但是,无论是哪种情况,在执行b=a之后,a和b表示的是同一块数据(指向内存中的同一片区域,即地址),而不是相同数据的不同拷贝,或者说,b是a的一个别名,你要找这片内存区域,说a也行,说b也行。
       那么,我们应该如何去理解“变量”?在这里我们应该把“变量”都理解成“引用”,它们代表的并不是该数据内容的本身,而是该数据所在的内存地址。把变量的重新赋值理解为指针的指向改变,而数据内容的本身是没有变化的。(这点对Integer之类的东西貌似也是对的,因此你不能说把某一片用于表示“1”的内存区域修改,使其表示“2”,你只能把变量指针的指向从“指向表示'1'的内存区域”改变成“指向表示'2'的内存区域”)
       在后面定义函数的时候,也会发生类似的现象。写过程序的朋友知道,函数上面的参数(我们叫做形式参数,简称形参)和实际的变量(我们叫实际参数,简称实参)没有什么关系,对形参的改变丝毫不影响实参的变化。举个例子来说,假如有下面的程序:
RUBY 代码复制
  1. def swap(a,b)
  2.   t = a
  3.   a = b
  4.   b = t
  5. end

函数的作用是交换a和b两个变量的值,但是如果运行下面的程序:
RUBY 代码复制
  1. a = 3
  2. b = 4
  3. swap(a,b)
  4. p a,b

       我们会发现a和b的值并没有发生交换,原因就是计算机只是把实际参数的值拷贝给了形式参数,之后函数内部对形式参数进行的操作与实际参数无关。
       但是,我们刚才说过,变量实际表示的就是地址,而我们知道,相同的地址必定指向相同的内存空间,对同一块内存空间进行操作,变量的值当然会发生改变。例如:
RUBY 代码复制
  1. def f(a)
  2.   a[1] = 3
  3. end
  4. a = [2,2,3]
  5. f(a)
  6. 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中,实变量以@开头,通常是跟具体对象关联的。
例如:
RUBY 代码复制
  1. class Person
  2.   def initialize
  3.     @name = "XX"
  4.   end
  5.   def pr_name
  6.     print @name
  7.   end
  8. end
  9. print @name

       在这里,@name进入到了函数pr_name的内部,对@name进行访问时,访问的应当是“这个对象的@name”。但是如果在这个类之外写print @name,那么你一定看到的是nil,因为这时@name已经不是Person中的@name。
(3)局部变量
       在Ruby中,局部变量就是没有前缀的变量,比如level,x,等等。这一类的作用范围更窄,只是在定义函数内部有效,作为块参数的局部变量只在当前块内有效。在函数外面则是无效的。因此,我们在函数临时需要一个变量,函数结束后完全不需要的时候,就应该用这种变量。另外,函数的形式参数也要用局部变量表示。在这里面说明的一点就是,这种类型的变量是没有默认值的。例如:
RUBY 代码复制
  1. def fun
  2.   a = 2
  3.   return 2 * a
  4. end
  5. def fun2
  6.   b = a + 1
  7.   return b
  8. 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方法,系统不会在内存中开辟区域。
下面是一个例子:
RUBY 代码复制
  1. class A
  2.   attr_accessor :x
  3.   attr_accessor :y
  4.   attr_accessor :z
  5.   def initialize(x,y,z)
  6.     @x = x
  7.     @y = y
  8.     @z = z
  9.   end
  10. 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存储对象的机制。
我们先看下这个脚本:
RUBY 代码复制
  1. class Game_Party
  2.   #--------------------------------------------------------------------------
  3.   # ● 同伴成员的还原
  4.   #--------------------------------------------------------------------------
  5.   def refresh
  6.     # 游戏数据载入后角色对像直接从 $game_actors
  7.     # 分离。
  8.     # 回避由于载入造成的角色再设置的问题。
  9.     new_actors = []
  10.     for i in [email]0...@actors.size[/email]
  11.       if $data_actors[@actors[i].id] != nil
  12.         new_actors.push($game_actors[@actors[i].id])
  13.       end
  14.     end
  15.     @actors = new_actors
  16.   end
  17. 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对象,这样一来,同样的数据要写入两遍。读取的时候自然也要读两遍。我们在这里做个测试:
RUBY 代码复制
  1. class Scene_Load
  2.   #--------------------------------------------------------------------------
  3.   # ● 读取存档数据
  4.   #     file : 读取用文件对像 (已经打开)
  5.   #--------------------------------------------------------------------------
  6.   def read_save_data(file)
  7.     # 读取描绘存档文件用的角色数据
  8.     characters = Marshal.load(file)
  9.     # 读取测量游戏时间用画面计数
  10.     Graphics.frame_count = Marshal.load(file)
  11.     # 读取各种游戏对像
  12.     $game_system        = Marshal.load(file)
  13.     $game_switches      = Marshal.load(file)
  14.     $game_variables     = Marshal.load(file)
  15.     $game_self_switches = Marshal.load(file)
  16.     $game_screen        = Marshal.load(file)
  17.     $game_actors        = Marshal.load(file)
  18.     $game_party         = Marshal.load(file)
  19.     $game_troop         = Marshal.load(file)
  20.     $game_map           = Marshal.load(file)
  21.     $game_player        = Marshal.load(file)
  22.     # 加上这个
  23.     p $game_actors[1].to_s,$game_party.actors[0].to_s
  24.     # 魔法编号与保存时有差异的情况下
  25.     # (加入编辑器的编辑过的数据)
  26.     if $game_system.magic_number != $data_system.magic_number
  27.       # 重新装载地图
  28.       $game_map.setup($game_map.map_id)
  29.       $game_player.center($game_player.x, $game_player.y)
  30.     end
  31.     # 刷新同伴成员
  32.     $game_party.refresh
  33.   end
  34. end

在读取数据完毕之后,我们来看看这两个对象的地址。注:所有对象都有to_s方法,如果不加重设的话,返回的是引用的地址。

       由于$game_actors实际有意义的数据是从下标[1]开始,因此$game_actors[1]和$game_party.actors[0]是一样的,但是它们的地址却不一样。这就要调用$game_party.refresh,让这两部分引用保持一致性。
       大家可以想想,如果不调用$game_party.refresh,会出现什么后果,而此时的内存地址模型又是怎样的?

       预备知识就说到这里吧,虽然比较繁琐,但是我觉得在帮助上还是看不到的。这个帖子是连载,有时间我会写后面的内容,那些才是重头戏,大家一起期待之后的帖子吧。

点评

编辑帖子时就会有这个提示。说明楼主更新了  发表于 2014-7-5 18:54
每次都看见你在“和我同一个楼层”评论,但是每次我都找不到  发表于 2014-7-5 18:54
短消息的内容我已经放在32L,请务必看一看  发表于 2014-7-2 15:48
请查收短消息  发表于 2014-7-2 15:34
回复:无脑之人 是这样的哈,我下了Ruby的源码,发现是从C上移植过来的,因此也没多想,多谢提醒。  发表于 2013-10-13 20:46

评分

参与人数 8星屑 +238 收起 理由
gonglinyuan + 30 精品文章
zmz6668 + 8 精品文章
季长风 + 10 精品文章
jspython + 10 精品文章
秋寒 + 30 塞糖
wingzeroplus + 20 精品文章
feizhaodan + 120 奖赏条例
无脑之人 + 10 精品文章

查看全部评分

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

2
 楼主| 发表于 2013-10-13 20:56:52 | 显示全部楼层
本帖最后由 RyanBern 于 2015-7-31 10:26 编辑

[box=RoyalBlue]
第1章节:类
[/box]

       有了预备知识,我们就可以进入Ruby中运用最广泛的部分——类(class)了。我在这里要说的是,在介绍类之前,我认为大家应该对Ruby的语法有了大致了解,对一些基本的概念也都掌握了,例如控制语句,函数,所以就没有再重复这些东西。如果大家有不明白的,请关注下RMXP的F1,那里面已经说得很清楚了。

1.1  类&实变量&实例方法
1.1.1  类的概念
       类其实是一个比较难说的概念,Ruby是面向对象的编程语言,所谓对象,就是计算机中的抽象数据,而且数据之间有一定的逻辑和结构关系。所谓类,不严格地来讲,就是抽象数据类型。翻开RMXP的F1,我们会发现,所有的类,竟然也都是一个对象,这就是我们所说的“万物皆对象”的观点。打比方来说,人类可以看作是一个类,那么人类的每一个个体就可以看作是一个对象,不知道这样是不是能好理解些。从感性的角度上来说,类应当是对象的一个集合。所有的人就构成了一个人类,而人是人类的一个实例(Instance)。类的上面定义了“属性(Property)”和“方法(Method)”。属性我们可以理解为描述(类生成)对象的性质,比方说人的身高,体重等等;方法可以理解为对象的某种操作,比方说吃饭,喝水,说话等等。对于类本身,还有专属于类的类变量和类方法。另外,类之间有继承关系,有多态性(Polymorphism)。如果你能理解这些,那么理解类也就不难了。
1.1.2  实变量&实例方法
       每一个类的对象(实例)都有属于自己的变量和方法,所谓实例的变量,就是定义在该对象中的,只限于该对象自己使用的变量,实例方法,也就是只有属于这个类的对象才能使用的方法。我们举了例子来具体说明:
RUBY 代码复制
  1. class Person #定义一个叫Person的类
  2.   def initialize
  3.     @name = "Ryan"
  4.     @gender = 'M'
  5.   end
  6.   def hello
  7.     print "I am " + @name + "!"
  8.   end
  9. end

       这个名叫Person的类就定义好了。
       首先说一下,Person是上面我们所说那个“类的类(Class)”中的一个实例,Class类中的实例只有一个方法叫做new,调用方法要用圆点运算符“.”表示。格式是a.method(…),
       其中a是该类的一个实例,method是方法的名字(函数名字,必要的时候要带参数)。
     【关于initialize方法】每个类都能定义一个名叫initialize的方法,这个方法比较特殊,是类的构造方法,或者成为“构造器”。当你使用Xxx.new生成一个类的实例时,便自动调用了这个initialize方法,'.new'后面跟的参数会原封不动地传递到initialize中去。
       刚才我们说过了,Person是Class中的一个实例,那么Person.new的意思就是生成一个Person类的实例。我们接下来插入下面一个代码:
RUBY 代码复制
  1. ryan = Person.new
  2. ryan.hello

       这两句的意思是生成一个Person类的实例,储存在一个变量中,然后调用该类的方法。因此执行完这两句之后,屏幕上将会打印出I am Ryan!的字符串。
       在上一章节我们已经说过,如果一个变量是对象的一部分数据,那么要把它定义成实变量的形式(即@开头的变量),这样定义出来的变量在整个类的内部都是有效的。试想如果把上面的语句中所有的@都去掉,那么后果将会是……

给initialize方法添加参数

       我们回头再来看看这个程序片段。
RUBY 代码复制
  1. class Person #定义一个叫Person的类
  2.   def initialize
  3.     @name = "Ryan"
  4.     @gender = 'M'
  5.   end
  6.   def hello
  7.     print "I am " + @name + "!"
  8.   end
  9. end

       我们会发现,这个类别建立好了之后,所有的实变量的值都是固定的,这就导致我们只能建立相同内容的实例,因此我们要稍稍改装一下,让建立的实例拥有不同的初值。
RUBY 代码复制
  1. class Person #定义一个叫Person的类
  2.   def initialize(name,gender)
  3.     @name = name
  4.     @gender = gender
  5.   end
  6.   def hello
  7.     print "I am " + @name + "!"
  8.   end
  9. end

       这里的initialize是所有类一般要定义的方法,在一些面向对象的语言中,把这个方法叫做“构造方法”或者“构造器”。实际上,Person.new这个语句已经在调用initialize,所有类的初始化函数名字必须是initialize,这也是Ruby的规定。这里的initialize带有2个参数,我们回忆一下函数参数的概念,就会发现@name和name根本不是一回事。这样定义好了之后,我们可以写下面的语句了。
RUBY 代码复制
  1. ryan = Person.new("Ryan",'M')
  2. ryan.hello

       这样和刚才的效果相同,但是我们可以随便设置实例的初值。

实变量公开化

       虽然我们可以用上面定义的类生成一个个实例,但是我们想一下这种情况:有一天Ryan出门了,见到了一个漂亮的女孩,因为我们定义了hello方法,所以Ryan很轻松地向她打了个招呼。女孩于是问:"Ryan"这个名字怎么拼啊?Ryan这时却抓耳挠腮,因为Ryan还没学会怎么让对方知道自己的名字拼写。再比如,Ryan觉得这个名字太简单了,想改成"RyanBern"(依然很矬),但是依然没有办法改变自己的名字。解决上面两个问题,就要用我们的实变量公开化。
RUBY 代码复制
  1. class Person #定义一个叫Person的类
  2.   def initialize(name,gender)
  3.     @name = name
  4.     @gender = gender
  5.   end
  6.   def hello
  7.     print "I am " + @name + "!"
  8.   end
  9.   ####
  10.   def name
  11.     return @name
  12.   end
  13.   def name=(name)
  14.     @name = name
  15.   end
  16.   ####
  17. end

       我们加入了两个方法,第一个函数是返回实例中@name的值,第二个函数的作用恰恰是修改实例中@name的值。
       我们注意到,这两个方法的名称比较奇特,和@name这个实变量很相似。实际上,第一个方法叫做'name',第二个方法叫做'name=',并且带一个参数。你可能会问了,怎么会有名字这么奇怪的方法?这种方法在C++/Java中都是没有的(除非你想操作符重载,但是即使重载了,效果也不如Ruby里面的这个好),而Ruby之所以这样做,是出于对实变量保护隐藏的目的。我们在类对象的外面,是没有办法修改对象内部的变量的,如果真的需要修改,那么只能通过定义方法的形式。注意,第二个函数开头的两个name是不一样的,前面那个是方法名字,括号里面的是形参名字。
       有了这两个函数,我们可以写(假设Ryan已经定义好了):
RUBY 代码复制
  1. nam = ryan.name #将ryan的@name赋值给变量nam
  2. ryan.name += "Bern" #将ryan的@name后面加个"Bern"


       这里注意,上面的例子中第二句同时调用了'name'方法和'name='方法,请仔细思考一下。

       这种通过定义方法来公开变量,是Ruby中最常见的。实际上,定义这样的函数之后,可以把被定义的变量看作是类的一个属性,我们可以获取属性的值或者修改属性的值(但严格来讲,【属性】这个概念并不存在于Ruby当中)。一种简单的理解,就是把'name','name=','@name'联系在一起,'name'方法返回'@name'的值,'name='方法修改'@name'的值。将原点运算符'.'理解为“的”,那么a.name就可以理解为某人的名字。但是我们要知道,其实圆点运算符表示的就是类方法的调用,加上属性的概念是为了突出这种方法的特殊地位。这样,相关的实变量便可以通过外部访问。

        当然,这样编写可能会浪费大量的篇幅,我们在这里有比较简单的形式来代替上面的两个方法。
RUBY 代码复制
  1. attr_accessor :name
  2. attr_reader :name
  3. attr_writer :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。啥?为啥说它是抽象类?我们看看下面的代码。
RUBY 代码复制
  1. class Game_Battler
  2.   #--------------------------------------------------------------------------
  3.   # ● 获取 MaxHP
  4.   #--------------------------------------------------------------------------
  5.   def maxhp
  6.     n = [[base_maxhp + @maxhp_plus, 1].max, 999999].min
  7.     for i in @states
  8.       n *= $data_states[i].maxhp_rate / 100.0
  9.     end
  10.     n = [[Integer(n), 1].max, 999999].min
  11.     return n
  12.   end
  13. end

       这是定义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吗?显然没有。

第二版更新·完
       当然,如果一个类的方法和属性定义不能满足我们的需求,而我们又不想再定义一个子类,这时候我们就要对原有的类进行扩充。当然,你可以在原来的类上面进行修改,不过,如果是系统整合的话,我们会采用以下的方式:
RUBY 代码复制
  1. class Person
  2.   def goodbye
  3.     print "See you!"
  4.   end
  5. end

       注意,Person我们已经在前面定义好了,但是你完全可以再写一遍class Person,表示对该类进行追加定义。

方法重写(Override)
       如果一个方法已经在父类被定义过,在子类再次对它定义(通常利用父类已有的方法)就叫做方法的重写。例如:
RUBY 代码复制
  1. class Student < Person
  2.   def hello
  3.     print "I am " + @name + ", and I am a student."
  4.   end
  5. end

这里我们重写了方法hello,那么在调用student.hello的时候,屏幕上显示的就是新定义叙述的内容(即后面多了and I am a student)。
       不过,这顶多算是把原来的方法覆盖掉了,重写的味道还不是很浓。我们在定义子类方法的时候,通常是对父类的同名方法的扩充,这就要用到关键字super,例如:
RUBY 代码复制
  1. class Student < Person
  2.   attr_accessor :student_id #定义新属性,学号
  3.   def intialize(name,gender,student_id)
  4.     super(name,gender)
  5.     @student_id = student_id
  6.   end
  7. end

       在这里,我们调用了Person类的initialize的方法,利用的是关键字super,当然参数什么的不能少。为什么不能写initialize(name,gender)呢?如果这样的话,系统会认为这是一种递归定义,即函数调用自身的过程(Ruby中允许递归定义函数),而并不是调用父类的同名方法,因此我们必须采用这样的形式。

       注意,super关键字的使用有一定特殊性,在使用super的时候,要格外注意参数问题。在这里推荐,即使是被调用的方法没有参数,也要跟一对空括号'()'来表示没有参数,而不能什么都不带直接写super。这是因为,如果super后面什么都不跟,那么默认传进去的参数和正在定义的方法相同,这有可能引起错误。例如:
RUBY 代码复制
  1. # 定义父类 A
  2. class A
  3.   def initialize
  4.     @a = 0
  5.   end
  6. end
  7. # 定义子类 B,这里 B 的 initialize 方法中,super的使用是错误的!!
  8. class B < A
  9.   def initialize(b)
  10.     super
  11.     @b = b
  12.   end
  13. end
  14. # 定义子类 C,这里 C 的 initialize 方法中,super的使用是正确的
  15. class C < A
  16.   def initialize(c)
  17.     super()
  18.     @c = c
  19.   end
  20. end

       如果有上面的定义,那么在使用B.new时,就会发生Argument Error。

方法的覆盖
       如果一个方法在一个类中被定义,再次对其定义就叫做方法的覆盖。例如
RUBY 代码复制
  1. class A
  2.   def test
  3.     p 1
  4.   end
  5. end
  6. a = A.new
  7. a.test # => 1
  8. class A
  9.   def test
  10.     p 2
  11.   end
  12. end
  13. a = A.new
  14. a.test # => 2

       在这里 A 类的方法'test'被定义了两次,那么后定义的会覆盖之前定义的。如果调用A.new.test,实际调用的方法取决于这句话的位置,如果在第一个test后第二个test前使用,那么实际调用的就是覆盖前的方法;如果在第二个test后使用,那么实际调用的就是覆盖后的方法。
       在这一小部分的最后,我想买个关子,在方法的定义和重定义中,还有一个重要的“别名”机制alias,这个东西的存在,为我们写脚本带来了更大灵活性,那么关于alias,我们要放到后面的章节进行讲解,这里就先不说了。

       应大家的要求,我们在这里布置一道小练习题。

       练习:请定义一个表示水瓶的类,水瓶有两个属性,一是最大容积(用一个正整数表示),二是当前水瓶中盛放水的量(也用一个非负整数表示,不得大于最大容积)。在这个类上面定义三个类方法:1.将一个水瓶装满水;2.清空一个水瓶里面的水;3.将这个水瓶自身里面的水倒入另一个水瓶,注意,不是随机地倒,倒完之后,保证自身是空的或者对方是满的。
       定义类class Bottle并验证你定义的方法。

第二版新增
*1.1.4  模块(module)简介
       首先,什么是模块?模块是用于实现某些特定功能的代码的组合。它与类不同,和类相比较,模块内部定义了一些常量和方法,从完整性看,模块不如类完整。不过模块内部可以定义内部类,这样定义的好处是让类的作用更加清楚。

       如何定义一个模块?
       利用关键字module可以定义一个模块。
RUBY 代码复制
  1. module Action
  2.   WORD_ON = "On"
  3.   WORD_OFF = "Off"
  4.   def turn_on
  5.      print WORD_ON
  6.   end
  7.   def turn_off
  8.      print WORD_OFF
  9.   end
  10. end

       这就是一个简单模块的定义。从这几句代码来看,定义了两个字符串常量和两个(普通)方法,而这两个方法,从名字来看是“打开”,“关闭”。可是这有什么用?我们知道,我们可以打开收音机,可以打开电脑,也可以打开煤气灶。那么,对于这几个类,都有相应的“打开”和“关闭”方法。一个个定义显然不妥,我们想到了定义父类。收音机和电脑可以归入到“家用电器(Appliance)”类中,但是煤气灶无论如何也不是什么家用电器。这可咋办?那就定义到更高的父类中,把家用电器和“煤气灶所属的类”归到一个类上去。那我们叫它“家庭用品(Utensils)”类。但是在这个类定义turn_on和turn_off方法,问题就更多了。家庭用品不但包括家用电器,煤气灶,还包括床,被单啥的,难道它们也可“打开关闭”?显然不行。那咋办?
       我们经过观察,可以发现,turn_on和turn_off只是两个特定的功能,和类什么的关系不是很大。于是我们把它定义到模块当中,然后在类中把这个模块糅合进去(Mix-in)。这样,类中便有了模块中实现的方法。

       要在类中糅合模块,要用include方法。
RUBY 代码复制
  1. class Radio < Appliance
  2.   include Action
  3.   attr_reader :brand
  4.   def initialize(brand)
  5.     @brand = brand
  6.   end
  7. end
  8. ra = Radio.new("ChangHong")
  9. ra.turn_on # => "On"
  10. ra.turn_off # => "Off"

       这样Radio对象就可以使用Action模块里面的功能。

       啥?不会用?那么我再说个简单的用法好了。Module可以当作命名空间(namespace)使用,主要是各种参数和常量的定义。啥?直接定义在外面?这可不好,万一别人定义的和你自己定义的重复了就麻烦了,还是放到命名空间里面好。我们看看著名的Fuki对话框脚本:
RUBY 代码复制
  1. module FUKI
  2.  
  3. # 头像图片保存目录的设定
  4. HEAD_PIC_DIR = "Graphics/Heads/"
  5.  
  6. # 是否显示尾部图标
  7. TAIL_SHOW = true
  8.  
  9. # Skin的设定
  10. # 使用数据库默认窗口Skin情况下这里使用[""]
  11. FUKI_SKIN_NAME = "skin3"   # 呼出对话框用Skin
  12. NAME_SKIN_NAME = "skin3"   # 角色名字窗口用Skin
  13.  
  14. # 字体大小
  15. MES_FONT_SIZE = 22    # 呼出对话框
  16. NAME_FONT_SIZE = 14   # 角色名字窗口
  17.  
  18. # 字体颜色
  19. #(设定为 Color.new(0, 0, 0, 0) 表示使用普通文字色)
  20. FUKI_COLOR = Color.new(255, 255, 255, 255)  # 呼出对话框
  21. NAME_COLOR = Color.new(255, 255, 255, 255)  # 角色名字窗口
  22.  
  23. # 窗口透明度
  24. # 如修改窗口透明度请同时修改尾部箭头图形内部的透明度
  25. FUKI_OPACITY = 255    # 呼出对话框
  26. MES_OPACITY = 255     # 默认信息窗口
  27. NAME_OPACITY = 255    # 角色名字窗口
  28.  
  29. # 角色名字窗口的相对位置
  30. NAME_SHIFT_X = 0      # 横坐标
  31. NAME_SHIFT_Y = 16     # 纵坐标
  32.  
  33. # 窗口表示时是否根据画面大小自动检查窗口出现的位置,
  34. # 自动改变位置( true / false )
  35. # 设置成 true 可能出现箭头图标颠倒的问题 <- bbschat
  36. POS_FIX = false
  37.  
  38. # 在画面最边缘表示时的稍微挪动
  39. # 使用圆形Skin的角和方框的角重合的情况下为 true
  40. CORNER_SHIFT = false
  41. SHIFT_PIXEL = 4   # true 时挪动的象素
  42.  
  43. # 角色高度尺寸
  44. CHARACTOR_HEIGHT = 48
  45. # 呼出对话框的相对位置(纵坐标)
  46. POP_SHIFT_TOP = 0         # 表示位置为上的时候
  47. POP_SHIFT_UNDER = 0       # 表示位置为下的时候
  48.  
  49. # 信息的表示速度(数字越小速度越快,0为瞬间表示)
  50. # 游戏中 可随时用数字代入 $mes_speed 来改变消息表示速度。
  51. MES_SPEED = 1
  52.  
  53. end

       Fuki脚本把这些常量都定义到module当中了,他为啥不定义在最外面?显然是考虑冲突的问题。因此我们写脚本也要有这样的习惯,把常量都定义到模块中,不要定义在外面,更不要定义成全局变量。例如(某升级提示脚本):
RUBY 代码复制
  1. $不显示升级窗口 = 88

       开始觉得这样没什么,后来越看越觉得不好。

       想要在别的类引用模块内部的常量,要用到'::'运算符。
RUBY 代码复制
  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| ...}
    具体块中的操作自己设定。
    例如:
    RUBY 代码复制
    1. a = (1..100).to_a
    2. s = 0
    3. a.each{|n| s += n}
    4. p s # => 5050

    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默认的系统,了解一下整个程序究竟是怎样工作的,大家就敬请期待吧!
      

点评

到了RGSS3不都是用each了吗  发表于 2013-10-19 21:08
回复:无脑之人 相应概念已经改正,多谢。  发表于 2013-10-14 12:48
根本上是类作为对象而被定义的特殊方法,类变量也是一样,是@@开头属于类的,@开头的叫做实例变量  发表于 2013-10-14 12:25
突然发现一个问题,「该类的方法」与「类方法」不同,该类的方法是由该类的实例所调用的,而类方法是由类自己调用【类名.方法名】  发表于 2013-10-14 12:24
表示从本章开始有30%的内容看不太懂了  发表于 2013-10-14 09:50
回复 支持 反对

使用道具 举报

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

3
 楼主| 发表于 2013-10-14 12:50:18 | 显示全部楼层
wingzeroplus 发表于 2013-10-13 10:45
原来数组居然不会被计算机复制一份,今天才知道,受教了(虽然怎么用过数组) ...

哪里不明白还是尽管在帖子里面提出来,类这个地方一定要好好理解。
回复 支持 反对

使用道具 举报

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

4
 楼主| 发表于 2013-10-14 20:44:56 | 显示全部楼层
本帖最后由 RyanBern 于 2015-7-31 11:25 编辑

[box=RoyalBlue]
第2章节:解密RGSS系统
[/box]

       在这一章节我们来全局地了解一下我们的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打开,就要写:
RUBY 代码复制
  1. $game_self_switches[[1, 2, 'A']] = true

       注意,最后一个参数是单引号引出的大写字母。
       我们知道,如果利用事件编辑器,只能对本事件的独立开关进行操作,但是,有了上面这个语句,我们便可以在一个事件中对另一个事件的独立开关进行操作,这样能解决很多问题。

那么在这里我们留下一个小小的作业。
       【踩冰机关】请尽量用事件编写一个小游戏,主角踏入一片奇怪的冰地,冰地上有一个入口和一个出口,只有主角把规定区域内所有的地面都踩过一次(规定不允许重复踩踏规定区域内的任何一个地面,即所有地面必须踩一次而且只能踩一次),出口才会打开,否则出口不会打开。当主角踩踏某一块地面两次时,宣布失败,利用场所移动将主角传送到入口,并且重置地面(如果主角没有把地面全踩一遍就走到出口,按照规则,出口是封闭的,这时候主角只能第二次踩出口的那块地面,因此游戏也会失败)。允许使用的脚本仅限于对独立开关的操作。小提示:实际上,在地面块数比较少的情况下,利用开关也是可以做到的,但是开关的数量实在是太少,会造成很多浪费,在这里我们会看出独立开关的优越性。

       最后要说的是,这三个全局变量,每次存档后,都会写入存档文件里面,利用这个特性,我们可以做出很多新的脚本,具体的还要到后面我们再说。

       还有就是队伍,角色,敌人队伍,敌人。
  • Game_Battler——游戏战斗者的类,分成3部分定义,是Game_Actor和Game_Enemy的父类
  • Game_Actor——角色的类
  • Game_Enemy——敌人的类
  • Game_Troop——敌人队伍的类
  • Game_Party——角色队伍的类
  • Game_Actors——角色排列的类
  • 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精灵 活动块 活动块组

       实际上,游戏中显示到屏幕上的图片,都是由这些类生成的,不要以为$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组脚本里面都有什么:
RUBY 代码复制
  1. begin
  2.   # 准备过渡
  3.   # 设置系统默认字体
  4.   Font.default_name = (["黑体"])
  5.   Graphics.freeze
  6.   # 生成场景对像 (标题画面)
  7.   $scene = Scene_Title.new
  8.   # $scene 为有效的情况下调用 main 过程
  9.   while $scene != nil
  10.     $scene.main
  11.   end
  12.   # 淡入淡出
  13.   Graphics.transition(20)
  14. rescue Errno::ENOENT
  15.   # 补充 Errno::ENOENT 以外错误
  16.   # 无法打开文件的情况下、显示信息后结束
  17.   filename = $!.message.sub("No such file or directory - ", "")
  18.   print("找不到文件 #{filename}。 ")
  19. end

       一开始先简短设置系统字体,准备画面过渡。
       然后就直接进入场景画面。
       注意,那个$scene是伴随这程序始终进行的全局变量,表示的就是当前场景本身。中间那个while循环是说,如果$scene的值不是nil,那么就调用$scene的main方法。也就是说,如果想退出这个循环,直接输入$scene = nil即可,这样的话,不但场景会退出,主函数也会跟着结束,因此整个游戏就退出了。
       在这里$scene一开始被赋予Scene_Title.new,也就是说我们即将进入标题画面,调用的也是标题画面的main方法。
       我们于是打开Scene_Title,在这里我就不放全部代码了。可以看到有下面的语句:
RUBY 代码复制
  1. $data_actors        = load_data("Data/Actors.rxdata")
  2. $data_classes       = load_data("Data/Classes.rxdata")
  3. $data_skills        = load_data("Data/Skills.rxdata")
  4. $data_items         = load_data("Data/Items.rxdata")
  5. $data_weapons       = load_data("Data/Weapons.rxdata")
  6. $data_armors        = load_data("Data/Armors.rxdata")
  7. $data_enemies       = load_data("Data/Enemies.rxdata")
  8. $data_troops        = load_data("Data/Troops.rxdata")
  9. $data_states        = load_data("Data/States.rxdata")
  10. $data_animations    = load_data("Data/Animations.rxdata")
  11. $data_tilesets      = load_data("Data/Tilesets.rxdata")
  12. $data_common_events = load_data("Data/CommonEvents.rxdata")
  13. $data_system        = load_data("Data/System.rxdata")

       这就是在游戏一开始,载入所有游戏数据库内所有数据,并保存在相应的全局变量里面。这些全局变量不会被更改,一直存在直到游戏退出。
RUBY 代码复制
  1. def command_new_game
  2.     # 演奏确定 SE
  3.     $game_system.se_play($data_system.decision_se)
  4.     # 停止 BGM
  5.     Audio.bgm_stop
  6.     # 重置测量游戏时间用的画面计数器
  7.     Graphics.frame_count = 0
  8.     # 生成各种游戏对像
  9.     $game_temp          = Game_Temp.new
  10.     $game_system        = Game_System.new
  11.     $game_switches      = Game_Switches.new
  12.     $game_variables     = Game_Variables.new
  13.     $game_self_switches = Game_SelfSwitches.new
  14.     $game_screen        = Game_Screen.new
  15.     $game_actors        = Game_Actors.new
  16.     $game_party         = Game_Party.new
  17.     $game_troop         = Game_Troop.new
  18.     $game_map           = Game_Map.new
  19.     $game_player        = Game_Player.new
  20.     # 设置初期同伴位置
  21.     $game_party.setup_starting_members
  22.     # 设置初期位置的地图
  23.     $game_map.setup($data_system.start_map_id)
  24.     # 主角向初期位置移动
  25.     $game_player.moveto($data_system.start_x, $data_system.start_y)
  26.     # 刷新主角
  27.     $game_player.refresh
  28.     # 执行地图设置的 BGM 与 BGS 的自动切换
  29.     $game_map.autoplay
  30.     # 刷新地图 (执行并行事件)
  31.     $game_map.update
  32.     # 切换地图画面
  33.     $scene = Scene_Map.new
  34.   end

       以新游戏命令为例,当玩家选择“新游戏”时,系统会做以上工作。
       其中我们看到直到这里才会生成各个游戏对象,如果是载入,那么就会载入各种游戏对象。
       最后一步将场景切换到地图场景中。这里把$scene变成Scene_Map.new,在所有场景的main函数中,都有一个无限循环loop do,其中有一句:
RUBY 代码复制
  1. if $scene != self
  2.   break
  3. end
这里的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,大家就敬请期待吧。

回复 支持 反对

使用道具 举报

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

5
 楼主| 发表于 2013-10-19 12:11:17 | 显示全部楼层
本帖最后由 RyanBern 于 2015-8-1 19:53 编辑

[box=RoyalBlue]
第3章节:游戏对象的解读和应用
[/box]

       从这一章节开始我们要具体说说如何在原脚本的基础上改动脚本,甚至你可以自己做出一个脚本。如果你前面的概念不是很明白的话,没有关系,不过只要你关注这里的话,相信你一定能够成功写出一个脚本的。
3.1  战斗者(Game_Battler) 角色队伍(Game_Party)
3.1.1  Game_Battler Game_Actor (角色类)Game_Enemy(敌人类)
       作为可以出现在战斗中的对象,RGSS将角色和敌人分别定义,再根据他们的共同性质(比方说都有HP,都可以进行技能伤害等等)定义出一个父类Game_Battler。那我们先翻开Game_Battler,看看这里面的结构究竟是什么。
RUBY 代码复制
  1. attr_reader   :battler_name             # 战斗者 文件名
  2.   attr_reader   :battler_hue              # 战斗者 色相
  3.   attr_reader   :hp                       # HP
  4.   attr_reader   :sp                       # SP
  5.   attr_reader   :states                   # 状态
  6.   attr_accessor :hidden                   # 隐藏标志
  7.   attr_accessor :immortal                 # 不死身标志
  8.   attr_accessor :damage_pop               # 显示伤害标志
  9.   attr_accessor :damage                   # 伤害值
  10.   attr_accessor :critical                 # 会心一击标志
  11.   attr_accessor :animation_id             # 动画 ID
  12.   attr_accessor :animation_hit            # 动画 击中标志
  13.   attr_accessor :white_flash              # 白色屏幕闪烁标志
  14.   attr_accessor :blink                    # 闪烁标志
  15.   #--------------------------------------------------------------------------
  16.   # ● 初始化对像
  17.   #--------------------------------------------------------------------------
  18.   def initialize
  19.     @battler_name = ""
  20.     @battler_hue = 0
  21.     @hp = 0
  22.     @sp = 0
  23.     @states = []
  24.     @states_turn = {}
  25.     @maxhp_plus = 0
  26.     @maxsp_plus = 0
  27.     @str_plus = 0
  28.     @dex_plus = 0
  29.     @agi_plus = 0
  30.     @int_plus = 0
  31.     [url=home.php?mod=space&uid=292300]@Hidden[/url] = false
  32.     @immortal = false
  33.     @damage_pop = false
  34.     @damage = nil
  35.     @critical = false
  36.     @animation_id = 0
  37.     @animation_hit = false
  38.     @white_flash = false
  39.     @blink = false
  40.     @current_action = Game_BattleAction.new
  41.   end

我们先看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进行更改就会变得很复杂,倒不如直接一次性取得所有的因素。
RUBY 代码复制
  1. def maxhp
  2.     n = [[base_maxhp + @maxhp_plus, 1].max, 999999].min
  3.     for i in @states
  4.       n *= $data_states[i].maxhp_rate / 100.0
  5.     end
  6.     n = [[Integer(n), 1].max, 999999].min
  7.     return n
  8.   end

       这里我们可以看到,获取maxhp的第一步,是计算基础maxhp和加成maxhp的和,再进行范围修正。第二步是进行状态的修正,我们知道,在数据库中的状态一栏,我们可以通过状态来改变maxhp值。最后那个n = [[Integer(n),1].max,999999].min表示先把n变成整数,然后再进行属性修正。
       在这里,我们发现在第二行有个base_maxhp,很多人会好奇这是个什么东西,不是实变量,在Game_Battler里面也找不到相应的方法,更不可能是局部变量。其实,这个东西确实是一个方法,但是不是在Game_Battler中,而是在它的两个子类Game_Actor和Game_Enemy中,因为两个子类对应的方法不一样,所以要分开定义。这就意味着虽然这个方法看似在这个类中没出现,但是如果我们在其子类中定义了它,则仍然可以在子类中使用。在后面我们会看到它的庐山真面目。
RUBY 代码复制
  1. def make_action_speed
  2.     @current_action.speed = agi + rand(10 + agi / 4)
  3.   end

       这个方法的作用是决定战斗者的行动速度,即速度越大的越先行动。agi是战斗者的速度,后面那个是上下浮动的随机分散值。这就说明了一个战斗者速度对行动先后的重要性。这也能说明,速度大的战斗者行动的顺序很可能排到前面(受到后面浮动随机分散的影响)。因此我们如果要更改这种排序模式,改变这个方法就OK。
       然后就是这个增加状态的方法。
RUBY 代码复制
  1. def add_state(state_id, force = false)
  2.     # 无效状态的情况下
  3.     if $data_states[state_id] == nil
  4.       # 过程结束
  5.       return
  6.     end
  7.     # 无法强制附加的情况下
  8.     unless force
  9.       # 已存在的状态循环
  10.       for i in @states
  11.         # 新的状态和已经存在的状态 (-) 同时包含的情况下、
  12.         # 本状态不包含变化为新状态的状态变化 (-)
  13.         # (ex : 战斗不能与附加中毒同时存在的场合)
  14.         if $data_states[i].minus_state_set.include?(state_id) and
  15.            not $data_states[state_id].minus_state_set.include?(i)
  16.           # 过程结束
  17.           return
  18.         end
  19.       end
  20.     end
  21.     # 无法附加本状态的情况下
  22.     unless state?(state_id)
  23.       # 状态 ID 追加到 @states 序列中
  24.       @states.push(state_id)
  25.       # 选项 [当作 HP 0 的状态] 有效的情况下
  26.       if $data_states[state_id].zero_hp
  27.         # HP 更改为 0
  28.         @hp = 0
  29.       end
  30.       # 所有状态的循环
  31.       for i in 1...$data_states.size
  32.         # 状态变化 (+) 处理
  33.         if $data_states[state_id].plus_state_set.include?(i)
  34.           add_state(i)
  35.         end
  36.         # 状态变化 (-) 处理
  37.         if $data_states[state_id].minus_state_set.include?(i)
  38.           remove_state(i)
  39.         end
  40.       end
  41.       # 按比例大的排序 (值相等的情况下按照强度排序)
  42.       @states.sort! do |a, b|
  43.         state_a = $data_states[a]
  44.         state_b = $data_states[b]
  45.         if state_a.rating > state_b.rating
  46.           -1
  47.         elsif state_a.rating < state_b.rating
  48.           +1
  49.         elsif state_a.restriction > state_b.restriction
  50.           -1
  51.         elsif state_a.restriction < state_b.restriction
  52.           +1
  53.         else
  54.           a <=> b
  55.         end
  56.       end
  57.     end
  58.     # 强制附加的场合
  59.     if force
  60.       # 设置为自然解除的最低回数 -1 (无效)
  61.       @states_turn[state_id] = -1
  62.     end
  63.     # 不能强制附加的场合
  64.     unless  @states_turn[state_id] == -1
  65.       # 设置为自然解除的最低回数
  66.       @states_turn[state_id] = $data_states[state_id].hold_turn
  67.     end
  68.     # 无法行动的场合
  69.     unless movable?
  70.       # 清除行动
  71.       @current_action.clear
  72.     end
  73.     # 检查 HP 及 SP 的最大值
  74.     @hp = [@hp, self.maxhp].min
  75.     @sp = [@sp, self.maxsp].min
  76.   end
  77.   def states_plus(plus_state_set)
  78.     # 清除有效标志
  79.     effective = false
  80.     # 循环 (附加状态)
  81.     for i in plus_state_set
  82.       # 无法防御本状态的情况下
  83.       unless self.state_guard?(i)
  84.         # 这个状态如果不是 full 的话就设置有效标志
  85.         effective |= self.state_full?(i) == false
  86.         # 状态为 [不能抵抗] 的情况下
  87.         if $data_states[i].nonresistance
  88.           # 设置状态变化标志
  89.           @state_changed = true
  90.           # 附加状态
  91.           add_state(i)
  92.         # 这个状态不是 full 的情况下
  93.         elsif self.state_full?(i) == false
  94.           # 将状态的有效度变换为概率、与随机数比较
  95.           if rand(100) < [0,100,80,60,40,20,0][self.state_ranks[i]]
  96.             # 设置状态变化标志
  97.             @state_changed = true
  98.             # 附加状态
  99.             add_state(i)
  100.           end
  101.         end
  102.       end
  103.     end
  104.     # 过程结束
  105.     return effective
  106.   end

       这是将战斗者自身增加状态的两个方法,第二个方法调用了第一个方法,而第一个方法是增加单个状态ID为state_id的状态,先是判断状态是否无效(即定义到了$data_states之外),然后是判断战斗者身上有没有抵抗这种状态的另一种状态(例如,“战斗不能”状态会解除所有其他的状态,如果给一个“战斗不能”的战斗者附加别的状态,不会成功),如果有则附加无效,然后是附加这个状态,并且产生一定的效果,然后对@state进行排序,排序的依据是状态的优先级,也就是下图:

       第二个方法是真正的增加状态方法,可以增加一组状态,并且有成功率判定的条件。先是判定是否防御了这个状态(如果角色穿上了相应装备就会进入这一过程),如果不能防御,看是否状态是“不能抵抗”,否则按照ABCDEF的有效度进行判定。A是1,B是2,等等(注意,F1中对此处的解释有误!)。因此,如果想改ABCDEF代表的成功率,就要改下面这一行:
RUBY 代码复制
  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号道具,就在函数的第一行写下这一句话就可以:
RUBY 代码复制
  1. if skill_id == 1 and $game_party.item_number(2) < 3
  2.   return false
  3. end

       这样,1号技能就有了个特殊判断,当然,要在1号技能的公共事件上设置使用后消耗3个2号物品即可,这个相信大家都会做。
例2:技能伤害加成
       这个就是修改skill_effect方法就可以了,注意它的两个参数分别是使用者和特技,进行相应调整就可以。比方说,如果角色持有含属性[火]的武器,释放含有属性[火]的技能,则获得20%的伤害加成,就可以写(假如火属性的ID为1):
RUBY 代码复制
  1. if self.is_a?(Game_Actor) && $data_weapons[self.weapon_id].element_set.include?(1) && skill.element_set.include?(1)
  2.   self.damage += self.damage / 5
  3. end

       这里的第一个条件self.is_a?(Game_Actor)不能省略,因为对于敌人类(Game_Enemy)来讲,是没有weapon_id这个属性的,会引发NoMethodError。这个is_a?是判断某个实例是否属于某个类的函数,对任意的实例都有效。例如:
RUBY 代码复制
  1. class A
  2. end
  3. class B < A
  4. end
  5. class C < A
  6. end
  7. ryan = B.new
  8. ryan.is_a?(A) #true 这里B类的实例当然属于A类
  9. ryan.is_a?(B) #true
  10. ryan.is_a?(C) #false ryan不是C类的实例

       这个语句希望大家能够熟练使用,很多判断没有这句话,似乎是很难完成的。后面的那些语法,参考F1就基本能够写出。

       接下来我们介绍一下Game_Actor,作为角色的类,有它的特殊性。
       我们看一下专属于Game_Actor类的变量。
RUBY 代码复制
  1. attr_reader   :name                     # 名称
  2.   attr_reader   :character_name           # 角色 文件名
  3.   attr_reader   :character_hue            # 角色 色相
  4.   attr_reader   :class_id                 # 职业 ID
  5.   attr_reader   :weapon_id                # 武器 ID
  6.   attr_reader   :armor1_id                # 盾 ID
  7.   attr_reader   :armor2_id                # 头防具 ID
  8.   attr_reader   :armor3_id                # 身体体防具 ID
  9.   attr_reader   :armor4_id                # 装饰品 ID
  10.   attr_reader   :level                    # 水平
  11.   attr_reader   :exp                      # EXP
  12.   attr_reader   :skills                   # 特技

       中间那个character_name和character_hue指的是角色行走图的文件名和色相,而不是战斗图的文件名和色相。在这里看出,默认系统里面,角色类才有装备,等级,经验的刻画。
我们再向下看,就会看到属于Game_Actor的各种方法。前面说到的base_maxhp也在其中。具体的我就不领着大家一个个说了,只举一个例子。
例:简单的装备附带技能系统
       这又是一个热门话题,不过很多人都解决过它。实际上,如果我们学习了这里,我们也能比较轻松地解决它,下面请看。(以下提供的解决办法还有很大提升空间,但是这个办法是最容易想到的办法)
步骤1:明确我们要做什么,如果给角色装备相应的装备,就让它学习相应的技能,卸下装备就遗忘相应的技能。注意:装备中的技能最好不要交叉(即两种不同种类的装备含有相同的技能),这样能让我们的系统简单一些,而且装备技能不可从其他渠道学习(即只能通过换上这种装备才能使用)。
步骤2:先要设定装备和技能的对应关系,有点像新数据库的构造,我们可以采用Hash表的方式构造一个数据库中无法设定的新数据。考虑到这个数据在全局都可以使用,应该定义成全局变量,而且定义在最外面的结构里面就可以了。
RUBY 代码复制
  1. $equipment_skill_table = {} #在这里定义的是Hash表
  2. $equipment_skill_table[[0,1]] = [1,2] #这里定义1号武器的附带技能为1号和2号技能
  3. # 注意,Hash表的主键是一个二元数组,第一个量表示装备种类,第二个量表示装备的ID,这样定义具有和系统的一致性。

步骤3:增加装备的同时增加技能。翻开Game_Actor,我们看到了下面的方法:
RUBY 代码复制
  1. def equip(equip_type, id)
  2.     case equip_type
  3.     when 0  # 武器
  4.       if id == 0 or $game_party.weapon_number(id) > 0
  5.         $game_party.gain_weapon(@weapon_id, 1)
  6.         @weapon_id = id
  7.         $game_party.lose_weapon(id, 1)
  8.       end
  9.     when 1  # 盾
  10.       if id == 0 or $game_party.armor_number(id) > 0
  11.         update_auto_state($data_armors[@armor1_id], $data_armors[id])
  12.         $game_party.gain_armor(@armor1_id, 1)
  13.         @armor1_id = id
  14.         $game_party.lose_armor(id, 1)
  15.       end
  16.     when 2  # 头
  17.       if id == 0 or $game_party.armor_number(id) > 0
  18.         update_auto_state($data_armors[@armor2_id], $data_armors[id])
  19.         $game_party.gain_armor(@armor2_id, 1)
  20.         @armor2_id = id
  21.         $game_party.lose_armor(id, 1)
  22.       end
  23.     when 3  # 身体
  24.       if id == 0 or $game_party.armor_number(id) > 0
  25.         update_auto_state($data_armors[@armor3_id], $data_armors[id])
  26.         $game_party.gain_armor(@armor3_id, 1)
  27.         @armor3_id = id
  28.         $game_party.lose_armor(id, 1)
  29.       end
  30.     when 4  # 装饰品
  31.       if id == 0 or $game_party.armor_number(id) > 0
  32.         update_auto_state($data_armors[@armor4_id], $data_armors[id])
  33.         $game_party.gain_armor(@armor4_id, 1)
  34.         @armor4_id = id
  35.         $game_party.lose_armor(id, 1)
  36.       end
  37.     end
  38.   end

       这样,以武器为例,我们在when 0这个结构层次上做如下改动:
RUBY 代码复制
  1. when 0
  2. if id == 0 or $game_party.weapon_number(id) > 0
  3.   $game_party.gain_weapon(@weapon_id, 1)
  4.   # 去掉被卸下装备的附带技能
  5. # 对应技能设定的情况下
  6.   if $equipment_skill_table.include?([0,@weapon_id])
  7. for skill_id in $equipment_skill_table[[0,id]]
  8.       self.forget_skill(skill_id)
  9. end
  10.   end
  11.   @weapon_id = id
  12.   $game_party.lose_weapon(id, 1)
  13.   # 增加新装备上的技能
  14.   # 对应技能设定的情况下
  15.   if $equipment_skill_table.include?([0,id])
  16. for skill_id in $equipment_skill_table[[0,id]]
  17.   self.learn_skill(skill_id)
  18. end
  19.   end
  20. end

       如法炮制剩下的几个when分支就OK了。
       再次提醒一下,$equipment_skill_table一定要先定义再使用,定义位置随意,但是最好定义在最外部的层次上(class保留字的外面)。
       关于Game_Enemy类的,没有什么多说的,但是我们也可以自定义一些内容。
       例如:能力值破限。在数据库中无法设置敌人的力量(str)为999以上,但是在这里我们就可以做到。如果要设置1号敌人的力量为1500,则找到base_str那一个函数,改写成:
RUBY 代码复制
  1. def base_str
  2.   return @enemy_id == 1 ? 1500 :$data_enemies[@enemy_id].str
  3. end

       利用条件表达式就可以轻松完成。不过要定义多个敌人能力值破限,就要定义一个自定义的数据库,然后再进行函数值返回,这里就不说了。
       我们向下看,可以知道,敌人的普通攻击是没有属性的,也没有附加状态的变化,用类似的方法,我们可以修改这部分的内容,这里就不说了。

       在这里我们留下第一个作业题目。请设定一个被动技能,它的效果是,学习该技能的角色会降低所有状态(不能抵抗的除外)的命中可能性(默认是20%),即如果原来角色对状态1的有效度是C(60%命中),则学习该技能后降为D(40%)。

3.1.2  角色队伍Game_Party
       这个也是我们经常修改的类别,下面是Game_Party的脚本开头。游戏里面Game_Party的实例是$game_party
RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2.   # ● 定义实例变量
  3.   #--------------------------------------------------------------------------
  4.   attr_reader   :actors                   # 角色
  5.   attr_reader   :gold                     # 金钱
  6.   attr_reader   :steps                    # 步数
  7.   #--------------------------------------------------------------------------
  8.   # ● 初始化对像
  9.   #--------------------------------------------------------------------------
  10.   def initialize
  11.     # 建立角色序列
  12.     @actors = []
  13.     # 初始化金钱与步数
  14.     [url=home.php?mod=space&uid=236945]@gold[/url] = 0
  15.     @steps = 0
  16.     # 生成物品、武器、防具的所持数 hash
  17.     @items = {}
  18.     @weapons = {}
  19.     @armors = {}
  20.   end

       我们注意到,角色,金钱,步数是整个队伍共有的东西。而我们看到,道具,武器,防具却没有用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,如果要做一个队伍脚本的话,首先就要突破这个限制。
       我们向后看,定义的方法包括角色的添加和删除,各种队伍物品的添加和删除,最后还有确定角色目标的方法,这个都不难理解,需要的时候,甚至都可以翻过来参考,不过我还是建议大家把这些方法熟练记住。
       例如,现在要清除队伍中所有的物品。如果要是不知道脚本的话,那就麻烦大了,用事件的话是会累死人的。但是知道了队伍脚本,就不一样了,于是我们可以写:
RUBY 代码复制
  1. def clear_items
  2.   @items = {}
  3. end

       把这个方法定义在Game_Party的内部,然后在事件中插入脚本$game_party.clear_items即可。
       注意@items没有被外部化,因此用$game_party.items[id] = 0这样的语句是不可以的,重申!
       大家可能看到Game_Party里面的方法简单易懂,因此涉及到物品,金钱等变量时,一定要参考Game_Party。
RUBY 代码复制
  1. def add_actor(actor_id)
  2.     # 获取角色
  3.     actor = $game_actors[actor_id]
  4.     # 同伴人数未满 4 人、本角色不在队伍中的情况下
  5.     if @actors.size < 4 and not @actors.include?(actor)
  6.       # 添加角色
  7.       @actors.push(actor)
  8.       # 还原主角
  9.       $game_player.refresh
  10.     end
  11.   end

       上面的脚本说明,$game_party.actors中最多只能容纳4个成员,如果队伍人数已满,再添加角色是无效的,有很多人认为用事件的“添加队员”指令,就一定会成功,其实不然。如果想让队伍中的人数突破限制的话,就必须首先在这里做文章。当然改成5人战斗脚本光改这里肯定是不够的,我们必须要进行大量的整合,这里就不细说了。

3.1.3  地图角色Game_Character及它的两个子类Game_Player(角色)和Game_Event(事件)[/size]
这些脚本都是处理地图画面上角色以及NPC移动的类,我们先从Game_Character看起。
RUBY 代码复制
  1. attr_reader   :id                       # ID
  2.   attr_reader   :x                        # 地图 X 坐标 (理论坐标)
  3.   attr_reader   :y                        # 地图 Y 坐标 (理论坐标)
  4.   attr_reader   :real_x                   # 地图 X 坐标 (实际坐标 * 128)
  5.   attr_reader   :real_y                   # 地图 Y 坐标 (实际坐标 * 128)
  6.   attr_reader   :tile_id                  # 元件 ID  (0 为无效)
  7.   attr_reader   :character_name           # 角色 文件名
  8.   attr_reader   :character_hue            # 角色 色相
  9.   attr_reader   :opacity                  # 不透明度
  10.   attr_reader   :blend_type               # 合成方式
  11.   attr_reader   :direction                # 朝向
  12.   attr_reader   :pattern                  # 图案
  13.   attr_reader   :move_route_forcing       # 移动路线强制标志
  14.   attr_reader   :through                  # 穿透
  15.   attr_accessor :animation_id             # 动画 ID
  16.   attr_accessor :transparent              # 透明状态

       在这里我们看到了Game_Character的一些属性,这里的x和y相当于我们地图编辑器中的这个:
       是理论的坐标值,实际坐标值和理论坐标值有个换算关系,实际坐标 = 理论坐标 * 128,这个记住就好。我们写脚本的时候,理论坐标用得最多。Character_name和character_hue表示的是角色行走图的文件名和色相,这个和我们刚才讲Game_Actor里面的属性几乎是一样的,不过Game_Character作为角色行走图类,这个属性和Game_Actor的还是有本质区别。
       pattern指的是角色行走图的图案序号,这是个什么东西呢?我们知道角色走动的时候,会有踏步动画,一个角色行走图文件是4*4个图形,不同横行代表不同方向,同一横行的四个图案就是用来做成动画的,这个pattern就是指同一横行的图形号码。
       direction指的是行走图的朝向,在RGSS中,数字2、4、6、8分别代表下,左,右,上。这个怎么记忆呢?我们看看电脑上的数字小键盘,这个对应关系,正好是小键盘数字和方向的对应。
       接下来我们看下这里面的各种方法。RGSS有一套默认的角色移动规则,这类规则我们也不用做太多改变,已经是比较合理的了。因此下面的函数我们看看形式即可。
RUBY 代码复制
  1. def screen_z(height = 0)
  2.     # 在最前显示的标志为 ON 的情况下
  3.     if @always_on_top
  4.       # 无条件设置为 999
  5.       return 999
  6.     end
  7.     # 通过实际坐标和地图的显示位置来求得画面坐标
  8.     z = (@real_y - $game_map.display_y + 3) / 4 + 32
  9.     # 元件的情况下
  10.     if @tile_id > 0
  11.       # 元件的优先不足 * 32
  12.       return z + $game_map.priorities[@tile_id] * 32
  13.     # 角色的场合
  14.     else
  15.       # 如果高度超过 32 就判定为满足 31
  16.       return z + ((height > 32) ? 31 : 0)
  17.     end
  18.   end

       例如,我们看一下这个画面z坐标的方法。因为我们是调查的行走图在画面上的z坐标,而不是元件,因此我们不要随便引入优先级这个概念。首先,我们知道,事件选项里面有一个“总在最前面显示”,这种事件的z坐标是最高的。另外,我们要注意,表示角色的Game_Player是没有这个属性的,因此角色永远也不能成为画面最上层的东西。下面我们来看其他情况的z坐标。这里先计算了一下画面的y坐标,这是为什么呢?因为我们知道表示角色和事件的图形都有一定的高度,而在地图上,一个格子的大小是32*32,超出的部分就要向上下左右扩张,而我们知道,一个NPC图形的大小是32*48,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)方法。这个大家看看就行了,没必要完全搞懂。
RUBY 代码复制
  1. def increase_steps
  2.     super
  3.     # 不是强制移动路线的场合
  4.     unless @move_route_forcing
  5.       # 增加步数
  6.       $game_party.increase_steps
  7.       # 步数是偶数的情况下
  8.       if $game_party.steps % 2 == 0
  9.         # 检查连续伤害
  10.         $game_party.check_map_slip_damage
  11.       end
  12.     end
  13.   end

       在这个方法中,我们终于知道了游戏为什么要记录主角的步数。很多人认为新工程里面主菜单显示“步数”完全是个多余之举,确实我也认为没有显示的必要。步数是用来生成遇敌计数和检查连续伤害用的,这里,地图用的连续伤害是每走2步就减去1%的HP,当然你可以对此进行更改。
RUBY 代码复制
  1. def refresh
  2.     # 同伴人数为 0 的情况下
  3.     if $game_party.actors.size == 0
  4.       # 清除角色的文件名及对像
  5.       @character_name = ""
  6.       @character_hue = 0
  7.       # 分支结束
  8.       return
  9.     end
  10.     # 获取带头的角色
  11.     actor = $game_party.actors[0]
  12.     # 设置角色的文件名及对像
  13.     @character_name = actor.character_name
  14.     @character_hue = actor.character_hue
  15.     # 初始化不透明度和合成方式子
  16.     @opacity = 255
  17.     @blend_type = 0
  18.   end

       在这里我们明白了,地图上角色的图形和队伍中第一名角色的图形是一致的。因此,我们完全可以把它改成别的。在Game_Party中我们可以增加“领队”这一属性,然后把中间的actor = $game_party.actors[0]换成actor = $game_party.leader即可。当然Game_Party的leader还需要定义。另外,我们经常遇到主角变更交通工具的情况,比方说航海的时候,主角图形则要显示为一艘船。这个我们纯用事件是无法做到的(我们可以通过改变队伍中第一个角色的行走图来获得这个效果,但是你打开菜单,会发现角色的脸谱也变成了一艘船,这个显然很不合理),我们仍然可以在这里进行改动。例如,设置一个航海中的标志,如果队伍在航海中,则直接设置@character_name和@character_hue即可。
       在Game_Player的update方法中,我们看到了输入的处理。
RUBY 代码复制
  1. case Input.dir4
  2. when 2
  3.   move_down
  4. when 4
  5.   move_left
  6. when 6
  7.   move_right
  8. when 8
  9.   move_up
  10. end

       这是对4方向输入的处理,用Input.dir4,游戏中还有一个对8方向的输入处理Input.dir8,增加了4个斜向的移动,1、3、7、9分别表示,左下,右下,左上,右上,还是跟小键盘的方位是一样的(注:F1再次出现错误,F1原来写的是数字1到8表示8方向输入,这是不对的,应该是1、2、3、4、6、7、8、9才对)。
RUBY 代码复制
  1. case Input.dir8
  2. when 1
  3.   move_lower_left
  4. when 2
  5.   move_down
  6. when 3
  7.   move_lower_right
  8. when 4
  9.   move_left
  10. when 9
  11.   move_upper_right
  12. when 6
  13.   move_right
  14. when 7
  15.   move_upper_left
  16. when 8
  17.   move_up
  18. end

       这个是修改后8方向是输入,大家可以试一下。
       关于Game_Xxxx类脚本我们就要说这些,大家有兴趣的话可以多看看脚本,会有更多意想不到的发现。

3.2  自制游戏对象
       看了这么多游戏对象的脚本,是不是有自制游戏对象的念头了呢?那我们在这一小节就说说如何自制属于你自己的游戏对象脚本。
3.2.1  起步
       首先要明白,你自己想自定义的游戏数据到底属不属于游戏对象,如果不属于(比方说你要建立一个数据库的类,这就不属于游戏对象),那么就不应该用游戏对象的思路。总的来说,随着游戏进行,随时都有改变可能,并且跟玩家进行直接互动的对象,大多都是游戏对象。下面我们来看一个例子:
例如,制作一个真实商店,要求是商店尽可能还原真实生活中的商店,有库存限制,你卖给这个商店的物品,商店也会原封不动显示出来。这就是一个游戏对象。
3.2.2  分析属性和方法
       下面我们要分析一下,我们建立的游戏对象应该有哪些内容和方法。
       还是以真实商店为例。
       我们需要的是商店的最大库存(所有商品的最大数量,为了方便起见,对于同一个商店,所有商品的最大库存都是一样的),以及商店里面各种商品的数量。除此以外,暂时还未想到其他的属性。
       真实商店的方法很简单,就是出货和进货,不过考虑到物品分为道具,武器,防具3种,我们可能要定义很多方法。
       暂时想到这么多,然后我们就可以开工了。
3.2.3  写出游戏对象脚本
       我们现在,就可以根据我们的分析,来写出真实商店脚本的游戏对象部分了。先是定义各种属性,考虑到道具,武器,防具的不同性质,我们把它们定义在三个表中。
RUBY 代码复制
  1. class Game_VisualShop
  2.   attr_reader :max_number
  3.   attr_reader :shop_goods_item
  4.   attr_reader :shop_goods_weapon
  5.   attr_reader :shop_goods_armor
  6. end

       接下来定义初始化:
RUBY 代码复制
  1. def initialize(max_number,items,weapons,armors)
  2.   @max_number = max_number
  3.   @shop_goods_item = {}
  4.   @shop_goods_weapon = {}
  5.   @shop_goods_armor = {}
  6.   for j in items
  7.     @shop_goods_item[j] = @max_number
  8.   end
  9.   for j in weapons
  10.     @shop_goods_weapon[j] = @max_number
  11.   end
  12.   for j in armors
  13.     @shop_goods_armor[j] = @max_number
  14.   end
  15. end

       这个初始化就完成了,我们注意,传进去的参数有4个,分别是库存最大数量,各种物品的数组,数组里面是物品的ID,我们这里用ID而不是用物品的实例,是为了模仿游戏主角背包的存储模式,这个一定要注意。然后就是把商店各种物品数量初始化为库存最大值。这个也是看个人需要。我们用的是Hash表来表示现有的库存,这个也是借鉴了Game_Party中对于队伍中各种物品武器防具的刻画。写到这里,我们可以考虑给真实商店增加一个变量,表示商店中可以显示库存为0的物品(其余的库存为0的物品就直接不显示)。这是因为你卖给这个商店的物品和这个商店原有的物品本质是不一样的。这是一个二维数组,含有3个一维数组,分别表示总显示的道具、武器、防具。因此在initialize的最后,加入
RUBY 代码复制
  1. @always_shown = []
  2. @always_shown[0] = @shop_goods_item.keys
  3. @always_shown[1] = @shop_goods_weapon.keys
  4. @always_shown[2] = @shop_goods_armor.keys

这个显然我们不用把它公开化,放在这里就可以了。
       然后是各种方法,也是非常简单的。
RUBY 代码复制
  1. def import(type,id,number)
  2.   case type
  3.   when 0
  4.     if @shop_goods_item[id] == nil
  5.       @shop_goods_item[id] = 0
  6.     end
  7.     @shop_goods_item[id] += number
  8.     if @shop_goods_item[id] > @max_number
  9.       @shop_goods_item[id] = @max_number
  10.     end
  11.     if @shop_goods_item[id] <= 0
  12.       @shop_goods_item[id] = 0
  13.       unless @always_shown[0].include?(id)
  14.         @shop_goods_item.delete(id)
  15.       end
  16.     end
  17.   when 1
  18.     …..
  19.   end
  20. end

       这是一个通用的商店进货的方法,type表示种类,0为道具,1为武器,2为防具。id是对应类型的id,number是数量。添加的时候,考虑到某物品的Hash个数可能不存在,因此要进行一步判断。然后再进行增加操作,并修改范围。如果最终个数是0并且不是永远显示的话,直接从相应Hash表中删除。这里只给出了道具部分的定义,武器和防具部分定义是类似的。
       有了这个东西,出货的方法就很容易写了。
RUBY 代码复制
  1. def export(type,id,number)
  2.   import(type,id,-number)
  3. end

       直接调用import方法,并将number取成负值即可。
       到这里我们所有的基本方法就定义完了,Game_VisualShop也告一段落,不过,现在它只是一个游戏对象,如何将它和玩家互动起来,我们还需要学习后面的内容。

       第3章的讲解就到这里,在下一章我们会将到窗口的使用,大家敬请期待吧。
回复 支持 反对

使用道具 举报

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

6
 楼主| 发表于 2013-10-20 08:25:03 | 显示全部楼层
火烧兔子 发表于 2013-10-19 22:16
我不知道为什么不论是RGSS还是C语言,都是从计算加减乘除开始的,这样很容易让我们丧失继续学习下去的耐心 ...

写游戏切忌急功近利,学脚本也是。只有先弄清楚基础知识,才能够深刻理解游戏的抽象数据类型。但是考虑到实际的运用价值,这些计算确实很难让人有学习下去的欲望。大概是大家对计算都熟悉得很吧,因此我这个帖子没有涉及很多计算和语法方面,而是侧重于理解游戏的数据结构。不过,如果要踏踏实实成为“脚本党”的话,计算方面的东西是马虎不得的。
回复 支持 反对

使用道具 举报

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

7
 楼主| 发表于 2013-10-20 21:06:05 | 显示全部楼层
lucifer4223 发表于 2013-10-20 20:09
自称做游戏的,一般两类人,一是改素材的,利用现有引擎,几乎不懂脚本。
二是偏向做程序的,甚至直接用 C+ ...

感谢你的支持!一直以来,我认为掌握了脚本就是掌握了写游戏的主动权,否则始终也不明白游戏运行的机理。当然,我觉得很多人写脚本纯粹是兴趣所在,这也是我们能继续把游戏创作下去的动力。这个帖子本身难度不高,而且只要你看过F1的基础篇,就可以从这里收获点什么。

这个帖子,最适合一些渴望写脚本,有一些基本知识,但是写出的脚本总也通不过的朋友们。我想,职业写脚本的人毕竟是少数,大多数人也是一点点摸索,一点点理解,经过不断尝试——犯错的反复中,摸清RMXP的脾气。我其实就是这样过来的……

分解RTAB系统,这个真是很好的计划。早就听说RTAB是个神器,里面的任何一部分都有我们借鉴的机会,因此要加油啊。

我写脚本时间不算太短,但是游戏却一个也没做出来,这也算是遗憾吧,毕竟,业余脚本党专注于脚本,对真正完成一个游戏的事情还是欠缺很多动力的。
回复 支持 反对

使用道具 举报

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

8
 楼主| 发表于 2013-10-22 21:40:58 | 显示全部楼层
本帖最后由 RyanBern 于 2015-8-2 13:24 编辑

[box=RoyalBlue]
第4章节:窗口的使用
[/box]

       接下来我们终于要学习窗口和精灵了,这我想也是大家特别想知道的。因为在这之前,我们都是同抽象的数据打交道,难免有些枯燥,那么在这一章,我们就要把我们自己脑中所想,全部展现在画面中。下面我们就开始吧。
4.1  窗口的使用
4.1.1各种概念
       在最开始,我们要明白一个窗口到底是什么,它到底有哪些属性。因此,我们必须要先介绍下面几个类。
①rect:矩形的类
       顾名思义,这个类表示矩形,但这种东西是不能直接显示出来的。矩形是一种基本的类,它的作用在很多高级的类中体现尤其明显。矩形有四个属性,x坐标,y坐标,宽度,高度。其中x坐标和y坐标是相对而言的。
②viewport:视口的类
       这个类是一个比较难理解的类,我在最开始也没明白这是什么东西。在屏幕上生成可见的对象都必须要指定视口viewport。简单说来,你面前有一堵墙,墙上有一个窗户可以看到墙对面的东西,那么这个窗户就可以类比成视口,你只能看到视口内有的东西,视口之外,即使有内容存在,你是看不到的。
       我们举个简单的例子,大家打开脚本编辑器,在Window_BattleStatus中的initialize方法中,输入self.back_opacity = 0

       然后随便找一个640*480的战斗图片(注意大小是640*480),设置为一个地图的战斗图,然后进入战斗场景,画面会变成这样:

       下面的640*160的矩形区域全部都是黑色的,这就说明图片显示不全,只有上半部分显示了出来。我们说过图片是640*480的,因此出现这种情况是我们视口的设置问题。打开Spriteset_Battle,我们在前面发现了这个:

       我们看到生成战斗背景的视口是640*320,因此我们的640*480图片才显示不出来。
       因此,无论图片是什么,生成的可视对象都必须在视口之内。
       视口的提出为我们管理界面上的可显示元素提供方便,因为视口的移动,色相改变,闪烁只会影响视口内部的元素,而视口外部的元素不受影响。
③bitmap:位图的类
       我们可以认为这一个类专门表示图片,但是要注意,Bitmap对象只是一个数据而已,并不能直接作为显示在屏幕中的对象。所以,bitmap是没有x坐标和y坐标这一属性的,这个要千万注意。但是,作为游戏的一种数据,在游戏运行的时候,必须要进入到游戏的内存中,RGSS里面有个高速缓存,专门用来保存这些临时的图片,用的时候就要在这里读取。因为高速缓存空间有限,因此我们要学会将不用的bitmap释放掉。
       我们来看看bitmap里面都有什么方法。
       dispose:释放位图,即将不用的位图从高速缓存中释放掉,我们在写程序时,必须有及时释放的好习惯,当然如果一个位图被频繁的使用,就不用释放了。
       clear:清除位图全体,即将位图的内容变成一个空的“画布”
       blt(x,y,src_bitmap,src_rect,opacity):这个方法非常重要,大家一定要熟练掌握。这个方法的作用有点类似于两个位图之间的拷贝,意思就是将位图A指定矩形的内部内容拷贝到本位图中,放到指定坐标的位置。下图更加直观说明了这个事情。

       这样大家应该理解了吧。
       fill_rect(x,y,width,height,color)
       fill_rect(rect,color):上面两个都是在位图中绘制实心矩形,可以用矩形的各种信息,也可以直接把矩形放在参数中,要指定填充的颜色,利用这两个函数,我们可以在bitmap中绘制矩形,然后把它显示出来(要用到精灵)。朴素的HP条SP条就是这样描绘的。
       draw_text(x,y,width,height,str,align):在位图的指定区域描绘文字,align是对齐方式。这有点类似于Windows画图中的创建文字。这个大家也要熟练使用。F1中也说,这个处理要花费时间,尽量不要重复描绘字符串(意思就是只有在文字改变的时候才可描绘,比较省CPU,防止游戏卡顿)。
       这些方法大家一定要熟练掌握,下面我们正式开始窗口的学习。

4.1.2  窗口的基本知识 一般窗口的实现
       脚本编辑器中,所有窗口的父类Window没有给出,我们也无法得知它的代码,但是我们打开F1,搜索Window,就会了解Window类的方法和属性,这对我们来说是个很好的参考。
       窗口实际上是一种可以在屏幕上显示的图片,因此生成必须要指定视口。但是由于系统默认的缘故,不用额外加以设置。窗口以大量精灵构成,因此要有dispose方法。我们使用一个窗口完毕后,一定要把它释放掉。这个我们在介绍场景的时候会有说明。
       F1中将各种方法已经说得很详细,这个我就不用多说。不过还是强调几个地方吧。
       visible是可见与否的属性,如果你已经生成了一个窗口,但是不想显示它,就要用到visible属性,注意,隐藏不意味着释放。
       active是活动状态属性,主要出现在有光标的窗口中,处于活动状态的窗口可以接受命令,进行光标闪烁等,游戏不能同时处理2个以上活动的窗口(你没见过按一下方向键两个窗口跟着一起变的吧?),因此同一个场景中,活动的窗口至多1个。
       ox,oy是窗口内容原点的坐标,在这里我们可以把窗口看作一个小小的视口,里面的内容只能通过窗口显示出来。
       注意:游戏默认窗口内容显示的视口是窗口上下左右边界向内16个像素,因此窗口实际内容和窗口边界总有16个像素的间隔,这个就尽量不要更改了。
       作为所有Window的父类,Window里面可用的方法很少,我们来看看真正能作为显示窗口类的Window_Base:
先看initialize部分:
RUBY 代码复制
  1. def initialize(x, y, width, height)
  2.     super()
  3.     @windowskin_name = $game_system.windowskin_name
  4.     self.windowskin = RPG::Cache.windowskin(@windowskin_name)
  5.     self.x = x
  6.     self.y = y
  7.     self.width = width
  8.     self.height = height
  9.     self.z = 100
  10.   end

       这里,我们生成一个窗口对象要指定它的位置和大小,并且设定窗口外观(这里默认是数据库的窗口外观),最后是z坐标,窗口的高度。
       紧接着后面是个释放的方法,重定义:
RUBY 代码复制
  1. def dispose
  2.     # 如果窗口的内容已经被设置就被释放
  3.     if self.contents != nil
  4.       self.contents.dispose
  5.     end
  6.     super
  7.   end

       在这里我们看到,对窗口释放同时也释放了作为窗口内容显示的位图,这样的好处是让我们的操作变得简单。例如写@a.dispose这样的语句,不但释放了窗口本身,也释放了窗口内容位图,一举两得。
       后面是定义获取各种颜色的方法,这我就不多说了。
       刷新update:
RUBY 代码复制
  1. def update
  2.     super
  3.     # 如果窗口的外关被变更了、再设置
  4.     if $game_system.windowskin_name != @windowskin_name
  5.       @windowskin_name = $game_system.windowskin_name
  6.       self.windowskin = RPG::Cache.windowskin(@windowskin_name)
  7.     end
  8.   end

       这个update方法是刷新窗口,在RGSS中,有刷新作用的方法,原则上1帧调用一次,这个一定要注意。我们看到Window_Base的刷新很简单,就是要判定窗口外观的改变,并没有涉及内容的处理。因此我们在描绘内容时,尽量不要刷新它,我们刚才说到了draw_text需要花费时间,因此要避免每1帧都重新描绘文字。
       下面则是一些基本的描绘:
RUBY 代码复制
  1. def draw_actor_graphic(actor, x, y)
  2.     bitmap = RPG::Cache.character(actor.character_name, actor.character_hue)
  3.     cw = bitmap.width / 4
  4.     ch = bitmap.height / 4
  5.     src_rect = Rect.new(0, 0, cw, ch)
  6.     self.contents.blt(x - cw / 2, y - ch, bitmap, src_rect)
  7.   end

       角色图形的描绘,有3个参数,角色,描绘x坐标,描绘y坐标。
       第二行是取得角色的位图,把它存储在高速缓存中。用的模块方法RPG::Cache,这个大家自行F1就好。然后是设定显示的内容,我们不能把整个图片显示出来,只需要显示左上角那个正脸的图就可以了,因此后面3行全部在设置截取图片的位置,随后,用blt方法,把截取的位置原封不动传送到self.contents中,window的contents就是表示窗口内容的位图,这恰恰就是blt完成的工作:把一个位图里面指定位置内容传给另一个位图中。在窗口中描绘图片就是这么简单。
       下面我们再来看一个描绘文字的:
RUBY 代码复制
  1. def draw_actor_hp(actor, x, y, width = 144)
  2.     # 描绘字符串 "HP"
  3.     self.contents.font.color = system_color
  4.     self.contents.draw_text(x, y, 32, 32, $data_system.words.hp)
  5.     # 计算描绘 MaxHP 所需的空间
  6.     if width - 32 >= 108
  7.       hp_x = x + width - 108
  8.       flag = true
  9.     elsif width - 32 >= 48
  10.       hp_x = x + width - 48
  11.       flag = false
  12.     end
  13.     # 描绘 HP
  14.     self.contents.font.color = actor.hp == 0 ? knockout_color :
  15.       actor.hp <= actor.maxhp / 4 ? crisis_color : normal_color
  16.     self.contents.draw_text(hp_x, y, 48, 32, actor.hp.to_s, 2)
  17.     # 描绘 MaxHP
  18.     if flag
  19.       self.contents.font.color = normal_color
  20.       self.contents.draw_text(hp_x + 48, y, 12, 32, "/", 1)
  21.       self.contents.draw_text(hp_x + 60, y, 48, 32, actor.maxhp.to_s)
  22.     end
  23.   end

       描绘角色HP。这里面参数多了一个宽度width,大家注意观察就可以发现,在菜单中描绘HP,是有最大HP的,在战斗中则没有,就是由于二者宽度不同导致的。后面的draw_text大家看看,基本上都会用。在这里提示一下,draw_text能描绘字符串,而不能描绘数字本身,因此draw_text(0, 0, 32, 32, 250)这样来描绘数字250是行不通的,Object类有个方法叫做to_s,把本身转换为字符串,因此要写250.to_s,这样等同于"250"。
       另外,很多人被那些坐标还有宽度设置困扰,总也描绘不到合适的位置。这个其实也比较好解决,整个画面是640*480的,简单进行一些运算就会知道各种位置。还有,在窗口中进行的x,y都是指相对窗口内容位图的原点位置,并不是屏幕的位置,这个大家要注意。游戏默认字的高度是24个像素,一行的高度是32个像素,因此,如果5个默认大小的字连起来时,长度就是120(一个汉字的宽度=2个英文字母的宽度),因此根据这个就可以设定描绘文字矩形的宽度了。最后要注意,draw_text是不能换行的,如果文字太长,它会一直描绘下去,可能会出边界。

       有了这些知识,我们就可以来刻画一个最基本的窗口了。下面来小小测试一下。
RUBY 代码复制
  1. class Window_Test < Window_Base
  2.   def initialize
  3.     super(0,0,196,64)
  4.     self.contents = Bitmap.new(width – 32, height - 32)
  5.     refresh
  6.   end
  7.   def refresh
  8.     self.contents.clear
  9.     self.contents.draw_text(0,0,144,32,”RMXP is good”)
  10.   end
  11. end

       这样一个简单的窗口就定义好了。
       但是这样是无法在屏幕上显示的,要让它显示出来,通常要借助场景Scene的帮助。
       我们先学一点后面的东西,让我们的窗口显示出来。
       在Scene_Map的main函数里面添加这样的语句:

       然后再运行一下。效果如图。

       在这里我们改动了Scene_Map脚本,但是也很简单,先创建一个窗口,在场景结束后再释放它,就是这样。
       但是现在我们生成的窗口内容不能自动变化,如果我们要生成一个内容可能变化的窗口,这样就不行了。比方说,我们要在地图上显示角色的金钱,但是如果通过事件增加了队伍的金钱,窗口的内容是没有变化的。这就需要我们对update进行下改装,如果内容变更了,就重新描绘内容。我们知道draw_text是不适合反复调用的,因此我们只有在需要的时候,才能重新描绘窗口内容。首先要在refresh方法加上
RUBY 代码复制
  1. @gold = $game_party.gold

       然后我们修改update方法:
RUBY 代码复制
  1. def update
  2.   super
  3.   if $game_party.gold != @gold
  4.     refresh
  5.   end
  6. end

       这里,我们可以设置一个不公开的变量@gold,表示当前窗口描绘的金钱数量。然后对update进行重定义,让它能自动检查内容的变化。当然,我们也要对场景脚本进行改变,场景脚本里面有个update方法,在里面,我们加入对窗口的刷新@a.update,在这里我就不给出所有的代码了,作为练习,希望读者能写出一个在地图上显示金钱的窗口。
当然,这样简单的窗口,我们还是有方法能判断内容是否变更的,但是对于复杂的窗口,我们就无法判断了,因此,我们还需要掌握其他方法。

4.1.3  滚动窗口的实现
       下面我们学习如何使用滚动窗口。滚动窗口要用Window_Selectable。
       打开这个脚本,我们发现它是Window_Base的子类。
RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2.   # ● 定义实例变量
  3.   #--------------------------------------------------------------------------
  4.   attr_reader   :index                    # 光标位置
  5.   attr_reader   :help_window              # 帮助窗口
  6.   #--------------------------------------------------------------------------
  7.   # ● 初始化对像
  8.   #     x      : 窗口的 X 坐标
  9.   #     y      : 窗口的 Y 坐标
  10.   #     width  : 窗口的宽
  11.   #     height : 窗口的高
  12.   #--------------------------------------------------------------------------
  13.   def initialize(x, y, width, height)
  14.     super(x, y, width, height)
  15.     @item_max = 1
  16.     @column_max = 1
  17.     @index = -1
  18.   end

       这里多了两个属性,光标位置(索引)和帮助窗口。考虑到很多滚动窗口(比方说物品和技能窗口)都要关联帮助窗口,因此需要设置这个属性。
       初始化也没什么特别的,多了2个内部的实变量@item_max和@colunm_max,分别是描绘项目的最大值和描绘列数的最大值。
       继续向下看,里面有计算窗口项目位置的各种方法。包括开头行,一页的最大项目数,一页的最大行数,都非常简单,这里就不说了。
       我们在这里看到,RMXP里面默认一行的高度是32,也就是说,上面的所有设置位置的方法,都是按照这个标准进行的。当然,这个功能并不完善,我们可以对Window_Selectable进行修改,让它能显示任意行高的项目。
       然后就是对输入的处理和光标矩形的更新,这里简单看看就可以了,不能完全理解也没有关系,只要记住这是干什么用的就好了。另外,Window_Selectable里面并没有给出光标明灭变化的方法,那个是在Window中定义的,只是我们看不到而已。
       下面我们以Window_Item为例,来看看滚动窗口该怎样制作。
RUBY 代码复制
  1. class Window_Item < Window_Selectable
  2.   #--------------------------------------------------------------------------
  3.   # ● 初始化对像
  4.   #--------------------------------------------------------------------------
  5.   def initialize
  6.     super(0, 64, 640, 416)
  7.     @column_max = 2
  8.     refresh
  9.     self.index = 0
  10.     # 战斗中的情况下将窗口移至中央并将其半透明化
  11.     if $game_temp.in_battle
  12.       self.y = 64
  13.       self.height = 256
  14.       self.back_opacity = 160
  15.     end
  16.   end
  17.   #--------------------------------------------------------------------------
  18.   # ● 获取物品
  19.   #--------------------------------------------------------------------------
  20.   def item
  21.     return @data[self.index]
  22.   end
  23.   #--------------------------------------------------------------------------
  24.   # ● 刷新
  25.   #--------------------------------------------------------------------------
  26.   def refresh
  27.     if self.contents != nil
  28.       self.contents.dispose
  29.       self.contents = nil
  30.     end
  31.     @data = []
  32.     # 添加项目
  33.     for i in 1...$data_items.size
  34.       if $game_party.item_number(i) > 0
  35.         @data.push($data_items[i])
  36.       end
  37.     end
  38.     # 在战斗中以外添加武器、防具
  39.     unless $game_temp.in_battle
  40.       for i in 1...$data_weapons.size
  41.         if $game_party.weapon_number(i) > 0
  42.           @data.push($data_weapons[i])
  43.         end
  44.       end
  45.       for i in 1...$data_armors.size
  46.         if $game_party.armor_number(i) > 0
  47.           @data.push($data_armors[i])
  48.         end
  49.       end
  50.     end
  51.     # 如果项目数不是 0 就生成位图、重新描绘全部项目
  52.     @item_max = @data.size
  53.     if @item_max > 0
  54.       self.contents = Bitmap.new(width - 32, row_max * 32)
  55.       for i in 0...@item_max
  56.         draw_item(i)
  57.       end
  58.     end
  59.   end
  60.   #--------------------------------------------------------------------------
  61.   # ● 描绘项目
  62.   #     index : 项目编号
  63.   #--------------------------------------------------------------------------
  64.   def draw_item(index)
  65.     item = @data[index]
  66.     case item
  67.     when RPG::Item
  68.       number = $game_party.item_number(item.id)
  69.     when RPG::Weapon
  70.       number = $game_party.weapon_number(item.id)
  71.     when RPG::Armor
  72.       number = $game_party.armor_number(item.id)
  73.     end
  74.     if item.is_a?(RPG::Item) and
  75.        $game_party.item_can_use?(item.id)
  76.       self.contents.font.color = normal_color
  77.     else
  78.       self.contents.font.color = disabled_color
  79.     end
  80.     x = 4 + index % 2 * (288 + 32)
  81.     y = index / 2 * 32
  82.     rect = Rect.new(x, y, self.width / @column_max - 32, 32)
  83.     self.contents.fill_rect(rect, Color.new(0, 0, 0, 0))
  84.     bitmap = RPG::Cache.icon(item.icon_name)
  85.     opacity = self.contents.font.color == normal_color ? 255 : 128
  86.     self.contents.blt(x, y + 4, bitmap, Rect.new(0, 0, 24, 24), opacity)
  87.     self.contents.draw_text(x + 28, y, 212, 32, item.name, 0)
  88.     self.contents.draw_text(x + 240, y, 16, 32, ":", 1)
  89.     self.contents.draw_text(x + 256, y, 24, 32, number.to_s, 2)
  90.   end
  91.   #--------------------------------------------------------------------------
  92.   # ● 刷新帮助文本
  93.   #--------------------------------------------------------------------------
  94.   def update_help
  95.     @help_window.set_text(self.item == nil ? "" : self.item.description)
  96.   end
  97. end

       这里的初始化大家应该都没问题了,然后在这里定义了一个方法item,返回当前光标所指的物品。这个方法是必要的,原因我们要到场景的地方再解释。最后就是refresh方法,用来描绘窗口内的所有内容。
       refresh里面大概分为以下几个层次。
       首先释放窗口内容,这样做的目的是节省内存。考虑到窗口内容可能比较多,用self.contents.clear反倒不如释放再重新生成来得直接。而且描绘的最大项目数也会随着队伍中物品的变化而变化,如果设置固定的contents大小,不便于后面的处理。
       接下来,就是制作窗口内容显示数据,把要显示的物品,武器,防具添加在一个@data数组里面,这是窗口的内部信息,没必要公开化。
       接下来就是描绘窗口内容,在窗口存在项目的时候,一个个描绘。
       描绘单个物品的方法draw_item(i)在后面定义,i指的是物品在@data中的索引,里面的结构清晰直观,大家看看就可以了。里面那个case语句希望大家熟练使用,case语句可以对实例进行类的判定,有了这个机制,写代码会比较方便。
       最后,定义刷新帮助文本的方法(Window_Selectable里面已经有说明)。
       怎么样?现在你可以写出有滚动光标的窗口了。

在这里我们留下一个小练习
       就是我们刚才提到的,Window_Selectable默认每一行高度是32,能否对其进行优化,让用户创建自定义行高的窗口,并且和原来的脚本不发生冲突?

4.2  一个滚动窗口的例子——真实商店
       不知道大家对之前介绍游戏对象的创建还有没有印象,在这一章节里面,我们要把商店的货物在窗口中描绘出来。如果忘了前面的内容的话,还请翻看下前面的帖子哦。
4.2.1  准备工作
       我们想想这个窗口要描绘出什么。肯定要描绘各种物品,它的剩余量和它的价格。我们利用Window_Selectable的滚动功能,来实现滚动处理。
4.2.2  Ruby代码
       有了上面的准备我们就可以写出代码了。
RUBY 代码复制
  1. class Window_Visual_ShopBuy < Window_Selectable
  2.   def initialize(shop_id)
  3.     super(0,128,368,480-128)
  4.     self.index = 0
  5.     @column_max = 1
  6.     @shop_current = $game_visual_shops[shop_id]
  7.     refresh
  8.   end
  9.   # 取得当前光标的物品
  10.   def item
  11.     return @data[self.index]
  12.   end
  13.   def refresh
  14.     # 内容被设置就释放,重新设置
  15.     if self.contents != nil
  16.       self.contents.dispose
  17.       self.contents = nil
  18.     end
  19.     # 设置各种项目数据
  20.     @data_items = []
  21.     @data_weapons = []
  22.     @data_armors = []
  23.     # 添加物品
  24.     for item_id in @shop_current.shop_goods_item.keys
  25.       @data_items.push($data_items[item_id])
  26.     end
  27.     # 排序按照物品ID
  28.     @data_items.sort!{|a,b| a.id – b.id}
  29.     # 添加武器
  30.     for weapon_id in @shop_current.shop_goods_weapon.keys
  31.       @data_weapon.push($data_weapons[item_id])
  32.     end
  33.     # 排序按照武器ID
  34.     @data_weapons.sort!{|a,b| a.id – b.id}
  35.     # 添加防具
  36.     for armor_id in @shop_current.shop_goods_armor.keys
  37.       @data_armors.push($data_armors[item_id])
  38.     end
  39.     # 排序按照物品ID
  40.     @data_armors.sort!{|a,b| a.id – b.id}
  41.     # 合并数组
  42.     @data = @data_items + @data_weapons + @data_armors
  43.     @item_max = @data.size
  44.     if @item_max > 0
  45.       self.contents = Bitmap.new(width - 32, row_max * 32)
  46.       for index in 0…@item_max
  47.         draw_item(index)
  48.       end
  49.     end
  50.   end
  51. end

       由于窗口显示的是三种不同的物品,因此设置数据要花一些时间,但是基本思路跟描绘道具一样,因为商店里面的物品在变化,因此不要一开始就创建bitmap。
       下面就是我们描绘物品的方法draw_item(index)了,注意,这个方法仍然定义在这个窗口内部,只不过是在这个帖子里面,写到外面了而已。
RUBY 代码复制
  1. def draw_item(index)
  2.   item = @data[index]
  3.   # 取得持有数量和库存
  4.   case item
  5.   when RPG::Item
  6.     number = $game_party.item_number(item.id)
  7.     number_left = @current_shop.shop_goods_item[item.id]
  8.   when RPG::Weapon
  9.     number = $game_party.weapon_number(item.id)
  10.     number_left = @current_shop.shop_goods_weapon[item.id]
  11.   when RPG::Armor
  12.     number = $game_party.armor_number(item.id)
  13.     number_left = @current_shop.shop_goods_armor[item.id]
  14.   end
  15.   # 设置颜色
  16.   if item.price <= $game_party.gold and number < 99 and number_left > 0
  17.     self.contents.font.color = normal_color
  18.   else
  19.     self.contents.font.color = disabled_color
  20.   end
  21.   # 开始描绘
  22.   x = 4
  23.   y = index * 32
  24.   rect = Rect.new(x, y, self.width - 32, 32)
  25.   self.contents.fill_rect(rect, Color.new(0, 0, 0, 0))
  26.   bitmap = RPG::Cache.icon(item.icon_name)
  27.   opacity = self.contents.font.color == normal_color ? 255 : 128
  28.   self.contents.blt(x, y + 4, bitmap, Rect.new(0, 0, 24, 24), opacity)
  29.   self.contents.draw_text(x + 28, y, 168, 32, item.name, 0)
  30.   self.contents.draw_text(x + 196, y, 80, 32, item.price.to_s, 2)
  31.   self.contents.draw_text(x + 276, y, 48, 32, “剩:”)
  32.   self.contents.draw_text(x + 308, y, 32, 32, number_left.to_s, 2)
  33. end

       最后别忘了定义update_help,这样就大功告成了。不过,我们暂时无法检验它的对错,有兴趣的朋友可以利用场景来检验一下。

       上图就是我们期待的效果,不过我们做的仅仅是左下角这个窗口哦,而且各种命令的处理还没有设定。

       窗口的学习我们告一段落,接下来我们将要学习场景的使用。大家就敬请期待吧。
回复 支持 反对

使用道具 举报

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

9
 楼主| 发表于 2013-10-29 17:51:39 | 显示全部楼层
本帖最后由 RyanBern 于 2015-8-24 11:57 编辑

[box=RoyalBlue]
第5章节:场景的使用(一)
[/box]

       场景的内容我们拆开来讲,第5章节主要说场景实现的一般过程,解读一些简单场景。第6章节我们主要是DIY一个我们自己想要的场景,并解读一个比较复杂的场景。
5.1  精灵的使用
       这个本来应该在上一章节就应该讲到的,但是写上一章的时候一时脑抽,忘记了,因此在这里补上吧。
5.1.1  精灵是什么
       所谓精灵(Sprite),就是一种能在屏幕上显示各种可见对象的类。例如我们上一章节提到的窗口,还有游戏地图显示的实现,都是靠精灵完成的。另外,拥有动画播放效果的精灵是RPG模块下的Sprite类,也是精灵的一种。在这里,我们只需要学习一下精灵的最基本使用方法就可以了,相信大家一定会举一反三的。
5.1.2  精灵的属性和方法
       既然是一种特殊的类,那我们有必要了解精灵的使用方法。翻开F1,输入 Sprite 搜素,我们便看到了精灵(Sprite)的原型。
       首先是类方法,生成一个新的精灵Sprite.new([viewport]),这里需要指定生成的视口Viewport,在第四章我们已经比较详细讲过视口的含义,和视口不同造成的显示区别。如果忘记了还请翻看19L的帖子哦。当然,这里viewport可以省略,这就默认精灵将直接显示在640*480的游戏窗口中。
       接下来是各种属性和方法:
  • dispose:方法,释放精灵,这个跟释放窗口的作用一样,如果一个精灵你不再使用它,那么需要把它从内存中释放。
  • bitmap:属性,作为精灵所显示的位图。注意,这个位图只是作为精灵显示的数据,实际显示在屏幕上的可能和这个位图略有不同。当然,在释放精灵之前,你有可能还要释放这个位图。释放的顺序则是先释放这个位图,再释放精灵本身。如果这个位图是利用Bitmap.new生成,则释放精灵之前,必须释放此位图;如果这个位图是利用RPG::Cache模块读取高速缓存,则可不必释放此位图。释放位图+精灵的代码一般写成这样:
    RUBY 代码复制
    1. sprite.bitmap.dispose
    2. sprite.dispose
  • src_rect:属性,传送位图的矩形,这个矩形的作用相当于“截取”原来位图的一部分,然后用精灵去显示。
  • visible:属性,精灵是否可见,如果想隐藏一个精灵而不是去释放它,请将visible置为false
  • ox,oy:属性,精灵原点坐标,我们可以把精灵占据的屏幕空间看作是一个矩形,默认这个原点在矩形的左上角。进行坐标变换和旋转的时候,需要参考这个原点。
  • zoom_x,zoom_y:属性,横向和纵向拉伸比例,浮点数。
  • angle:属性,表示逆时针旋转的角度,单位是度。

我们可以用下面几个图片来表示精灵的各种属性:

5.1.3  精灵的具体使用
       了解了精灵的各种属性,现在我们可以使用它了。
       我们要在屏幕上显示各种对象,需要借助Graphics模块。我们不必掌握Graphics的具体内容,在每个场景脚本中,已经在main方法中含有对Graphics模块的方法调用,在这里我们只需照葫芦画瓢就可以了。
       核心的方法,是通过Graphics.update完成的,我们翻开Scene类的脚本,也可以看到这一句。
       在Graphics.update之前,我们必须先生成一个精灵,然后Graphics会自动把它算在画面窗口里面显示的东西,然后通过调用update就可以显示出来了(同理,窗口对象也是,因为窗口本身就由大量精灵组成)。
       简单显示一个图片的代码如下:
RUBY 代码复制
  1. a = Sprite.new
  2. a.bitmap = RPG::Cache.picture("123.png") # 这里输入Graphics/Pictures下文件名
  3. loop do
  4.   Graphics.update
  5. end
  6. a.dispose # 跳出循环后释放,注意,这里利用RPG::Cache载入位图,可不必释放bitmap

       总结下过程就是设置——显示——释放,是不是很简单?

5.2  场景的简单使用
       下面我们就要来学习场景的使用了,这也是最重要的技术之一,希望大家能好好掌握。所谓场景,就是把一些游戏对象(如窗口,精灵)组合起来的一个综合画面,每一个场景都可以看作是一个小系统。可以反馈信息,接收信息,跟玩家互动。下面我们就来具体解读一下。
5.2.1  场景实现的一般步骤
       我们翻开任意一个场景脚本,都会发现开头有相似之处。先定义了main方法,有的是要定义initialize方法(有些则没有)。这个main方法是必要的,因为在前面我已经说过,Main组脚本就是不断调用场景的main方法来进行游戏的。而initialize是初始化场景类的某些具体的信息,而不是进行主处理,这点要记住。
       一般的场景main方法主要分3部分。
       设置(包括初始化)——(画面)刷新,更新——退出后的处理。
       以Scene_Title为例,下面是它的main方法。
RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2.   # ● 主处理
  3.   #--------------------------------------------------------------------------
  4.   def main
  5.     # 战斗测试的情况下
  6.     if $BTEST
  7.       battle_test
  8.       return
  9.     end
  10.     # 载入数据库
  11.     $data_actors        = load_data("Data/Actors.rxdata")
  12.     $data_classes       = load_data("Data/Classes.rxdata")
  13.     $data_skills        = load_data("Data/Skills.rxdata")
  14.     $data_items         = load_data("Data/Items.rxdata")
  15.     $data_weapons       = load_data("Data/Weapons.rxdata")
  16.     $data_armors        = load_data("Data/Armors.rxdata")
  17.     $data_enemies       = load_data("Data/Enemies.rxdata")
  18.     $data_troops        = load_data("Data/Troops.rxdata")
  19.     $data_states        = load_data("Data/States.rxdata")
  20.     $data_animations    = load_data("Data/Animations.rxdata")
  21.     $data_tilesets      = load_data("Data/Tilesets.rxdata")
  22.     $data_common_events = load_data("Data/CommonEvents.rxdata")
  23.     $data_system        = load_data("Data/System.rxdata")
  24.     # 生成系统对像
  25.     $game_system = Game_System.new
  26.     # 生成标题图形
  27.     @sprite = Sprite.new
  28.     @sprite.bitmap = RPG::Cache.title($data_system.title_name)
  29.     # 生成命令窗口
  30.     s1 = "新游戏"
  31.     s2 = "继续"
  32.     s3 = "退出"
  33.     @command_window = Window_Command.new(192, [s1, s2, s3])
  34.     @command_window.back_opacity = 160
  35.     @command_window.x = 320 - @command_window.width / 2
  36.     @command_window.y = 288
  37.     # 判定继续的有效性
  38.     # 存档文件一个也不存在的时候也调查
  39.     # 有效为 @continue_enabled 为 true、无效为 false
  40.     @continue_enabled = false
  41.     for i in 0..3
  42.       if FileTest.exist?("Save#{i+1}.rxdata")
  43.         @continue_enabled = true
  44.       end
  45.     end
  46.     # 继续为有效的情况下、光标停止在继续上
  47.     # 无效的情况下、继续的文字显示为灰色
  48.     if @continue_enabled
  49.       @command_window.index = 1
  50.     else
  51.       @command_window.disable_item(1)
  52.     end
  53.     # 演奏标题 BGM
  54.     $game_system.bgm_play($data_system.title_bgm)
  55.     # 停止演奏 ME、BGS
  56.     Audio.me_stop
  57.     Audio.bgs_stop
  58.     # 执行过渡
  59.     Graphics.transition
  60.     # 主循环
  61.     loop do
  62.       # 刷新游戏画面
  63.       Graphics.update
  64.       # 刷新输入信息
  65.       Input.update
  66.       # 刷新画面
  67.       update
  68.       # 如果画面被切换就中断循环
  69.       if $scene != self
  70.         break
  71.       end
  72.     end
  73.     # 装备过渡
  74.     Graphics.freeze
  75.     # 释放命令窗口
  76.     @command_window.dispose
  77.     # 释放标题图形
  78.     @sprite.bitmap.dispose
  79.     @sprite.dispose
  80.   end

       我们看到,在注释“准备过渡”之前,进行的都是一些设置的工作,比如载入数据库,生成各种图片和窗口,在设置完毕之后,准备过渡到画面状态(此时画面上什么也没有),然后接着是一个无限循环loop do,按照一定顺序刷新各种东西,在这里我们看到了Graphics.update,刷新游戏画面,然后是Input.update,这里Input也是内部模块,用来处理输入用的,最后是一个单独的update,这个其实是该场景内部的刷新方法,需要我们在后面进行追加定义。刷新完毕后,判断场景是否已经被切换,即$scene变量是否在update之后发生改变。如果改变,退出无限循环,场景结束,进行各种事后处理(各种释放和过渡)。
       在这里,可能有些人注意到了,虽然这里的Sprite的位图是用RPG::Cache模块载入的,但是在释放精灵之前,仍然对此位图进行了释放操作。这似乎和我前面提到的释放原则不一致。在这里,我要仔细说明一下使用RPG::Cache模块的原则。首先,RPG::Cache模块存在的意义,就是将图片载入内存中,如果该图片需要重复被使用(或者被多个Sprite对象使用),那么将其放入缓存中可以节约内存,提高载入速度,所以RPG::Cache中的位图是可以不释放的。而对于一些使用频率较少的位图,则不必将其放入RPG::Cache中,或者是使用之后及时释放,以腾出更多空间来存储使用率更大的位图。
       整个过程清晰明了,需要我们做的,就是丰富这个骨架的内容。
       我们注意中间那个loop do的无限循环,原则上是1帧执行1次,因此里面放的方法都是定期需要重复执行的方法,这种方法通常我们命名为update(注意不是refresh),也就是说,放到这里的东西,都应该是我们需要反复刷新的,如果不需要反复刷新,则一般不要单独放进去(可以放到场景私有的update方法里,然后再进行判断)。

5.2.2  场景的具体实现
       接下来我们就可以解读一下场景的具体实现了。我们以Scene_Item为例,来具体说明一下。
RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2.   # ● 主处理
  3.   #--------------------------------------------------------------------------
  4.   def main
  5.     # 生成帮助窗口、物品窗口
  6.     @help_window = Window_Help.new
  7.     @item_window = Window_Item.new
  8.     # 关联帮助窗口
  9.     @item_window.help_window = @help_window
  10.     # 生成目标窗口 (设置为不可见・不活动)
  11.     @target_window = Window_Target.new
  12.     @target_window.visible = false
  13.     @target_window.active = false
  14.     # 执行过度
  15.     Graphics.transition
  16.     # 主循环
  17.     loop do
  18.       # 刷新游戏画面
  19.       Graphics.update
  20.       # 刷新输入信息
  21.       Input.update
  22.       # 刷新画面
  23.       update
  24.       # 如果画面切换就中断循环
  25.       if $scene != self
  26.         break
  27.       end
  28.     end
  29.     # 装备过渡
  30.     Graphics.freeze
  31.     # 释放窗口
  32.     @help_window.dispose
  33.     @item_window.dispose
  34.     @target_window.dispose
  35.   end

       上面是Scene_Item的main方法,主要生成了3个窗口,帮助窗口,物品窗口,目标窗口。而最初,目标窗口是不可见的(因为你还没使用某个道具),而帮助窗口和物品窗口是互相关联的,必须设置好。然后就可以过渡了,整个过程非常清晰。
RUBY 代码复制
  1. def update
  2.     # 刷新窗口
  3.     @help_window.update
  4.     @item_window.update
  5.     @target_window.update
  6.     # 物品窗口被激活的情况下: 调用 update_item
  7.     if @item_window.active
  8.       update_item
  9.       return
  10.     end
  11.     # 目标窗口被激活的情况下: 调用 update_target
  12.     if @target_window.active
  13.       update_target
  14.       return
  15.     end
  16.   end

       这就是我们在上面说的,场景私有update方法的定义,这个方法定义非常重要,这决定了你的场景是否能和玩家互动。
       一般来说,update方法分为2部分,一是自动刷新,即无论玩家有没有操作,都必须刷新的对象;二是条件刷新,当玩家有一定操作时,进行刷新相应对象。执行的顺序是先自动刷新,再条件刷新,这个顺序不能变。我们在这里可以设置一个输入等待的机制,必须等待固定时间,才能接受玩家的输入(注意,自动刷新是一直进行的)。这种情况出现的时候有很多,比方说玩家按了一个按键,画面进行某种变换,变化持续时间是1秒(40帧),你不希望在画面变化的时候接受玩家的其他输入,这时候就有必要设置输入等待了。比方说  这里,我们先要在main方法内设置一个内部变量@wait_count,并初始化为0,表示等待计数的时间。然后在update的条件刷新之前,放上以下代码:
RUBY 代码复制
  1. if @wait_count > 0
  2.   @wait_count -= 1
  3.   return
  4. end

       这就表示在等待时间不为0的情况下,将等待时间减去1,注意,update方法是1帧调用一次,@wait_count等于多少,就意味着要等待多少帧。随后立即结束update方法,注意,那个return不能少,这个return是避免update进行条件更新的,因此在@wait_count大于0的情况下,系统无法接受玩家的输入。这个问题就被我们解决了。
       而在这里,条件刷新有两个地方,如果物品窗口被激活就调用update_item方法,如果目标窗口被激活就调用update_target方法。这个必须分开设置,因为不同状态下刷新的规则肯定是不一样的。另外还需要注意每一个分支下面,要有return,否则很可能出现在同一次update下进行两种以上更新的情况。
RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2.   # ● 刷新画面 (物品窗口被激活的情况下)
  3.   #--------------------------------------------------------------------------
  4.   def update_item
  5.     # 按下 B 键的情况下
  6.     if Input.trigger?(Input::B)
  7.       # 演奏取消 SE
  8.       $game_system.se_play($data_system.cancel_se)
  9.       # 切换到菜单画面
  10.       $scene = Scene_Menu.new(0)
  11.       return
  12.     end
  13.     # 按下 C 键的情况下
  14.     if Input.trigger?(Input::C)
  15.       # 获取物品窗口当前选中的物品数据
  16.       @item = @item_window.item
  17.       # 不使用物品的情况下
  18.       unless @item.is_a?(RPG::Item)
  19.         # 演奏冻结 SE
  20.         $game_system.se_play($data_system.buzzer_se)
  21.         return
  22.       end
  23.       # 不能使用的情况下
  24.       unless $game_party.item_can_use?(@item.id)
  25.         # 演奏冻结 SE
  26.         $game_system.se_play($data_system.buzzer_se)
  27.         return
  28.       end
  29.       # 演奏确定 SE
  30.       $game_system.se_play($data_system.decision_se)
  31.       # 效果范围是我方的情况下
  32.       if @item.scope >= 3
  33.         # 激活目标窗口
  34.         @item_window.active = false
  35.         @target_window.x = (@item_window.index + 1) % 2 * 304
  36.         @target_window.visible = true
  37.         @target_window.active = true
  38.         # 设置效果范围 (单体/全体) 的对应光标位置
  39.         if @item.scope == 4 || @item.scope == 6
  40.           @target_window.index = -1
  41.         else
  42.           @target_window.index = 0
  43.         end
  44.       # 效果在我方以外的情况下
  45.       else
  46.         # 公共事件 ID 有效的情况下
  47.         if @item.common_event_id > 0
  48.           # 预约调用公共事件
  49.           $game_temp.common_event_id = @item.common_event_id
  50.           # 演奏物品使用时的 SE
  51.           $game_system.se_play(@item.menu_se)
  52.           # 消耗品的情况下
  53.           if @item.consumable
  54.             # 使用的物品数减 1
  55.             $game_party.lose_item(@item.id, 1)
  56.             # 再描绘物品窗口的项目
  57.             @item_window.draw_item(@item_window.index)
  58.           end
  59.           # 切换到地图画面
  60.           $scene = Scene_Map.new
  61.           return
  62.         end
  63.       end
  64.       return
  65.     end
  66.   end

       在这里我们终于看到了对各种输入的处理,知道了物品的使用效果和消耗,都是在update及其子方法里面实现的。注意,Window_Selectable中光标的移动不在此中,Window_Selectable内部的update上。这里大家粗略看一下就能明白大意,我就不多做介绍了。在这里注意refresh方法的调用,我们调用refresh方法,只是在窗口需要刷新的时候才进行调用。如果窗口需要刷新的原因是我们输入了某种指令,那么在指令的最后,一定要重新刷新窗口,否则一般不进行窗口内容再描绘的处理(因为这样太消耗时间了)。
       最后我们说一下带initialize方法的场景类。Scene类可以不用带initialize方法,但是在某些场合下,我们需要设置initialize方法。
       比方说Scene_Menu场景,就有initialize方法,这里的initialize方法非常简单。
RUBY 代码复制
  1. def initialize(menu_index)
  2.   @menu_index = menu_index
  3. end

       这是设置菜单初始光标位置,考虑到从不同场景退回Scene_Menu,菜单光标位置不同,才考虑加的这个内部变量。比方说从Scene_Item返回,要写$scene = Scene_Menu.new(0),这是因为物品选项在菜单的第一个位置上。其余的同理。

       场景类的基本知识就到这里,在第6章节,我们要亲自DIY一个场景,并解读一些复杂场景,大家敬请期待吧。
回复 支持 反对

使用道具 举报

Lv4.逐梦者 (版主)

梦石
0
星屑
9497
在线时间
5073 小时
注册时间
2013-6-21
帖子
3580

开拓者贵宾剧作品鉴家

10
 楼主| 发表于 2013-11-15 09:43:26 | 显示全部楼层
本帖最后由 RyanBern 于 2015-8-26 22:05 编辑

[box=RoyalBlue]
第6章节:场景的使用(二)
[/box]

       说正题前,扯一点题外话。好久没填坑了,眼睁睁看着帖子沉了不太好啊,不过貌似这个教程贴没什么回复呢,偶好伤心啊,我的水平就这么差么,啊啊啊……最近忙得要死,也没时间写了,明天就有一门考试等着呢……
6.1  朴素的任务脚本
       在我们真正开始这部分内容,让我们回顾一下场景制作的一般步骤。首先是定义main方法,第一步是建立各种可见的对象,然后进行主循环,最后进行释放dispose操作。其中,主循环要进行三步刷新,Graphics.update,Input.update,update,其中,我们要重点定义update的内容,包括自动更新和对各种输入的反应。那么我们想要写一个脚本时,先判断需不需要一个独立的窗口来处理这个问题。对于我们今天的“朴素的任务脚本”来说,就比较适合用一个场景来描述任务。
6.1.1  游戏对象的设计:任务对象
       在6R中,已经有各色各类的任务脚本,本人见到的第一个任务脚本,是采用的物品——武器——道具描述法,即把任务数据存放在RMXP内部数据库中。这样做的好处就是方便不会用脚本的人来设计任务,但缺点就是它把任务这一和物品,武器,道具不太相同的游戏对象混在一起了,因此,我们采取定义新对象的方式,考虑这是一种RPG内部数据,因此也把它定义在RPG模块下为好。
       一个任务应该具有的基本属性有:名称,内容。当然,内容你可以再扩展很多很多。比方说内容可以分为具体描述,难度,奖励,目标的人物或地点或物品,结算任务的地点和人物等等。在这里,我们只挑选几个有代表性的定义就OK。
RUBY 代码复制
  1. module RPG
  2.   class Journal
  3.     attr_accessor :name           # 名称(String)
  4.     attr_accessor :description       # 描述(String 数组)
  5.     attr_accessor :difficulty        # 难度(Integer)
  6.     attr_accessor :reward_gold     # 奖励金钱(Integer)
  7.     attr_accessor :reward_items    # 奖励物品(数组,内部元素依然是数组,格式为[物品ID, 物品数量])
  8.     attr_accessor :reward_weapons  # 奖励武器,格式同上
  9.     attr_accessor :reward_armors    # 奖励防具,格式同上
  10.     def initialize
  11.       @name = ""
  12.       @description = ["","",""]
  13.       @difficulty = 0
  14.       @reward_gold = 0
  15.       @reward_items = []
  16.       @reward_weapons = []
  17.       @reward_armors = []
  18.     end
  19.   end
  20. end

       在这里,我们初始化的时候,并没有急着描述任务的内容,这个到后面的时候再写也不迟。每个属性后面都有注释,来说明是用什么样的结构来表示这个属性。这点在后面的initialize方法里面也能看出来。
       注意,这里的@description表示一个含有三个字符串的数组,这是考虑到draw_text无法换行,而描述又比较多,写下三行会比较美观。难度@difficulty是自己定义的,可以有很多等级,在这里我们定义6个等级(0到5),当然,0级就是那种只要电脑不死机就能完成的任务,5级当然就是要花很大力气才能通关的任务。
       当然,作为一种游戏的数据,地位和$data_items之类的应该相当,都应该在游戏刚打开的时候就被载入。但是,由于定义的性质不同,其他的数据早在你用RMXP之时,就已经安静地躺在data文件夹下,用的时候读取文件即可,但是我们新定义的RPG::Journal却不是这样,因此我们只能在游戏中处理它(会浪费一些时间,不过数据规模不大的话影响应该很小)。
       和其他数据一样,我们用一个全局变量(其实是一个数组)$data_journals来表示所有任务的数据,为了和游戏内部统一,我们把$data_journals的0号单元置为nil,其余的都是RPG::Journal类的对象。我们需要定义一下方法,这个方法我们定义为RPG模块的模块方法(module function):
RUBY 代码复制
  1. module RPG
  2.   # 定义模块方法 get_journal_data
  3.   def self.get_journal_data
  4.     data = [nil]
  5.     # 1号任务
  6.     journal = RPG::Journal.new
  7.     journal.name = "第一个任务"
  8.     journal.description[0] = "随便玩玩就可以啦"
  9.     journal.difficulty = 0
  10.     journal.reward_items = [[1,1],[2,1]]
  11.     data.push(journal)
  12.     # 1号任务完毕
  13.     return data
  14.   end
  15. end

       在这里,我们看到了,有关任务奖励的,其实是一个二维数组(金钱的除外),表示奖励,每一个大数组的元素又是一个含有2个元素的小数组,前面那个表示ID,后面那个表示数量。我们只需要模仿这个模式,逐个定义即可。最后那个return是必须的,以便$data_journals接受我们的返回值。完成这个方法后,一定要在Scene_Title相关载入数据的下面写上$data_journals = RPG.get_journal_data,在这里就不再写代码示例了。有关多个任务数据的定义,直接复制上面代码中“1号任务——1号任务完毕”中间的部分,依次排在return data之前,就可以定义其他的任务,ID就是脚本编辑器中的顺序(建议用注释标明)。这样,我们的游戏对象就定义完成了。
       不过,还有一个问题,就是如何保存队伍中当前的任务,要知道数据库中的任务和实际有的任务是不一样的(就像数据库有所有的道具,但是队伍中只含有一部分)。考虑到任务是队伍的一个属性,因此可以定义在Game_Party内部,当然也可以单独存放在别的位置里面。在这里我们用一种特殊的方法来表示,借助游戏变量$game_variables来存储。这是因为$game_variables是可以写到存档数据内部的,虽然这里存储着游戏中所有的自定义变量(默认是数值,就是事件编辑器里面的“变量操作”操作的变量),但是实际上可以存储任何东西。我们就用它来存储当前队伍的任务数据。我们选定一个变量ID,例如1号来保存我们的当前任务,考虑到任务只有两种状态(即接受和完成),我们只需要定义一个数组来保存当前所有任务的ID即可。具体实现方法就是在初始化游戏的时候(Scene_Title里面),写下$game_variables[1] = [],来初始化当前任务内容,不过,1号变量在游戏中就不能作为它用了,这点要格外注意。
       第二版注:$game_variables是个筐,什么都可以往里装。虽然此话不假,但是这样做违反了脚本编写的一致性,现在更推荐将任务存储在Game_Party的内部。不过这样对教程的改动略大,因此第二版对此不加改动,希望大家注意。
6.1.2  任务的窗口描述
       有了这个,我们才能在窗口内描述任务的细节,以便让玩家更清楚了解情况。首先我们要知道,任务窗口大概分为两部分,一是描述所有当前的任务名称,二是描述任务细节。这个跟我们道具的显示是一样的,分为显示道具名字和说明。因此,我们要制作两个窗口为好。
       首先是显示任务名字的部分:
       回忆一下窗口的制作过程,是初始化——描绘(refresh)——刷新窗口(如果有需要的话),初始化做的工作是设置窗口位置和大小,refresh是描述窗口内容(不要重复刷新,很浪费时间)。
       如果大家窗口已经使用熟练的话,应该很容易。这个窗口的描绘请参考Window_Item。
RUBY 代码复制
  1. # 显示当前任务的窗口
  2. class Window_Journal < Window_Selectable
  3.   # 对象初始化
  4.   def initialize
  5.     super(0, 64, 240, 416)
  6.     self.index = 0
  7.     refresh
  8.   end
  9.   # 取得当前光标选中的任务信息
  10.   def journal
  11.     return @data[self.index]
  12.   end
  13.   # 刷新
  14.   def refresh
  15.     # 如果内容被设置了就释放
  16.     if self.contents != nil
  17.       self.contents.dispose
  18.       self.contents = nil
  19.     end
  20.     @data = []
  21.     # 添加当前任务
  22.     for i in $game_variables[JOURNAL]
  23.       @data.push($data_journals[i])
  24.     end
  25.     # 取得最大项目数
  26.     @item_max = $game_variables[JOURNAL].size
  27.     # 最大项目数不为 0 就开始描绘
  28.     if @item_max > 0
  29.       self.contents = Bitmap.new(width - 32, @item_max * 32)
  30.       for i in 0...@item_max
  31.         draw_item(i)
  32.       end
  33.     else
  34.       self.contents = Bitmap.new(width - 32, 32)
  35.       self.contents.draw_text(4, 0, width - 36, 32, "无任务")
  36.     end
  37.   end
  38.   # 描绘项目
  39.   def draw_item(i)
  40.     x = 4
  41.     y = i * 32
  42.     bitmap = RPG::Cache.icon("Journal.png")
  43.     name = @data[i].name
  44.     self.contents.blt(x, y, bitmap, Rect.new(0,0,24,24))
  45.     self.contents.draw_text(x+28, y, width-x-36-24, 32, name)
  46.   end
  47. end

       在这里,我们用一个常量JOURNAL来表示队伍中含有的任务数组在$game_variables里面存放的位置,当然可以随便更改。这个窗口是仿照了Window_Item脚本,没写太多注释,大家可以对比着看一下,都比较简单。
       然后就是显示任务具体内容的窗口,这个用Window_Base生成就好,不过还是有些地方需要大家注意一下。首先大家要清楚,这个窗口是为了描述某个具体任务的,因此必须要有一个私有的实变量来保存当前描绘的任务,当然你不必将它设置为属性。还有一点就是关于描述任务奖励的,考虑到任务奖励种类比较多,为了美观,我们把它们合并在一起来描绘。
RUBY 代码复制
  1. # 描绘任务具体内容的窗口
  2. class Window_Journal_Contents < Window_Base
  3.   def initialize
  4.     super(240, 64, 400, 416)
  5.     self.contents = Bitmap.new(width - 32, height - 32)
  6.     @journal = nil
  7.     refresh
  8.   end
  9.   # 设置当前要描绘的任务
  10.   def journal=(journal)
  11.     # 如果任务和当前任务有差异
  12.     if @journal != journal
  13.       @journal = journal
  14.       refresh
  15.     end
  16.   end
  17.   def refresh
  18.     self.contents.clear
  19.     self.contents.font.size = 18
  20.     self.contents.font.color = system_color
  21.     self.contents.draw_text(0, 0, 72, 24, "具体内容")
  22.     self.contents.draw_text(0, 96, 36, 24, "难度")
  23.     self.contents.draw_text(0, 144, 72, 24, "任务奖励")
  24.     if @journal != nil
  25.       self.contents.font.color = normal_color
  26.       (0..2).each do |i|
  27.         self.contents.draw_text(0, 24 + 24 * i, width - 32, 24, @journal.description[i])
  28.       end
  29.       diff = "★" * @journal.difficulty + "☆" * (5 - @journal.difficulty)
  30.       self.contents.draw_text(0, 120, width - 32, 24, diff)
  31.       total = 0
  32.       for item in @journal.reward_items
  33.         x = 184 * (total % 2)
  34.         y = 168 + 24 * (total / 2)
  35.         name = $data_items[item[0]].name
  36.         icon = RPG::Cache.icon($data_items[item[0]].icon_name)
  37.         number = item[1]
  38.         self.contents.blt(x, y, icon, Rect.new(0,0,24,24))
  39.         self.contents.draw_text(x + 28, y, 126, 24, name)
  40.         self.contents.draw_text(x + 28 + 126, y, 30, 24, number.to_s, 2)
  41.         total += 1
  42.       end
  43.       for item in @journal.reward_weapons
  44.         x = 184 * (total % 2)
  45.         y = 168 + 24 * (total / 2)
  46.         name = $data_weapons[item[0]].name
  47.         icon = RPG::Cache.icon($data_weapons[item[0]].icon_name)
  48.         number = item[1]
  49.         self.contents.blt(x, y, icon, Rect.new(0,0,24,24))
  50.         self.contents.draw_text(x + 28, y, 126, 24, name)
  51.         self.contents.draw_text(x + 28 + 126, y, 30, 24, number.to_s, 2)
  52.         total += 1
  53.       end
  54.       for item in @journal.reward_armors
  55.         x = 184 * (total % 2)
  56.         y = 168 + 24 * (total / 2)
  57.         name = $data_armors[item[0]].name
  58.         icon = RPG::Cache.icon($data_armors[item[0]].icon_name)
  59.         number = item[1]
  60.         self.contents.blt(x, y, icon, Rect.new(0,0,24,24))
  61.         self.contents.draw_text(x + 28, y, 126, 24, name)
  62.         self.contents.draw_text(x + 28 + 126, y, 30, 24, number.to_s, 2)
  63.         total += 1
  64.       end
  65.       if @journal.reward_gold > 0
  66.         str = "获得金钱:" + @journal.reward_gold.to_s
  67.         y = 168 + ((total-1) / 2 + 1) * 24
  68.         self.contents.draw_text(0, y, width - 32, 24, str)
  69.       elsif total == 0
  70.         self.contents.draw_text(0, 168, 18, 24, "无")
  71.       end
  72.     end
  73.   end
  74. end

       注意那个journal方法的定义,只有在@journal和参数不相等的时候,才进行刷新,这样也是为了减少refresh调用的次数。
       中间refresh定义得比较啰嗦,不过物品武器装备这三个在一起真的好烦啊,希望高手能精简一下哈。大家可能注意到了,我们现在创建的两个窗口的y坐标都是64而不是0,这是由于我们在最顶端要说明每个窗口是干什么用的。
6.1.3  任务场景的制作
       有了这两个窗口,任务场景的制作就会相当简单。新加的东西不多,不过要注意这里在场景中生成Window_Base的方法,因为这个窗口内容太单一了,我们就不单独给设一个类了。
       另外,在update中,要保持左右窗口描述的任务一致,因此我们要不定期执行这个语句:
RUBY 代码复制
  1. @contents_window.journal = @journal_window.journal

       在这里,我们假设在地图Scene_Map上可以查看任务,那么在Scene_Map脚本也要进行输入的处理,这里略去过程了,想必大家已经会怎么弄了吧。
RUBY 代码复制
  1. # 查看任务的场景
  2. class Scene_Journal
  3.   def main
  4.     @journal_window = Window_Journal.new
  5.     @contents_window = Window_Journal_Contents.new
  6.     @base1 = Window_Base.new(0, 0, 240, 64)
  7.     @base1.contents = Bitmap.new(208, 32)
  8.     @base1.contents.draw_text(0, 0, 96, 32, "任务名称")
  9.     @base2 = Window_Base.new(240, 0, 400, 64)
  10.     @base2.contents = Bitmap.new(368, 32)
  11.     @base2.contents.draw_text(0, 0, 96, 32, "具体内容")
  12.     Graphics.transition
  13.     loop do
  14.       Graphics.update
  15.       Input.update
  16.       update
  17.       if $scene != self
  18.         break
  19.       end
  20.     end
  21.     Graphics.freeze
  22.     @base1.dispose
  23.     @base2.dispose
  24.     @journal_window.dispose
  25.     @contents_window.dispose
  26.   end
  27.   def update
  28.     @base1.update
  29.     @base2.update
  30.     @journal_window.update
  31.     @contents_window.journal = @journal_window.journal
  32.     # 按下 B 键的情况下,返回地图
  33.     if Input.trigger?(Input::B)
  34.       $game_system.se_play($data_system.cancel_se)
  35.       $scene = Scene_Map.new
  36.     end
  37.   end
  38. end

       来看看这个脚本的效果吧,这个朴素的任务脚本就做完了,是不是很简单?

6.2  默认回合制战斗场景的解读
       RMXP中最复杂的场景脚本应当是这个回合制战斗了。虽然作为一个游戏的默认系统,但是如果接触RGSS时间不长,恐怕也难以写出一个没有BUG的回合制脚本。需要指出的是,游戏默认Scene_Battle虽然很长,但是从算法上和原理上都不难理解,从这个角度上来说也非常适合新手学习。
6.2.1  准备工作
       我们打开Scene_Battle,翻开它的分割定义1,可以看到main方法以及update方法。有了解读脚本的基础,我们对main方法的基本定义模式已经非常熟悉了。这里main方法的结构和普通场景相似,初始化数据——生成窗口——主循环——释放。但是在这里,我们要额外生成战斗场景用的活动块,这里需要调用Spriteset_Battle类的一个对象,这里Spriteset_Battle是一个特殊的类,这个活动块里面含有几乎所有战斗画面上显示的内容(窗口除外),例如角色的战斗图形,战斗背景图等等。另外,战斗场景中需要显示技能和物品的窗口,但是在main方法里面没有生成它们。这样做的原因可能是为了节省内存空间,但是一遍一遍生成可能会以时间为代价。
       接下来的update才是这里的关键所在。在update之前,先定义了几个Scene_Battle的内部方法,我们暂时可以先不看。update进行的方法,首先是刷新战斗事件,然后刷新系统对象,再然后是计时器,窗口,活动块。刷新完毕后,本应该等待玩家输入以便进行条件刷新,但是在这之前要等待一些效果执行完毕(包括手动等待),才能接受玩家的输入,这个机制和我们之前讲的输入等待是一样的。之后,就是各种条件刷新了。
       在条件刷新下,我们看到,这里并不是根据窗口的激活情况进行的判断刷新,而是根据战斗进行的回合种类进行分拆。说是各种回合,其实就是在一个回合下的不同阶段而已。在后面我们会看到,在同一个阶段进行的刷新,才进一步根据窗口激活情况不同而进行选择刷新。所以,大家也要多多借鉴这种刷新机制,如果场景比较简单,就可以通过窗口激活状况进行刷新;如果场景比较复杂,那么你可能也要引入一个类似于“回合”的变量来控制。
6.2.2  五个战斗阶段的解读
       接下来我们就要分别说说这五个阶段Scene_Battle都做了些什么。
       打开Scene_Battle分割定义2,在这个分割定义中,分别定义了第一阶段,第二阶段,第五阶段。这些阶段的执行都比较简单,为了方便放在一起了。
  • 第一阶段:自由战斗回合
           这里说得很不清楚,自由战斗回合实际上就是指事先处理各种战斗事件,强制行动,玩家不能操控的回合。在战斗事件的设置中,如果选择了“回合0”,那么这样的事件会立即在进入场景后执行。其余的“回合X”,就是在经过了X回合之后的自由战斗回合(第一阶段)立即执行。如果执行完毕,那么就进入第二阶段。第一阶段就是这样短暂。
  • 第二阶段:同伴命令回合
           实际上就是进行队伍总体操作的回合,这场战斗你是打还是不打,如果选择“战斗”则进入第三阶段,如果选择“逃跑”,则进行相应的处理。在这里我们看到了逃跑的基本处理,代码如下:
    RUBY 代码复制
    1. def update_phase2_escape
    2.     # 计算敌人速度的平均值
    3.     enemies_agi = 0
    4.     enemies_number = 0
    5.     for enemy in $game_troop.enemies
    6.       if enemy.exist?
    7.         enemies_agi += enemy.agi
    8.         enemies_number += 1
    9.       end
    10.     end
    11.     if enemies_number > 0
    12.       enemies_agi /= enemies_number
    13.     end
    14.     # 计算角色速度的平均值
    15.     actors_agi = 0
    16.     actors_number = 0
    17.     for actor in $game_party.actors
    18.       if actor.exist?
    19.         actors_agi += actor.agi
    20.         actors_number += 1
    21.       end
    22.     end
    23.     if actors_number > 0
    24.       actors_agi /= actors_number
    25.     end
    26.     # 逃跑成功判定
    27.     success = rand(100) < 50 * actors_agi / enemies_agi
    28.     # 成功逃跑的情况下
    29.     if success
    30.       # 演奏逃跑 SE
    31.       $game_system.se_play($data_system.escape_se)
    32.       # 还原为战斗开始前的 BGM
    33.       $game_system.bgm_play($game_temp.map_bgm)
    34.       # 战斗结束
    35.       battle_end(1)
    36.     # 逃跑失败的情况下
    37.     else
    38.       # 清除全体同伴的行动
    39.       $game_party.clear_actions
    40.       # 开始主回合
    41.       start_phase4
    42.     end
    43.   end

           逃跑成功与否取决于处于战斗中的敌人和角色速度平均值的大小(已经阵亡的战斗者和没有出现的战斗者不算在内),如果敌人和角色速度平均值相等,则由50%的几率成功逃跑。当然,如果逃跑失败了,游戏还是要继续的,这时候所有角色都没有行动,整个队伍只能被敌人痛扁一顿……当然,如果你不喜欢这种逃跑的设定,完全可以通过修改,跳过第二阶段,不妨自己试一下吧。
  • 第三阶段:角色命令回合
           这个阶段相对前两个阶段,比较复杂,但是思路还是很明确的。在这个阶段,玩家为各个角色设定行动(当然行动能否执行取决于主回合执行的情况)。按照角色在队伍中的顺序来为各个角色设定他们的行为,首先是角色的基本命令,然后根据基本命令来选择接下来要显示的窗口。
    RUBY 代码复制
    1. def update_phase3
    2.     # 敌人光标有效的情况下
    3.     if @enemy_arrow != nil
    4.       update_phase3_enemy_select
    5.     # 角色光标有效的情况下
    6.     elsif @actor_arrow != nil
    7.       update_phase3_actor_select
    8.     # 特技窗口有效的情况下
    9.     elsif @skill_window != nil
    10.       update_phase3_skill_select
    11.     # 物品窗口有效的情况下
    12.     elsif @item_window != nil
    13.       update_phase3_item_select
    14.     # 角色指令窗口有效的情况下
    15.     elsif @actor_command_window.active
    16.       update_phase3_basic_command
    17.     end
    18.   end

           这就是第三阶段,根据窗口激活的不同来选择刷新方法。在这里,我们看到了特技窗口和道具窗口在这个地方生成。
    RUBY 代码复制
    1. def start_skill_select
    2.     # 生成特技窗口
    3.     @skill_window = Window_Skill.new(@active_battler)
    4.     # 关联帮助窗口
    5.     @skill_window.help_window = @help_window
    6.     # 无效化角色指令窗口
    7.     @actor_command_window.active = false
    8.     @actor_command_window.visible = false
    9.   end

           上面的方法是开始选择特技,在最后的结束特技选择时,@skill_window会被释放掉。
  • 第四阶段:主回合
           实际上就是战斗真正进行的阶段,前面几个阶段都是准备工作。
           在这个阶段,系统会自动生成敌人的作战行动(见方法start_phase4中的语句),而后决定行动的先后次序,代码如下。
    RUBY 代码复制
    1. def make_action_orders
    2.     # 初始化序列 @action_battlers
    3.     @action_battlers = []
    4.     # 添加敌人到 @action_battlers 序列
    5.     for enemy in $game_troop.enemies
    6.       @action_battlers.push(enemy)
    7.     end
    8.     # 添加角色到 @action_battlers 序列
    9.     for actor in $game_party.actors
    10.       @action_battlers.push(actor)
    11.     end
    12.     # 确定全体的行动速度
    13.     for battler in @action_battlers
    14.       battler.make_action_speed
    15.     end
    16.     # 按照行动速度从大到小排列
    17.     @action_battlers.sort! {|a,b|
    18.       b.current_action.speed - a.current_action.speed }
    19.   end

           首先是把所有的敌人和角色都放到@action_battlers这个数组中。然后为所有即将行动的战斗者确定行动速度,具体方法参见Game_Battler分割定义1里面的make_action_speed,行动速度为当前战斗者的速度加上一个随机平移量,范围是0到10+战斗者的速度/4。而后将目标数组按照速度大小,由小到大排序。
           而后我们看到了update_phase4的原型,同样地,这个刷新操作也是根据步骤来进行,第四阶段共分为6个步骤。为什么要这样分呢?原因是第四阶段不需要进行玩家的任何输入(当然公共事件的除外),而现实战斗者行动效果的时候,要一个个地显示,即不会出现两个战斗者同时进行物理攻击或者释放特技的情况(如果需要改,可以自定义),因此这6个步骤是针对每一个战斗者而设计的。系统从刚刚生成的@action_battlers中,按照速度从大到小,依次选出一个战斗者(实际上就是@action_battlers[0])作为当前行动的战斗者@active_battler,然后再进行各种操作。如此循环,当@action_battlers数组里面的元素都取出时,表示所有战斗者行动已经处理完毕,那么要开始一个新的回合。
    • 【步骤1】准备行动
             由于每一次行动都会影响到此次战斗胜败的判定(特指玩家胜败的判定),所以要在这一步进行一个判断,如果战斗胜败能够确定(即主角队伍和敌人队伍之一全灭),那么直接结束主回合,进入战斗的第五个阶段(在主角队伍胜利的情况下),否则才能进行下面的内容。具体判断方法参见Scene_Battle分割定义1的judge方法,战斗结束方法参见battle_end(result)方法。如果战斗需要继续进行,那么刷新战斗事件(注意不是物品或者技能的公共事件),从@action_battlers取出行动速度最大的作为@active_battler,进行各种准备操作(例如连续伤害和状态变化),然后就可以进行步骤2了。
    • 【步骤2】开始行动
            在这一步骤中,主要进行的是各种行动效果的判断,具体看代码:
      RUBY 代码复制
      1. def update_phase4_step2
      2.     # 如果不是强制行动
      3.     unless @active_battler.current_action.forcing
      4.       # 限制为 [敌人为普通攻击] 或 [我方为普通攻击] 的情况下
      5.       if @active_battler.restriction == 2 or @active_battler.restriction == 3
      6.         # 设置行动为攻击
      7.         @active_battler.current_action.kind = 0
      8.         @active_battler.current_action.basic = 0
      9.       end
      10.       # 限制为 [不能行动] 的情况下
      11.       if @active_battler.restriction == 4
      12.         # 清除行动强制对像的战斗者
      13.         $game_temp.forcing_battler = nil
      14.         # 移至步骤 1
      15.         @phase4_step = 1
      16.         return
      17.       end
      18.     end
      19.     # 清除对像战斗者
      20.     @target_battlers = []
      21.     # 行动种类分支
      22.     case @active_battler.current_action.kind
      23.     when 0  # 基本
      24.       make_basic_action_result
      25.     when 1  # 特技
      26.       make_skill_action_result
      27.     when 2  # 物品
      28.       make_item_action_result
      29.     end
      30.     # 移至步骤 3
      31.     if @phase4_step == 2
      32.       @phase4_step = 3
      33.     end
      34.   end

             这里的代码结构非常简单,首先是要判断当前战斗者能不能进行行动,如果能进行行动,那么能进行什么样的行动。这里的restriction就表示行动的限制,4为完全不能行动,2和3分别表示普通攻击同伴和普通攻击敌人。而后便真正进行行动效果的计算。
             在这里略去对行动效果计算的方法的解读,不过强烈建议大家看看这3个方法:attack_effect,skill_effect,item_effect,这三个方法可以在Game_Battler3里面寻找。这里面说的都是各种行动的效果是如何定义的,我们在改动战斗系统时,改动最频繁的就是这三个方法了。
    • 【步骤3、步骤4、步骤5】显示动画
             步骤3显示的是行动方的动画,步骤4显示的是对象方的动画,步骤5显示的是各种伤害(包括行动放和对象方),没什么好讲的,主要功能的实现是利用了@spriteset(战斗活动块)的方法,这个实例在一开始就已经生成了。大家如果有兴趣,可以翻开F1,搜索RPG::Sprite,在这里有战斗活动块的所有方法和属性的定义。
    • 【步骤6】公共事件
             在执行完动画之后,才轮到公共事件的处理。因此公共事件是每一个行动者行动的最后一步。执行完这一步后,返回到步骤1,进行下一个战斗者(如果有的话)的行动。
  • 第五阶段:结束战斗回合
           这个阶段只有在角色队伍胜利的情况下才进行。主要功能是显示战后所得,获得战斗胜利的奖励。这里的方法非常简单,大家可以自行解读start_phase5,update_phase5执行的实际上是战斗结果窗口的显示,需要等待100帧才能显示战斗结果窗口,在主角按下C键的时候,结束战斗,返回地图画面。


       这样,整个默认的战斗系统就被我们解读完了,不知道效果如何。总觉得自己跟什么都没说似的呢……不过具体效果还要看大家体会了,我没有提及的代码,大家最好也看看,这样能为自己改脚本提供思路。

       这个脚本教程贴已经快要接近尾声了哈,感觉自己已经把能说明白的东西都说了。下一章节,可能是最后一个章节了,我们将会谈谈Ruby内部的一些机制,这也是我使用Ruby多年的体会,大家就敬请期待吧。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2024-5-6 19:43

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表