Project1

标题: 跟我从头学Ruby (6) [打印本页]

作者: SailCat    时间: 2010-11-27 11:51
标题: 跟我从头学Ruby (6)
本帖最后由 SailCat 于 2010-11-27 15:29 编辑

第六章  因为出众,所以不同

第一节  游戏类的功能
   
    在之前的讲解中,我们从事件脚本开始,逐步扩展游戏所能实现的功能,直到上一讲,我们终于深入了游戏内核脚本的世界,面对着上万行代码的脚本编辑器,迈出了我们自定义个性化游戏功能的第一步。
    然而,仅仅对系统的默认方法做出零星的修改,说起来多少还是有点小家子气的感觉。想要自己做的游戏有吸引力,在系统上和别人的“大路货”有区别,光是用修改系统默认方法的做法,是不行的。你需要了解更多,了解各个游戏类的功能,了解你所关注的游戏类里面各项属性、方法的功能。当然最重要的是了解这一点——你所想要实现的系统功能,需要对哪些游戏类的什么属性和方法作出改动。
    每个游戏类所负责的功能并不复杂,RGSS默认脚本的结构非常好,每个类的实现有极高的专一性。在系统帮助的“脚本入门”之“解读篇”中,对游戏默认脚本的功能做了简单的描述。但是这个描述恐怕太简单了,两三个字的说明可能会让你看得云里雾里。不过没关系,当你打开F11的脚本编辑器窗口之后,把每一段脚本都逐个打开,阅读头几行的注释,你就会对不同游戏类的功能有一个大概的了解。这里,我也简单说一下:
    游戏实时对象类:Game_xxxx,共11个,它们的共同特点是,拥有一个全局的$game_xxxx(同名小写)的实例化变量(少数内部类、公共类没有)。这些游戏对象类,随着游戏的进程不断地发生变化,是推动游戏进行的核心所在。几乎所有的自定义系统都会修改它们。
    游戏精灵类:Sprite_xxxx与Spriteset_xxxx,共5个,它们通常在场景中调用,拥有自己的显示、表现等方法,是游戏界面的组成部分之一。
    游戏窗口类:Window_xxxx,共31个,它们也是在场景中调用,拥有自己的显示、表现等方法,是游戏界面的组成部分之二。
    光标箭头类:Arrow_Base, Arrow_Enemy, Arror_Actor这3个,它们只在战斗中使用,一般的修改也动不到它们头上。
    事件解释器类:Interpreter,只有1个,这个类总管对事件处理的解释功能——怎么说呢,属性就别动了,方法也不用增删了,小打小闹地改点解释方式倒是可以,不过也是高级技巧了。
    游戏场景类:Scene_xxxx,共16个,共享一个全局实例变量$scene,几乎所有的场景表现和用户交互的处理都在这里,是游戏与用户互动的基础。大部分自定义系统会修改它们,不同插件脚本的冲突也一大半来自这里。
    好了,介绍完了。最后那个Main不是一个类,是游戏的主处理进程,去掉注释只有12行代码,也从来没见有人改过这个东西……

第二节  属性的改变

    和前面所说的小敲小打不同的是,对于属性和方法的修改,关系到游戏脚本的结构层面,因此,在介绍下一步的修改内容之前,有必要提一下RGSS脚本编辑器的一些功能:
    Ctrl+X, Ctrl+C, Ctrl+V不解释,文抄公都是这么过来的。
    Ctrl+F是在页面内搜索,Ctrl+Shift+F是全文搜索,当你增加或者删除一些脚本中的方法之后,要多用这个功能,全文通查通改。
    Ctrl+G是行号定位,其他不表。
    首先我们来看一下属性的增添,还是拿Game_Temp这个类来说吧(这个类是所有游戏类中最简单的一个)
    如果要给Game_Temp类增加一个属性,只要依葫芦画瓢,在那一长串attr_accessor之后,再添加一个你自己的attr_accessor :标识符,就可以了,比如
    attr_accessor :time_elapsed   # 本次装载后的游戏执行时间
    定义属性的标记有三种:attr_accessor, attr_reader, attr_writer
    使用attr_accessor定义的属性,为可读可写的属性;
    使用attr_reader定义的属性,为可读不可写的属性(只读属性);
    使用attr_writer定义的属性,为可写不可读的属性(只写属性),罕用。
    有人可能会问,比如我明明在Game_Battler里看到了attr_reader :hp的字样,为什么我在其他地方也看到了actor.hp = xxxx的用法呢?这里解释一下,出现这种情况,是因为在Game_Battler类中额外定义了一个hp=的赋值方法,这个在下面一节的方法里会详细再说。
    如果你要删除原来的某个属性,只要把那行注释掉或删掉就可以了。当然,你需要用Ctrl+Shift+F,把游戏中所有出现这个属性的地方都处理一下,这就是为什么我说,对于现成的系统来讲,删除比增加难得多的原因。我们还是从常见的增加着手。
    当你新增一个属性之后,第一节事情,是找到initialize这个方法,在里面加上对属性值的初始化处理,比如:
    @time_elapsed = 0
    这个做法不是必须的,但如果你不这么做,这个值的初始化就是nil(前面提到过),后面你要用它计算时,可能会有一些问题。
    这里特别需要说明的一点是,除了Game_Temp这种不随存档而保存的属性可以随意增添以外,其他的Game_xxxx类,在增加属性之后,读取原先的存档会有一些问题,主要就是新增的属性全部变成了nil,然后运算时一堆错误。这是因为读档时是不会去执行各个类的initialize方法的。要解决这个问题,一种办法是从头开始玩,呵呵,另一种办法是,在你对脚本做出修改之前,先写一个公共存档事件,触发条件是某个开关自动执行,在游戏中用F9(或事件)打开这个开关,并用它存一个档:
    ◆ 呼叫存档画面
    ◆ 注释:新增属性的初始化处理
    ◆ 脚本:# 你的初始化属性代码
    ◆ 开关:[触发开关]=OFF
    如此这类。然后你去修改属性,并且记得把属性的初始化加在事件脚本中。然后当你改完脚本,回到游戏中的时候,读取这个存档,在读取之后,程序在初始化各个对象后,会继续执行事件中的未完成内容,将这些属性初始化。这个例子,可以参见本章的范例工程,这个工程在Game_Party类中加了一个test属性用于测试。为什么要用公共事件呢?因为这个东西不会随存档保存,就这么简单。如果你定义的并不是属性,而只是成员变量,你也可以先增加这样一个属性,如此操作处理完你的存档之后,再把属性去掉。
    言归正传,当你定义好一个属性之后,你就可以到游戏类的各个地方,用@标识符来操作这个属性了,也可以到游戏类之外的其他地方,用游戏对象(如$game_xxxx).标识符的方法,来操作这个属性的改变。
    但是,多一半的情况下,一个为了系统新功能增加的属性,应该会有自己专属的一些方法。

第三节  定义方法

    方法的定义很简单,只要在class......end这段代码之间,插入一个def......end的程序段,就定义了一个方法。
    什么情况下我需要定义方法?
    情况很多,常见的大概有这么一些:
    一是当你定义了一个新的只读或只写属性,你几乎没有理由不定义一个对应的写(或读)方法。
    二是当你的系统有一长段全新的处理时,你应当把它包装成一个方法,然后在系统默认流程中调用这个方法。
    三是当你定义了一个新的游戏类,所有的方法你都得自己写(好像是废话)。
    还是从属性开始吧,比如你定义了一个attr_reader :cp的只读属性,那么至少你应该义务性地定义这样一个方法
    def cp=(value)
      @cp=value
    end
    当然,如果这个方法只是这么简单的话,那你不如写成attr_accessor :cp算了。有兴趣的人,不妨仔细看看Game_Battler这个类中对于HP和SP的读写处理,非常典型。顺便体会一下,当使用只读属性,并定义了相应的写方法时,在本类中调用self.hp=xxx和@hp=xxx有什么区别——后一种是仅操作成员变量,前一种是调用方法,包含更多的逻辑处理。
    第二种情况,在本章的实例中,会有说明,此处就不表了。而最后一种情况,算是相当高级的技巧了,在以后的章节中,会有详细的涉及。敬请期待。

第四节  让阿尔西斯更出众

    又到了用实例说话的阶段。那我们就再拿可怜的阿尔西斯说事吧。这人是RPGXP默认的游戏主角,即然是主角,多少该有点主角的样子吧。下面我们不妨按照一个英雄的标准来重新打造一下阿尔西斯:
    1. 圣炎:“十字斩”对不死系怪物能造成额外2倍的伤害。
    2. 吸精:每杀死一个敌人可以回复50点SP。
    3. 浴血:每累计杀死十个敌人可以在战斗结束时额外获得100点经验。
    4. 后勤:在地图上每走行1步可以回复1点HP。
    好了,然后我们来思考一下,实现这些功能需要对哪些类的属性和方法作出修改。
    1. 战斗中即时生效的效果,考虑修改Game_Battler类;
    2. 一种方法修改同1,但杀死敌人有普攻、技能、道具三种方法,也就是要修改三处,还有一种方法就是在Scene_Battle控制角色行动那里修改,因为角色行动了才能杀死敌人;
    3. 注意系统默认不统计每个人的杀敌数目,因此需要新增属性。本条是在战斗结束时获得,参考上一章的“双倍金钱”,应该是修改Scene_Battle类;
    4. 在地图上的效果,关系步数,应该在Game_Player类中修改,并且需要新增方法(本条当然也可以用公共事件实现,但是有诸多不便之处)。
    然后我们就进入RGSS的世界,来实现这些效果:
    1. 定位到Game_Battler 3这段,找到应用特技效果的方法skill_effect。在第157行“第二命中判定”之前,插入对阿尔西斯的特殊关照:
      # 如果技能是十字斩,使用者是阿尔西斯,受术者是不死系
      if skill.id==57 and user.is_a?(Game_Actor) and user.id==1 and self.element_rate(9)>100
         self.damage += self.damage * 2 # 额外2倍的伤害
      end
    怎么样,很简单吧。其实这个系统并不复杂,然而,要写出这样的代码,你必须充分的了解每一个类的各项属性和它们的作用。例如,判定十字斩和阿尔西斯,都用到了id属性,因为这个属性是从游戏数据库中直接读取,一般不会改变,即使你在游戏中把阿尔西斯改名成了阿妮茜丝(呃,变性了么……)或者把他转职成了牧师,他的id依然是1。而判定敌人是什么系怪物,用element_rate这个方法再精准不过。
    2. 在Scene_Battle 4这段,找到角色行动效果的方法update_phase4_step2,在行动完成之后(第173行之前),照例关注一下我们的阿尔西斯:
      # 如果当前行动的人是阿尔西斯
      if @active_battler.is_a?(Game_Actor) and @active_battler.id==1
        # 循环所有目标敌人,每挂掉一个,加50SP
        for t in @target_battlers
          if t.hp==0
            @active_battler.sp += 50
          end
        end
      end  
    好像也没什么难的,恩。
    3. 这里需要先定义杀敌数,如果只需要统计阿尔西斯一个人的,可以用变量,当然也可以用属性,属性可以用于多个角色,将来你哪天突发奇想要看看特萝西杀了多少人,也可以用程序记录一下。我们先在Game_Actor类中增加一个普通属性:
      attr_accessor :kills  #角色的杀敌数
      然后在initialize方法中加上初始化:
      @kills = 0
      然后是累计杀敌数,在第2条中修改那一段的@active_battle.sp += 50后面加上:
      @active_battler.kills += 1
      最后是给奖励,轻车熟路了,Scene_Battle 2第173行之前:
        # 阿尔西斯的经验奖励
        if actor.id == 1 and actor.kills >= 10
          actor.exp += 100
          actor.kills -= 10
        end
      搞定。
    4. 我们先找到在地图上行走增加步数的处理,这个是在Game_Player的57-72行。但是Game_Player中并不包含具体角色的HP,SP等数值,怎么办呢?大家知道,中毒后在地图上行走会掉血,这个是怎么处理的呢?看一下这一行代码就明白了:
      $game_party.check_map_slip_damage
      这就是在Game_Player的处理中,通过全局实例化对象的方式,调用了另一个类的方法,我们也可以如法炮制。
      首先是在Game_Party类中给阿尔西斯开开小灶:
      def move_hp_up
        # 循环队伍中所有角色
        for actor in @actors
          # 是阿尔西斯的话,加1点HP
          if actor.id == 1
            actor.hp += 1
          end
        end
      end
      然后在Game_Player中调用这个方法,把增加步数的方法改成下面这个样子
      def increase_steps
        super
        # 不是强制移动路线的场合
        unless @move_route_forcing
          # 增加步数
          $game_party.increase_steps
          # 步数是偶数的情况下
          if $game_party.steps % 2 == 0
             # 检查连续伤害
            $game_party.check_map_slip_damage
          end
          # 给阿尔西斯移动回血
          $game_party.move_hp_up
        end
      end
      大功告成,现在你可以按F12测试这个游戏的实际效果了。
    这时候,身为队伍中魔法师的西露达不高兴了。凭什么就你阿尔西斯有这么多天赋,你是男主你了不起啊,不行,我是女主,我也要天赋,队伍中的每一个人都要有天赋,比如,我在队中的时候遇敌率下降一半。好吧好吧,要实现这个,就得对游戏中各个类之间的关系有比较清楚的了解,我们也要探寻到面象对象编程的核心思想了。请看下一章:继承与重载。
(暂时无法上传附件,范例工程欠奉)
作者: a694251305    时间: 2010-11-27 15:18
看不懂......愚弟蠢了点......
↓↓↓↓↓↓↓↓
↓↓↓支持↓↓↓
↓↓继续努力↓↓
↑↑↑↑↑↑↑↑
作者: liqunsz    时间: 2010-11-27 19:25
这便是前辈诈尸的新作么?~先抢板凳待编辑~
作者: moy    时间: 2010-11-27 19:36
这个太美了前辈> <
作者: enghao_lim    时间: 2010-11-27 21:15
前辈这篇早点出 ... 我预想的第一个游戏应该就坑不了了 ...
当初就是栽在 def cp=(n) 这东西手上 ...
还有,顺便问下 ...
attr(:test) 与attr_accessor :test有何不同?感觉类似 ... 可是实际测试时却很不同 ...
作者: 苏小脉    时间: 2010-11-28 02:54
回复 enghao_lim 的帖子

attr :test 等同于 attr_reader :test
attr :test true 等同于 attr_accessor :test
作者: 精灵使者    时间: 2010-11-28 14:42
那啥,main脚本里主要的修改有:
更改字体(肯定谁都知道)
更改字号
Font.default_size = 字号
更改初始的位置
$scene = Scene_title.new(这个如果做LOGO的话可以换成LOGO的Scene,然后编辑Logo即可。)
那个初始循环不用说了。
while下面的东东大概是exit时候的动作……
如果你想添些效果就添下吧
  Graphics.transition(20)
这个是不是可以改为其他的渐变或者什么的呢?
剩下的就是抓错部分了(这个地方很多容错脚本都会修改这里)
作者: 滑板天空    时间: 2010-11-28 16:14
虽然说看不懂。不过还是支持一下啊。不学脚本,何来优秀游戏呢?
作者: 俊熙    时间: 2014-8-31 14:59
看不懂怎么办?




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