Project1

标题: RGSS3小探——脚本结构的改变 [打印本页]

作者: DeathKing    时间: 2013-4-20 12:54
标题: RGSS3小探——脚本结构的改变
本帖最后由 DeathKing 于 2013-4-20 12:56 编辑

整理磁盘东西时发现有这么一个东西还没发布,现在发布也不太应景了,不过还是发布出来吧,这个来自于去年我做的系列报告《RGSS3小探》,这些就是一些介绍性的东西。从2012年1月20日第一次发表报告,到现在,都过了一年半了,还真是久啊。前几次都是精品,这次也申个精吧。 @论坛助理
第一次报告:http://rpg.blue/thread-220112-1-1.html
第二次报告(未完成):http://rpg.blue/thread-220654-1-1.html
PDF下载: RGSS3小探(二).pdf (670.18 KB, 下载次数: 256)
这只是一个报告,不是作为教学的教程!需要一定的脚本知识。



RGSS3小探(二)

——脚本结构的改变


1 引言

RGSS3在RGSS2的基础上,再次调整了脚本结构:保留了由RGSS2引入的Vocab、Sound、Cache模块,并加入了DataManager、SceneManager和BattleManager三个新模块。并且,脚本在细节部分进行了重构,如前一章提及到的Window_MenuCommand将生成命令列表拆分成了5个方法,而Scene_Base利用元编程技术,定义了update_all_windows和dispose_all_windows方法来减少额外的代码开销。稍后我们将详细讨论。

RGSS3最成功的改动则是将之前由Scene_Title负责的载入游戏数据的部分分离开来,并独立的写成DataManager模块,使得更具有规范意义。SceneManager同样重要,他使得以往漫天调用$scene的现象不复存在 ,SceneManager.return提供了快速退回到前一场景的功能。另外,从一些制作者反馈的信息来看,BattleManager的存在为制作明雷遇敌提供了方便。

2 启动过程

所有脚本定义完后,由Main脚本组调用rgss_main方法正式开始处理整个脚本。rgss_main也是RGSS3系统引入的新方法,引入这个方法是为了避免F12带来的堆栈过深的问题。rgss_main { SceneManager.run }调用,开始处理游戏画面。

SceneManager有三个重要的实例变量,他们首先被定义:

代码片段 2.1 RGSS3::SceneManager
  1. module SceneManager
  2.   @scene = nil                            # 当前场景实例
  3.   @stack = []                             # 场景切换的记录
  4.   @background_bitmap = nil             # 背景用的场景截图
  5. end
复制代码
@scene类似于之前的RGSS系统中的$scene,但他作为实例变量出现,显得更加地可控和合理。@stack是一个人为设定的栈(Stack),里面存放了调用场景的历史记录,这样使得我们有能力进行逐个回溯(Backtrack)。@background_bitmap是用作背景的场景截图,通常会经过模糊(blur)的处理。

代码片段 2.2 RGSS3::SceneManager
  1.   def self.run
  2.     DataManager.init
  3.     Audio.setup_midi if use_midi?
  4.     @scene = first_scene_class.new
  5.     @scene.main while @scene
  6.   end
复制代码
该方法首先初始化DataManager和Audio,然后根据实际情况建立首个Scene。在Scene建立完后,如果@scene具有意义,就调用@scene的main方法进行处理。而决定第一场景的方法定义如下:

代码片段 2.3 RGSS3::SceneManager

  1.   def self.first_scene_class
  2.     $BTEST ? Scene_Battle : Scene_Title
  3.   end
复制代码
系统通过判断是否设置了战斗测试($BTEST)标识来判定是生成战斗场景(Scene_Battle)还是标题场景(Scene_Title)。

考虑到Scene_Title不再具有加载游戏数据的实际意义,我们可以很轻易的做出跳过标题。修改后的self.first_sccene_class代码如下:

代码片段 2.4 RGSS3::SceneManager

  1.   #------------------------------------------------------------
  2.   # ● 获取最初场景的所属类
  3.   #------------------------------------------------------------
  4.   def self.first_scene_class
  5.     unless $BTEST  # 如果不是战斗测试就设置新游戏数据
  6.       DataManager.setup_new_game
  7.       $game_map.autoplay
  8.     end   
  9.     $BTEST ? Scene_Battle : Scene_Map
  10.   end
复制代码
3 审视SceneManager

SceneManager的代码不到100行,然而提供了一套规范化的Scene切换方案。@stack数组的引入尤其值得品味。

首先,场景的切换、返回以及历史的清除定义如下:

代码片段 3.1 RGSS3::SceneManager

  1.   #----------------------------------------------------------
  2.   # ● 切换
  3.   #----------------------------------------------------------
  4.   def self.call(scene_class)
  5.     @stack.push(@scene)
  6.     @scene = scene_class.new
  7.   end
  8.   #----------------------------------------------------------
  9.   # ● 返回到上一个场景
  10.   #----------------------------------------------------------
  11.   def self.return
  12.     @scene = @stack.pop
  13.   end
  14.   #----------------------------------------------------------
  15.   # ● 清空场景切换的记录
  16.   #----------------------------------------------------------
  17.   def self.clear
  18.     @stack.clear
  19.   end
复制代码
通过SceneManager.call切换场景,系统会把当前的Scene放到栈中,然后再切换到新的Scene。返回之前的场景,则直接弹出栈中的顶层元素即可。相较于RGSS之前的版本,这样做考虑更加周全。

RGSS2为了区别是从Scene_Title还是Scene_Menu呼出的Scene_File,使用了一个标识。然而,从实际上考虑,能呼出Scene_File的地方有很多,我们不能单一的判断是从Scene_Title还是Scene_Menu。RGSS3并不在乎是谁呼出的Scene_File,他总能够在呼出下一个Scene之前保存好当前的环境,并能轻易的恢复。简单来说:RGSS2像是特事特办,RGSS3则是人人平等。

实际上,RGSS3的默认脚本结构严谨,你不必担心@stack过深、占用太多内存等问题,下面的脚本提供了监视@stack变化的功能,你需要在调试时打开控制台。

代码片段 3.2 RGSS3::SceneManager 自定义

  1. module SceneManager
  2.   #----------------------------------------------------------
  3.   # ● 切换
  4.   #----------------------------------------------------------
  5.   def self.call(scene_class)
  6.     @stack.push(@scene)
  7.     @scene = scene_class.new
  8.     @stack.each { |c| print c.class.name.to_s + " -> " }
  9.     puts
  10.   end
  11.   #--------------------------------------------------------
  12.   # ● 返回到上一个场景
  13.   #--------------------------------------------------------
  14.   def self.return
  15.     @scene = @stack.pop
  16.     @stack.each { |c| print c.class.name.to_s + " -> " }
  17.     puts
  18.   end
  19. end
复制代码
然而,SceneManager中定义的某个方法,使得游戏存在严重的安全隐患。

代码片段 RGSS3::SceneManager

  1.   #--------------------------------------------------------
  2.   # ● 直接切换某个场景(无过渡)
  3.   #--------------------------------------------------------
  4.   def self.goto(scene_class)
  5.     @scene = scene_class.new
  6.   end
复制代码
SceneManager.goto的调用使得系统不会记录当前的Scene,因此,我们不建议在Scene_Map中通过SceneManager.goto(Scene_Menu)来呼出菜单。此时@stack为空值,当玩家按下ESC键返回时,@scene就会取nil值,导致游戏结束。除非有目的性的指定跳转,否则千万不要使用SceneManager.goto!

3 关于DataManager
DataManager并不是什么新东西——它从其他类里面提取了很多经常使用到的公共模式。例如,读取和载入数据库的工作,以往是由Scene_Title负责的。而存档和读档等存档操作以往是由Scene_File负责。其基本想法是将管理逻辑给集中化、合理化,使得整个系统的设计跟符合MVC的设计实践。

RGSS/RGSS2的程序员可以通过阅读DataManager的代码轻松过度到RGSS3。

4 Scene_Base的改动

Scene_Base是由RGSS2引入的,而定义Scene_Base很大程度上是为了规范Scene的编写并通过适当的抽象来减少代码。而RGSS3添加了update_all_windows来更新Scene中可能存在的窗体。而不需要像RGSS2那样傻傻的手动更新。

update、update_basic和update_all_windows的定义如下:

代码片段 4.1 RGSS3::Scene_Base

  1.   #-------------------------------------------------------
  2.   # ● 更新画面
  3.   #-------------------------------------------------------
  4.   def update
  5.     update_basic
  6.   end
  7.   #-------------------------------------------------------
  8.   # ● 更新画面(基础)
  9.   #-------------------------------------------------------
  10.   def update_basic
  11.     Graphics.update
  12.     Input.update
  13.     update_all_windows
  14.   end
  15.   #------------------------------------------------------
  16.   # ● 更新所有窗口
  17.   #------------------------------------------------------
  18.   def update_all_windows
  19.     instance_variables.each do |varname|
  20.       ivar = instance_variable_get(varname)
  21.       ivar.update if ivar.is_a?(Window)
  22.     end
  23.   end
复制代码
如果所有的Scene都由Scene_Base继承而来的话,那么他的update方法也将继承Scene_Base的定义。而update周全的考虑到了可能会用到的基本更新,因此我们无需画蛇添足。这也就是为什么很少在RGSS3的Scene脚本中见到update方法的原因了。

而update_all_window通过反射(Reflection)获得类中所有实例变量,且进行简单判断,若为Window的话则调用其update方法。这种自动化过程,使得我们可以只写少量的代码来完成大量的工作。从RGSS/RGSS2过渡来的脚本制作者应该适应并使用这种思维方式。

5 庞大的Window脚本
RGSS3中有着数量巨大的Window脚本,为历届之最:RGSS有Window脚本31个,RGSS2有29个,RGSS3则高达45个!并且,RGSS3的Window脚本有着更复杂的继承关系。Window类作为所有Window类的基础,其地位仍然没有改变,但以往直接继承自Window_Base的类,被重新设计,变成继承自Window_Selectable。

如果从细节上审视,又多了哪些类呢?



RGSS3把横向选择框给抽象成了一个单独的类,即Window_HorzCommand。虽然Window_HorzCommand只是对Window_Command的简单封装,但他提供了一个更加严谨的规范。Window_HorzCommand的一个子类Window_EquipCommand即是如下的窗口:



另外,RGSS3把一些实例给单独类化了:比如,RGSS系统的早期版本的标题命令窗体是由Window_Command类直接生成的实例,而RGSS3则是把他单独设计为了Window_TitleCommand类。这样做可以定义更丰富的内容,并且,联系第一小节对命令窗体的探讨,我们也可以发现,这样做我们可以很容易的向这些窗体添加新的选项。当然,缺点亦是有的,很明显的一点就是:我们的代码量增加了。

而作为所有Window类的“第二父亲”的Window_Base,又有怎样的变革呢?首先,取消了常量WLH(Window Line Height),取而代之的是line_height方法,虽然同为24,也有着相同的效果。同时,引入了一个新的属性——边距(Padding)。网页制作者对这个属性很熟悉,他是描述内容与容器的坐标偏移(Offset)。

很容易具化的讲解这个属性究竟是什么,因为我想大家都做过这么一件事:调整对话框的高度,使得他刚好能够容纳1行文字。当然,正如你所想得那样,我曾经也干过。于是,我在RGSS2上进行了试验:



不幸的是,尽管我把Window_Message的height调整得足够的小,但也留出了一些空白,文字与上下部都有16像素的距离。32 + WLH被认为是显示一行文字所需的最小窗口高度,考虑下面的代码:

代码片段5.1 RGSS2::Window_Base
  1.   #-----------------------------------------------------
  2.   # ● 生成窗口内容
  3.   #-----------------------------------------------------
  4.   def create_contents
  5.     self.contents.dispose
  6.     self.contents = Bitmap.new(width - 32, height - 32)
  7.   end
复制代码
虽然Window_Message的父类Window_Selectable中的create_contents方法的定义于此有别,但异曲同工,他解释了窗体的contents——可简单理解为可绘制区域——在被创立时,实际高度和宽度都比窗体的高度和宽度小32个像素。这样,可绘制内容相对于窗体的x、y坐标的偏移即为16像素。

相比之下,RGSS3就显得更为友好,他支持用户自定contents相对于Window的坐标偏移:通过padding属性或standard_padding方法。几个相关的方法定义如下:

代码片段5.2 RGSS3::Window_Base
  1.   #--------------------------------------------------------
  2.   # ● 获取标准的边距尺寸
  3.   #--------------------------------------------------------
  4.   def standard_padding
  5.     return 12
  6.   end
  7.   #--------------------------------------------------------
  8.   # ● 更新边距
  9.   #--------------------------------------------------------
  10.   def update_padding
  11.     self.padding = standard_padding
  12.   end
  13.   #--------------------------------------------------------
  14.   # ● 计算窗口内容的宽度
  15.   #--------------------------------------------------------
  16.   def contents_width
  17.     width - standard_padding * 2
  18.   end
  19.   #--------------------------------------------------------
  20.   # ● 计算窗口内容的高度
  21.   #--------------------------------------------------------
  22.   def contents_height
  23.     height - standard_padding * 2
  24.   end
复制代码
contents_width和contents_height都是计算contents的实际宽度和长度,他们需要通过调用standard_padding来获得padding值,并计算。默认padding值采用的是通过一个方法取得,而不是常量,这样做,使得我们有能力通过一些分歧,合理返回padding可能会使用的不同取值(通常是在子类重定义此方法)。self.padding仅作为一个存放padding值的容器,其使用频率并不太高,你可以使用全局搜索来查看。

在不修改standard_padding的情况下,最理想的One-Line-Window或许是这样的,不过如你所视,他仍有一些空白:



而我们可以修改一下standard_padding方法,考虑到不要影响到其他窗口,我直接在Window_Message中重定义了此方法,将下面的代码直接加入到Main脚本之前:

[code]class Window_Message
  # padding值的修改
  def standard_padding
    0
  end

  # 窗体高度的修改
  def window_height
    self.line_height
  end
end[code]

如果你还修改了窗体的宽度等其他属性,那么他看起来就像下图那样,不过,出乎我们意料的是——他不是特别的好看。



事实上,如果想要实现比较漂亮的ONE-LINE,即留有合适的空间的话,RGSS3提供了一个快捷的途径。在window_height方法中,获取窗体实际的考虑是要通过调用fitting_height方法进行计算并取得的。fitting_height通过利用传递过来的参数做出决策,而在Window_Message中,他的参数由visible_line_number方法获得。visible_line_number方法默认返回的4,意即默认显示4行,我们可以将这个值修改为2,并查看一下现象。我们使用“显示文章”指令显示如下文段:

I am the 1st line of texts;
I am the 2nd line of texts;
I am the 3rd line of texts;
I am the 4th line of texts;

即使我们没有人为的分为两段,RGSS3也很智能的将他们处理开来:



遗憾的是,RGSS3并没有将这个功能开放为一个接口,这将为在游戏进行过程中使用此功能带来诸多不便。但同时,脚本设计者可以利用此特性,设计出一些具有特色的系统,滚动显示的呼出对话框就是一个很好的例子。

还有哪些改进呢?我想draw_text_ex方法的加入算一个吧?在之前版本的RGSS系统中,的确有draw_text方法,然而他并不强大,于是RGSS3就引入了此方法,使得其有能力处理带控制符的字体。而RGSS3的很多地方都是采用的这个方法绘制文字,于是,RPG Maker VX Ace的选择窗体就允许你带入/n[]或/c[]等控制符:



这个功能惠及到了哪些地方呢?通过全局搜索draw_text_ex,我们不难发现:



讲解他的机理将会占用很大的篇幅,而我们会在稍后的章节中更系统的介绍,读者可以参考RGSS3小探的第五章——对话框机理。

6 总结

RGSS3的变动,可以说是很大,也可以说是不大。从工程的角度来看,RGSS3的规划更合理的——但也变得相对复杂。功能也更齐全,庞大的Window类就是一个很好的例子。总的来说,RGSS3算是一个进步,虽然一些改动会导致脚本兼容性出现一些问题,但是还是能够轻松过渡的。

准备迎接RGSS3的新时代吧!


作者: xingsy    时间: 2013-5-11 00:54
这个比一年前有用多了  话说  你那个 深入浅出RGSS3 是不是坑了?
作者: DeathKing    时间: 2013-5-11 08:06
xingsy 发表于 2013-5-11 00:54
这个比一年前有用多了  话说  你那个 深入浅出RGSS3 是不是坑了?

没坑,只是太忙写得有点慢,下个月会发部分内容。
话说,一年前哪个哪里没用了?
作者: xingsy    时间: 2013-5-11 11:26
DeathKing 发表于 2013-5-11 08:06
没坑,只是太忙写得有点慢,下个月会发部分内容。
话说,一年前哪个哪里没用了? ...

不是说那个没用,我的意思是这个对于我个人角度来说更有收获。
十分期待您那本书啊,打算用这个写毕业设计呢
作者: 秋寒    时间: 2013-5-14 12:27
高手啊,尽管看不懂,还是顶一下
作者: tangtang125125    时间: 2013-6-22 16:42
大大写的真好!!
作者: tangtang125125    时间: 2013-6-22 16:55
大大请教我读程序结构吧,我看的 跳转 判断 继承方面有点迷糊 还有对象方法 也有点晕
作者: DeathKing    时间: 2013-6-22 19:09
tangtang125125 发表于 2013-6-22 16:55
大大请教我读程序结构吧,我看的 跳转 判断 继承方面有点迷糊 还有对象方法 也有点晕 ...

那是属于Ruby的部分,跟这个教程无关。想要理解RGSS,就得先把Ruby学好。

我在着手写RGSS的教程,也在寻找一份优秀的Ruby入门教程来让大家学习。论坛上找了一圈却没发现。我也没精力再写一份Ruby 教程了。所以我才这么急切的想找 小旅 酱。
作者: tangtang125125    时间: 2013-7-6 12:58
DeathKing 发表于 2013-6-22 19:09
那是属于Ruby的部分,跟这个教程无关。想要理解RGSS,就得先把Ruby学好。

我在着手写RGSS的教程,也在寻 ...

倒是有一些JAVA和C++的基础,学Ruby应该不是很难
作者: tangtang125125    时间: 2013-7-6 13:51
DeathKing 发表于 2013-6-22 19:09
那是属于Ruby的部分,跟这个教程无关。想要理解RGSS,就得先把Ruby学好。

我在着手写RGSS的教程,也在寻 ...

在脚本编辑器里的代码不是RGSS3的代码么?
作者: hejinyihao    时间: 2016-12-18 19:25
很有新意!




欢迎光临 Project1 (https://rpg.blue/) Powered by Discuz! X3.1