Project1

标题: RGSS3小探——更智能的选择窗体 [打印本页]

作者: DeathKing    时间: 2012-1-20 20:07
标题: RGSS3小探——更智能的选择窗体
本帖最后由 DeathKing 于 2014-1-28 13:12 编辑

这篇文章因为某种原因被写得又臭又长,你如果不能够忍受这种排版,可以访问:http://ftp.66rpg.com/user/deathking/RGSS3_Exp_Doc_1_BT.pdf本文中的图片(png或vsd档)及参考代码,可以从http://ftp.66rpg.com/user/deathking/RGSS3_Exp_Attach_1.rar 取得




RGSS3小探(一)

——更智能的选择窗体     


1 引言

  和传统的RGSS/RGSS2系统相比,RGSS3在Window脚本组方面并没有做革命性的结构调整,继续沿袭由Window类开始继承的风格。下面给出了不太深的一些继承关系:



  而仔细研究RGSS3,你会发现确实有令人眼前一亮的地方。在浏览RGSS3脚本的过程中,我们发现了set_handler和add_command等有趣的方法来处理与选择项有关的内容。在比较了这些新引入的方法与RGSS/RGSS2的异同后,我们欣喜的发现,RGSS3确实使用了一种更先进且智能的方法。我们将来探讨他们。

2 历史

  让我们回过头来看看以前的RGSS系统是如何处理选择窗体的(Window_Selectable及其子类)。从以往制作者所反馈的问题来看,“如何向菜单中加入新选项,如‘用语辞典’”是一个很热门的话题。为此,我们还需要剖析一下旧版本的RGSS系统相关部分,我们利用RGSS2做示范。

  菜单由Scene_Menu负责处理,而选择项部分则由其实例变量@command_window维护。生成@command_window的代码如下:

代码片段 2.1 RGSS2::Scene_Menu
  1.   #--------------------------------------------------------
  2.   # ● 生成命令窗口
  3.   #--------------------------------------------------------
  4.   def create_command_window
  5.     s1 = Vocab::item
  6.     s2 = Vocab::skill
  7.     s3 = Vocab::equip
  8.     s4 = Vocab::status
  9.     s5 = Vocab::save
  10.     s6 = Vocab::game_end
  11.     @command_window = Window_Command.new(160, [s1, s2, s3, s4, s5, s6])
  12.     @command_window.index = @menu_index
  13.     if $game_party.members.size == 0          # 如果队伍为空
  14.       @command_window.draw_item(0, false)     # 无效化物品选项
  15.       @command_window.draw_item(1, false)     # 无效化技能选项
  16.       @command_window.draw_item(2, false)     # 无效化装备选项
  17.       @command_window.draw_item(3, false)     # 无效化状态选项
  18.     end
  19.     if $game_system.save_disabled             # 如果禁止存档
  20.       @command_window.draw_item(4, false)     # 无效化存档选项
  21.     end
  22.   end
复制代码
语句@command_window = Window_Command.new(160, [s1, s2, s3, s4, s5, s6])告知RGSS2生成宽度为160,并且有6个选项的选择项,这六个选择项的文字即为s1~s6的值。当然,这个选择项目前没有实际意义,他只是将外壳给做好了而已。实际投入应用时,还需要做一些相应。

代码片段 2.2 RGSS2::Scene_Menu
  1.   #-----------------------------------------------------------
  2.   # ● 更新命令窗口
  3.   #-----------------------------------------------------------
  4.   def update_command_selection
  5.     if Input.trigger?(Input::B)
  6.       Sound.play_cancel
  7.       $scene = Scene_Map.new
  8.     elsif Input.trigger?(Input::C)
  9.       # 省略了部分代码
  10.       Sound.play_decision
  11.       case @command_window.index
  12.       when 0      # 物品
  13.         $scene = Scene_Item.new
  14.       when 1,2,3  # 技能、装备、状态
  15.         start_actor_selection
  16.       when 4      # 存档
  17.         $scene = Scene_File.new(true, false, false)
  18.       when 5      # 结束游戏
  19.         $scene = Scene_End.new
  20.       end
  21.     end
  22.   end
复制代码
而Window_Selectable定义的一些类容,使得当我们按下↑(上)或↓(下)后矩形选框能够做反应,并且改变一个记录变量的值。上述update_command_selection方法的过程如左图所示,请注意,我们省略了一部分内容:



  因此,当我们添加一个选项后,还需要在负责组织的相应的Scene类中添加相应的响应。令人沮丧的是,这种方法不太智能,一种可能遇到的麻烦就像下面描述的那样:

  当我们添加新选项后,每个选项实际对应的index值会改变,例如,添加后“状态”的index值不再是3,因此,选择“状态”或许会弹出存档界面。并且,我们不是很能够预知这种改变。这样就给一些脚本作者带来了麻烦:他们需要为菜单添加“任务”一项,然而,他们并不知道游戏制作者之前是否也添加类似的选项,而且强行覆盖掉并不是一个很好的方法,我们就这样陷入了两难。

3 革命

  RGSS3决定采用一个更智能化的方法,继承自Window_Selectable的类Window_Command在初始化时,通过调用clear_command_list方法来初始化了一个@list变量。@list变量是一个数组(Array),他按照一定的顺序存放选项。值得说明的是,这些选项是以散列表(Hash)的形式存在的。

  选项的结构如下[  来自脚本注释,Window_Command类,第55~61行 ]:


:name指令名称
:symbol对应的符号
:enabled有效状态标志
:ext任意的扩展数据
    一个合法选项的表述类似与下面的情况:
  1. {:name => "新选项", :symbol => :new_cmd, :enabled => true, :ext => nil}
复制代码
一个可能的@list如下:
  1. @list = [
  2. {:name => "物品", :symbol => :item, :enabled => true, :ext => nil},
  3. {:name => "特技", :symbol => :skill, :enabled => true, :ext => nil},
  4. {:name => "装备", :symbol => :equip, :enabled => true, :ext => nil}
  5.         ]
复制代码
如果要添加一个选项,不推荐直接对@list变量操作,并且,它也未对外公开。取而代之的是add_command方法。想要添加上述的选项,可以这样做:
  1. add_command("新选项", :new_cmd)
复制代码
其他的直接让系统取默认值即可。如果enable的值不为true或者ext的值不为空,你可以这样写:
  1. add_command("新选项", :new_cmd, false, "User Added")
复制代码
我们也需要建立一个响应,但情况与RGSS2大不相同,RGSS3引入了“处理器(Handler)”的概念。调用set_handler来设置对应的选项所响应的操作。Scene_Menu中就有这么一句:@command_window.set_handler(:cancel, method(:return_scene))。

  set_handler并不神秘,但你需要知道Window_Selectable在初始化的时候,也初始化了一个@handler变量用于存放对应选项的相应相应方法。当调用set_handler(:new_cmd, Proc.new{ msgbox_p "Hi" })的时候,@handler变量将会新添加一项:
  1. @handler[:new_cmd] = Proc.new{ msgbox_p "Hi" }
复制代码
当选项:new_cmd被点击后,RGSS3将自动寻找@handler变量中是否有对应的处理方法,如果有的话就调用(Call)它。

  使用@list变量的好处是可以分开地添加选项,这样就使得脚本有更大的灵活性。Window_MenuCommand中定义了make_command_list方法,他按照一定的顺序组织选项:

代码片段 3.1 RGSS3:: Window_MenuCommand
  1.   def make_command_list
  2.     add_main_commands                # 添加主选项
  3.     add_formation_command        # 添加队列选项
  4.     add_original_commands        # 添加自定义选项
  5.     add_save_command                        # 添加存档选项
  6.     add_game_end_command        # 添加结束游戏选项
  7.   end
复制代码
上面的数个方法便是分别为@list变量添加内容。而add_original_commands方法对于我们来说更有意义。我们稍后讨论。

4 机理

  RGSS3采用了一个更复杂的调用关系,但可以确保绝对的抽象。我们简化了一下这个调用过程,制成了下面所示的图像。从图示可以看出,参与内容更新的都不是直接由Scene_Menu和Window_MenuCommand定义的方法,这种高度抽象有着不可言说的好处。

  起点是Scene_Menu,从Scene_Base继承而来的update方法一直在被调用,因此可以不断更新。update调用了update_basic,这是希望自定义脚本不要变动一些东西。update_basic中又调用了update_all_window来刷新Scene中的窗口。此例中,调用了Window_MenuCommand的update方法来刷新选项窗体。此update方法由Window_Selectable类定义,Window_MenuCommand只是继承而来。process_cursor_move处理光标的移动,process_handling处理按键的响应,process_ok调用按下确定键的动作。



  call_handler(current_symbol)即调用由set_handler建立的响应关系。

  RGSS3是在按下确定键的瞬间开始检索,current_symbol指明了当前选择项的:symbol值(参考第3小节给出的参考),如果使用了set_handler为当前选项制定相应方式,那么就启用他。RGSS和RGSS2更关心选择项的顺序(Order),而不是选择项的意义。因此容易出错。

5 实战

  我们分别以两个独立脚本作者的角度,制作分别制作“任务”和“世界地图”脚本,并且,要向菜单选项中添加这两个选项。首先,我们制作“任务”。考虑到“最简化我们的工作”,我们不讨论如何去制作任务系统,而是讨论这个独立系统如何融入制作者的系统。

  菜单画面由Window_MenuCommand负责,因此需要覆盖一下。至此,我们遇到了先前提及的add_original_commands方法,这个是官方留好的接口(Interface),当我们添加自定义选项时,要合理使用此方法:

代码片段 5.1 RGSS3::Window_MenuCommand 自定义
  1. class Window_MenuCommand < Window_Command
  2.   
  3.   alias add_original_commands_task add_original_commands
  4.   
  5.   def add_original_commands
  6.     add_command("任务", :task, true,"User Added" # 我们故意设置了:ext的值来说明这是我们人为添加的
  7.   end
  8.   
  9. end
复制代码
因为我们不得不修改add_original_commands方法,而我们不希望破坏add_original_commands的内容。因此,我们使用关键字alias创建了一份add_original_commands的拷贝。并在重新定义的add_original_commands方法中调用重定义前的方法,然后才是我们新定义的内容。这种委曲求全的方式使得脚本有着很高的可扩展性。



  相应的内容在Scene_Menu中:

代码片段 5.2 RGSS3::Scene_Menu 自定义
  1. class Scene_Menu < Scene_MenuBase
  2.   
  3.   alias create_command_window_task create_command_window
  4.   
  5.   def create_command_window
  6.     create_command_window_task
  7.     @command_window.set_handler(:task, method(:command_task))
  8.   end
  9.   
  10.   def command_task
  11.     msgbox_p "A Interface for Task."
  12.     #SceneManager.call(Scene_Task)
  13.     @command_window.activate
  14.   end
  15. end
复制代码
用同样的技巧处理create_command_window方法,但我们需要首先调用create_command_window_task,因为@command_window是由该方法创建的。我们追加的语句@command_window.set_handler(:task, method(:command_task))是为“任务选项”添加一个响应,当点击“任务”选项后,便激活此响应。

  这个相应存放的是类似于闭包的东西,你不需要理解这些,只需要了解生成这些东西的通常做法。RGSS3的标准做法是先把这个响应定义为一个方法,如我们定义的command_task。然后在set_handler的时候使用method方法将这个方法变成一个闭包(不要忘记有冒号),虽然听起来有点绕,但却是是这样。

  像上述处理后,当我们点击“任务”,则会按照command_task的定义行事。

  值得注意的一点,如果最后调用的是场景切换(示范中给处理掉了),如SceneManager.call(Scene_Task),就不需要再做额外的处理,但其他情况则最好加上一个@command_window.activate来激活选项框。另外,指定的别名最好能区别与一般的别名,避免发生Stack too deep[ 这是由于不合理的递归引起的,合理的使用别名,就不会出现此类错误。]的错误(比如可以以具体的功能为后缀,如_task)。

  “世界地图”功能亦可如法炮制。而我们的最后结果如下:



  并且,我们编制的脚本有很好的兼容性,任意的增删任何一个脚本都不会影响其他的脚本。

6 总结

  从对RGSS3脚本的分析来看,add_command和set_handler等新增的方法的确很好用,这得益于新的编程思路,并且,这些新技巧使得脚本制作者更能写出用户友好型的脚本。这里,强烈建议RGSS3脚本制作者关注以下几个方面:


  RGSS3确有很多革新,这些革新是有意义的,他使得我们能够做出更成熟的系统。我也希望大家能加入到这个探索中来,让RGSS3的魅力早日展现在我们面前!



作者: 阿尔西斯的马甲    时间: 2012-1-20 20:30
又臭又长么?没觉得。。。
不过我倒没觉得这方法有什么方便(虽然兼容性明显提升)。
不过由于这是RGSS3中唯一的添加新菜单项的方法,还是顶一下。
作者: 仲秋启明    时间: 2012-1-20 20:34
本帖最后由 仲秋启明 于 2012-1-20 22:57 编辑

虽然刚开始是很蛋疼,但是慢慢习惯吧

PS:如果在同一个Scene里使用同一个Window的话如
  1.     @1_window = Window_Command.new
  2.     @1_window.set_handler(:1,      method(:command_1))
  3.     @1_window.set_handler(:2,      method(:command_2))
  4.     @2_window = Window_Command.new
  5.     @2_window.set_handler(:3,      method(:command_3))
  6.     @2_window.set_handler(:4,      method(:command_4))
复制代码
就会出现两个窗口全都出现四个选项且第一个窗口只有前两个有效,第二个窗口只有后两个有效的问题
作者: DeathKing    时间: 2012-1-20 21:00
阿尔西斯的马甲 发表于 2012-1-20 20:30
又臭又长么?没觉得。。。
不过我倒没觉得这方法有什么方便(虽然兼容性明显提升)。
不过由于这是RGSS3中 ...

个人觉得尤其实在插入新的选项这方面,这种做法尤为有效。

@仲秋启明手头没有ACE,无法测试。不应该吧……
作者: 垨護Satr婞諨    时间: 2012-1-20 21:53
不懂啊~~~·
作者: feizhaodan    时间: 2012-1-20 23:05
学习了。第一次看Ace脚本的时候真的懵了。
作者: yangff    时间: 2012-1-20 23:17
RGSS3把整个代码梳理了一遍,提供了底层=>应用层的表达,把核心代码抽象化,简单的说就是更像Java写出来的,就是这样。事实上Ruby在抽象化的表达方面没有优势……尽管Ruby的元编程确实被很多人推崇,但是这货说是在的……完全无力吐槽啊混蛋!简单的说就是
RGSS=〉普通程序猿
RGSS2 =〉文艺程序猿
RGSS3=〉2B程序猿
……

作者: DeathKing    时间: 2012-1-21 10:38
Shy07 发表于 2012-1-20 22:05
RGSS强调的是Ruby的好用,即使代码不够严谨也能完成理想中的效果,这是Ruby追求的对程序员友好
RGSS2强调的 ...

我觉得RGSS3一定程度上沿袭了RGSS2的特点,更加强调代码的复用,就是感觉Window类的规划不太合理,继承有点复杂。昨天花了2个小时用Visio做了一个继承关系图,多久发布上来。
作者: 各种压力的猫君    时间: 2012-1-22 00:19
第一次看Ace的新结构真心晕了,适应了之后感觉的确较VX优秀。
没想到能有人进行系统的讲解,前辈的帖子总能让人受益匪浅,佩服佩服啊。
作者: 琪露诺    时间: 2012-1-22 08:25
本帖最后由 琪露诺 于 2012-1-22 08:26 编辑

很喜欢这个结构啦~
觉得直接用method(:command)做参数比when的那个反而更直接?
(据说写RGSS3的程序员换了?)
作者: 忧雪の伤    时间: 2012-1-22 12:09
本帖最后由 忧雪の伤 于 2012-1-22 12:09 编辑
琪露诺 发表于 2012-1-22 08:25
很喜欢这个结构啦~
觉得直接用method(:command)做参数比when的那个反而更直接?
(据说写RGSS3的程序员换了 ...


喵,
就是看了 RGSS3 才意识到以前用 eval 简直弱爆了。
作者: orzfly    时间: 2012-1-22 22:21
忧雪の伤 发表于 2012-1-22 12:09
喵,
就是看了 RGSS3 才意识到以前用 eval 简直弱爆了。

ACE里的Interpreter
  1.   #--------------------------------------------------------------------------
  2.   # ● 执行事件指令
  3.   #--------------------------------------------------------------------------
  4.   def execute_command
  5.     command = @list[@index]
  6.     @params = command.parameters
  7.     @indent = command.indent
  8.     method_name = "command_#{command.code}"
  9.     send(method_name) if respond_to?(method_name)
  10.   end
复制代码
喵~send(method_name) if respond_to?(method_name)
作者: DeathKing    时间: 2012-1-24 19:23
仲秋启明 发表于 2012-1-20 20:34
虽然刚开始是很蛋疼,但是慢慢习惯吧

PS:如果在同一个Scene里使用同一个Window的话如就会出现两个窗口全 ...

不好意思,之前我没有搞清楚你的意思……

实际上是这样的,set_handler是为选项绑定需要执行的Proc(应该是这东西,或与之类似),而add_command才是添加命令。既然@1和@2都是Window_Command的子类,那么他们当然有相同的按钮。(如果想额外添加的话需要自己重新定义一下刷新函数,否则还是不会显示)。

而至于无效的问题,也很简单:你的代码只告诉了@1要怎么处理:1和:2两项, @2的:3、:4两项,当然剩余的选项不会有作用。


另外,谢谢大家的支持!这几天断网在家,只能够研究脚本,确有点个人感悟,此节是RGSS3小探的第一节,而第二节和第三节正在撰写中。

第二节讨论了脚本结构的改变,RGSS3的Window类更加的复杂、更加的多,继承关系也更加复杂。RGSS3也引入了DataManager等可复用模块。

第三节重点关注脚本如何解析由工具制定的类容,如Game_Interpreter的机理,我也欣喜的发现RGSS3使用了Ruby 1.9引入的新技术——纤程(Fiber)。

待我整理好后发布,与各位交流。

最后,祝大家新年快乐!

@orzfly谢谢指出,一时疏忽;
@R-零 @退屈£无聊 @忧雪の伤 @各种压力的猫君 @fux2
作者: 忧雪の伤    时间: 2012-1-24 19:38
本帖最后由 忧雪の伤 于 2012-1-24 19:38 编辑

在后面直接加 _old 的别名真的大丈夫么,大丈夫么。
作者: R-零    时间: 2012-1-25 11:26
DeathKing 发表于 2012-1-24 19:23
不好意思,之前我没有搞清楚你的意思……

实际上是这样的,set_handler是为选项绑定需要执行的Proc(应 ...

召唤吾有何事?

RGSS3?……其实我什么都不知道的啦- - b

话说纤程是甚?
作者: z12508186    时间: 2012-2-4 12:01
好东西。。收藏了楼群主。。不过话说回来。。链接不可用了。。。
作者: 雪流星    时间: 2012-2-4 12:44
最近撰寫偷竊腳本的時候稍微瀏覽了一下 RGSS3
整個戰鬥流程被分解在好幾個類裡面 = =!
還多了一個 BattleManager 模組在處理

不過感覺起來可擴充性是增加了很多

當初看到 add_command 的時候幾乎跳起來歡呼
不過真的用起來卻不是那麼容易
作者: eve592370698    时间: 2012-2-19 21:50
仲秋启明 发表于 2012-1-20 20:34
虽然刚开始是很蛋疼,但是慢慢习惯吧

PS:如果在同一个Scene里使用同一个Window的话如就会出现两个窗口全 ...

选择项独立成为一个类Window_ChoiceList
实际上也独立成一个类似于http://rpg.blue/thread-158161-1-1.html
的窗口了。不过通过移植FUKI对话框我发现,VA的智能化程度确实很让我佩服。显示文章能只能话分页显示,而选择项上自动调整窗口。
不过XP和旧VX那个不少过2行文章同时显示选择项的功能VA给去掉了。
个人感觉XP向VA移植似乎比VX向VA移植更容易(没准纯粹是个人原因)。
作者: lixiao888    时间: 2012-2-24 16:32
顶下技术帝。。。
作者: eve592370698    时间: 2012-2-27 22:19
orzfly 发表于 2012-1-22 22:21
ACE里的Interpreter喵~send(method_name) if respond_to?(method_name)

纤程请看百度百科.这里的fibermain和xp的update似乎相似.表示Fuki对话框三分之二的功能移植算比较顺利,剩下三分之一的功能看来希望渺茫.我很看好智能分次显示,准备伸手求助程序修改,让事件显示文章开启批量文章输入功能后文章在Rm上强制四行分割的功能屏蔽掉.
作者: 步兵中尉    时间: 2012-2-28 00:51
感谢大侠的指点,难怪以前的思路改写经常碰壁。
不过这下子岂不更麻烦了?个人认为应该用更成熟的脚本系统
作者: eve592370698    时间: 2013-1-25 09:01
标题: 说真的,我只看懂了大概思路。
本帖最后由 eve592370698 于 2013-1-25 12:37 编辑

@某菜单 = Window_Command.new(0 , 0 )
然后还要
@某菜单.add_command(“选项一”)
@某菜单.add_command(“选项二”)
是不是就可以生成菜单?
说真的,具体方法没有实例和详细注解我还是不会写。
:symbol        对应的符号
:enabled        有效状态标志
:ext        任意的扩展数据
这三个变量因为之前的RM很多人并不会使用到,所以对它到底是方法、变量还是什么?如果是变量,那属于文本变量还是数字变量?这都不清楚。




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