查看: 19556|回复: 71

[原创发布] 【教程】【不定期更新】实用型XP脚本编写小技巧#13

[复制链接]
梦石
0
星屑
9557
在线时间
5074 小时
注册时间
2013-6-21
回帖
3459

开拓者贵宾剧作品鉴家

发表于 2015-1-17 12:55:15 | 显示全部楼层 |阅读模式

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

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

×
本帖最后由 RyanBern 于 2017-1-5 20:45 编辑

这个帖子记录了我在编写RMXP脚本时候学会的若干技巧,在编写脚本的时候我会把一些有价值的想法记录在这个帖子中,此帖会不定期更新。
此帖涉及到的技巧多实用性,没有特别高深的技术,应该很容易就能学会。不过,具体问题具体分析,这个教程里面的脚本一般不能直接拿过来实用,也只是提供了一个处理该类问题的思路,所以请不要问“这脚本怎么不能实现XX效果”这样的问题。
另外请大家多多提出批评意见。


技巧1:数据库内容扩展
【更新】2015.01.17
【摘要】默认的数据库提供的功能大概不能满足一些人的需要,如果你想要给一个特技定义“冷却时间”的属性,但是数据库并没有相应的设置区域,这时候该如何处理?是打开脚本编辑器直接设置固定值,还是使用外部文件存储这一设定?原则上说,这个本来是数据库的设定,只是数据库的功能不够而已,因此我们希望还可以在数据库中设置自己的自定义属性,而不是把数据编辑转移到脚本编辑器中。以下这个方法经过测试,稳定性较好,并且能满足多数需求。
【正文】
点击展开/收起
大家可能会说类似的问题已经被解决了,如果我没猜错的话,普遍应该是这样的方法:
[pre lang="Ruby"]module RPG
  class Skill
    def name
      texts = @name.split(/,/)
      return texts[0] == nil ? "" : texts[0]
    end
    def cd
      return @name.split(/,/)[1].to_i
    end
  end
end[/pre]
上面的脚本是给技能附加了“冷却时间”的属性,在设置的时候,用逗号分隔特技名称和数字即可实现。例如把[十字斩]技能命名为[十字斩,2],这就表示十字斩技能有2个回合的冷却时间。这个方法一直在用,效果也不错。
但是,这个方法最明显的问题就是多个脚本之间互不兼容。例如还有以下的脚本:
[pre lang="Ruby"]module RPG
  class Skill
    def name
      text = @name.split(/@/)
      return texts[0] == nil ? "" : texts[0]
    end
    def color
      return @name.split(/@/)[1].to_i
    end
  end
end[/pre]
上面的脚本给技能附加了“文字颜色”的属性,即在描绘该技能的时候使用特殊的颜色。在设置的时候,用'@'符号分隔特技名称和数字即可实现。例如把[十字斩]技能命名为[十字斩@1],这就表示描绘十字斩技能的时候使用1号颜色。
假如两个脚本放在一起,会发生什么事情呢?无论怎么放置,至少一个脚本会失效。因为上面两个脚本很可能来自两个人,他们写的时候认为只有他们自己才对@name变量动了手脚,而两个人的脚本放在一起,就会发生冲突。
为了解决这个问题,我想出了这样的一个方法:我们放弃split对字符串的处理,改为使用正则表达式。我们也不再对@name做手脚,而是转移到说明@description上面去(不要再吐槽XP没有备注了)。这样在编写的时候,只要用的是同样的编写模式,脚本之间基本就不会冲突。
[pre lang="Ruby"]module RPG
  class Skill
    Regex_Cd = /%cd\[(\d+)\]/
    unless method_defined? :rb_description_20150117_01
      alias rb_description_20150117_01 description
      def description
        return rb_description_20150117_01.gsub(Regex_Cd, "")
      end
    end
    def cd
      Regex_Cd =~ @description
      return $1.to_i
    end
  end
end[/pre]
[pre lang="Ruby"]module RPG
  class Skill
    Regex_Color = /%color\[(\d+)\]/
    unless method_defined? :rb_description_20150117_02
      alias rb_description_20150117_02 description
      def description
        return rb_description_20150117_02.gsub(Regex_Color, "")
      end
    end
    def color
      Regex_Color =~ @description
      return $1.to_i
    end
  end
end[/pre]
这样,两个脚本各取所需,把自己需要的部分拿出来,用完之后再返回一个“较为干净”的字符串。如果使用正确,两个脚本可以同时工作。使用的时候,在特技说明里面随便哪个地方写'%cd[2]',就表示该技能的冷却回合为2,在说明中随便哪个地方写'%color[1]',就表示描绘该技能使用1号颜色。
使用该方法还需要注意几个问题:
1.alias之前使用了一个unless卫语句,如果嫌麻烦可以不写。不过要保证alias的使用要得当,几个脚本对description定义的别名不能够一样,否则会发生错误。这点比较容易实现,你可以自己形成一个你自己的alias风格,这样别人的基本就不会和你相同。我的风格是把原来的方法前面加上前缀"rb_",后面加上写这个脚本的日期"_20150117"。由于自己在一天之内写的脚本数量比较少,所以这么做就可以避免这个问题。
2.永远不要更改原始数据,而是根据原始数据计算出应该出现的结果,然后把它返回出来。例如在这里我没有更改@description里面的任何信息。
3.使用的正则表达式的风格最好一致,例如我这里是使用了%+英文名+[参数]形式,大家可以开发出自己的风格来。



技巧2:额外信息的存储
【更新】2015.01.17
【摘要】在编写一些XP的脚本系统中,不可避免要涉及到数据的存储问题,特别是用户存档数据的存储。这类问题如果处理不好,那么写出来的脚本的兼容性会很差,而且修改也十分麻烦。这个方法利用默认脚本的一些结构,可以被用作比较简单的额外信息存储,并且实现起来也不是非常麻烦。
【正文】
点击展开/收起
既然是用户存档的存储,存储的额外信息就一定要设置在能被写入的对象中,千万不要设置在$game_temp这种处理临时信息而不会保存的对象里面。通过检查Scene_Save类,我们可以看到到底存档文件里面存储了哪些对象。因此,我们只能把要存的额外信息附加到这几个对象中的某一个。
注意,不推荐新建一个独立的全局对象(和$game_system, $game_party等在一个层次上),然后把这个对象写入到文件当中。因为这样做会让测试中其他的存档失效(发生EOF错误),以及造成不同版本之间的存档不兼容(例如你发布了游戏第一版给你的玩家玩,然后你更新了第二版,这个版本的存档比第一版多一个对象,可是玩第一版的人已经玩到了一半,更新第二版就会导致之前的存档全部失效)。
通常来说,用于附加额外信息的对象有下面几个:
$game_actors
$game_party
$game_system
$game_switches
$game_self_switches
$game_variables
对于要附加的信息,我们必须要弄清楚它和原有信息的关系,才能确定把它放在哪个地方比较好。例如:
角色的CP:放在$game_actors
扩展背包:放在$game_party
当前任务列表:放在$game_party
无法界定类别的可以考虑放在$game_system里面或者$game_self_switches里面,不推荐放在$game_switches或$game_variables里面。这是因为$game_switches和$game_variables里面都是类似于数组的结构,而且在事件编辑器中会用到,除非你处理的额外信息是布尔值或者整数,可以考虑这两个地方,否则不要在上面存东西。我记得曾经有人说$game_variables是个好东西,可以存任何自定义信息,这话倒是没错,不过这样做的话给其他部分带来的麻烦还真是不小。
光说没有例子是不行的,我们可以看看这个多队伍系统:
[pre lang="Ruby"]class Game_System
  def swap_party(party_id)
    @reserved_parties ||= {}
    party = @reserved_parties[party_id]
    return if party == nil
    @reserved_parties[party_id] = nil
    @reserved_parties[$game_party.id] = $game_party
    $game_party = party
    $game_player.refresh
    $game_party.refresh
    $game_map.need_refresh = true
  end
  def add_party(party_id)
    @reserved_parties ||= {}
    party = Game_Party.new
    party.id = party_id
    @reserved_parties[party_id] = party
  end
  def remove_party(party_id)
    @reserved_parties ||= {}
    @reserved_parties[party_id] = nil
  end
  def merge_party(party_id)
    @reserved_parties ||= {}
    party = @reserved_parties[party_id]
    return if party == nil
    $game_party.gain_gold(party.gold)
    $game_party.actors |= party.actors
    (1...$data_items.size).each{|i| $game_party.gain_item(i, party.item_number(i))}
    (1...$data_weapon.size).each{|i| $game_party.gain_weapon(i, party.weapon_number(i))}
    (1...$data_armors.size).each{|i| $game_party.gain_armor(i, party.armor_number(i))}
    @reserved_parties[party_id] = nil
    $game_party.refresh
  end
end

class Game_Party
  attr_accessor :actors
  attr_writer   :id
  def id
    @id = 0 if @id.nil?
    return @id
  end
end[/pre]
这个系统是我在XP提问区的一个回帖,当时做的也比较简单,但是功能上已经足够。可以看到,额外的队伍由于和$game_party是并列关系,而且有多个,所以不能放在$game_party或者是$game_actors中,因此考虑放在$game_system里面。这样再给Game_System定义几个方法就可以自由读取这个自定义的信息了。
以上方法需要注意几个问题:
1.@reserved_parties ||= {}是必要的,这可以防止旧存档没有初始化而产生的异常。
2.多利用方法对实变量进行修改,而不是直接在实变量上操作。例如创建一个新的队伍应该调用方法add_party,而不是直接$game_system.reserved_parties[id]=val(实际上我这个脚本里不允许这样做)
3.为了使用方便,可以更改默认脚本变量访问权限。
4.不要增加一个$reserved_parties之类的东西独立出来,原因同上。



技巧3:让Sprite和Window动起来
【更新】2015.01.17
【摘要】在各种场景中,我们希望我们界面中的元素运动起来,而不是静止地呆在原地。例如在菜单场景中,我们希望命令窗口从屏幕外飞入到场景中,要一个动态的效果。我们自然会想到可以通过更改坐标让它动起来。再比如我们需要一个图片淡出画面,而不是一下子从画面上消失,我们就会想通过更改不透明度来实现这个效果。原理是很简单的,但是怎么样处理才比较妥当呢?下面这个技巧通过定义了一些额外的接口,能够让Sprite和Window随心所欲地移动。
【正文】
点击展开/收起
首先看一下我们常用的两种方法:
[pre lang="Ruby"]@test_window = Window_Base.new(0, 0, 100, 100)
16.times do
  @test_window.x += 8
  @test_window.y += 6
  Graphics.update
end[/pre]
以上脚本的原理很简单,相信也有不少人在用,写起来也方便。Graphics.update原则上1帧调用一次,在调用的间隔里面更改坐标,然后刷新,便可以轻松完成移动的任务。
另一种方法是这样的:
[pre lang="Ruby"]@test_window = Window_Base.new(0, 0, 100, 100)
@waiting = 0
def update
  if @waiting > 0
    @waiting -= 1
    @test_window.x += 8
    @test_window.y += 6
  end
  # do other work
end[/pre]
这个方法的原理也不是很难,用的人也不算少。不过写起来没有上面那种方便。在这里要注意的是update方法实际是Scene类的update方法,因此该update方法位于一个loop do里面,而在此之前用Graphics.update来刷新,也可以达到移动窗口的效果。
但是,以上两种方法不是我这里要说的小技巧。仔细想想就会发现,第一种方法不能用于多个窗口不同步的移动,即使是同步移动,局限性也非常大,因为它私自调用Graphics.update会影响其他动态效果的执行。因此Graphics.update最好只在一个地方执行。第二种方法使用起来太麻烦,移动一个对象就要生成一个变量记录等待时间,变量一多写起来就十分不爽。
下面的方法,结合了以上两种方法的优点,它相当于给每个对象准备一个计时器。各个计时器之间是没有影响的,Graphics.update只有当前场景的main才能调用。
[pre lang="Ruby"]module UI_Effect
  def move_effect(target_x, target_y, frame_waiting)
    return if self.moving?
    @waiting_move = frame_waiting
    @move_count = 0
    @old_x = self.x
    @old_y = self.y
    @target_x = target_x
    @target_y = target_y
    @move_effect = true
  end
  def moving?
    return @move_effect
  end
  def fade_effect(target_opacity, frame_waiting)
    return if self.fading?
    @waiting_fade = frame_waiting
    @fade_count = 0
    @old_opacity = self.opacity
    @target_opacity = target_opacity
    @fade_effect = true
  end
  def fading?
    return @fade_effect
  end
end

class Window_Base < Window
  include UI_Effect
  unless method_defined? :rb_update_20150117
    alias rb_update_20150117 update
    def update
      rb_update_20150117
      if self.moving?
        @move_count += 1
        new_x_f = 1.0 * @move_count * (@target_x - @old_x) / @waiting_move + @old_x
        new_y_f = 1.0 * @move_count * (@target_y - @old_y) / @waiting_move + @old_y
        self.x = Integer(new_x_f)
        self.y = Integer(new_y_f)
        @move_effect = @move_count != @waiting_move
      end
      if self.fading?
        @fade_count += 1
        new_opacity_f = 1.0 * @fade_count *
          (@target_opacity - @old_opacity) / @waiting_fade + @old_opacity
        self.opacity = Integer(new_opacity_f)
        @fade_effect = @fade_count != @waiting_fade
      end
    end
  end
end[/pre]
脚本明显变得好长,但是在这之后,移动窗口将会变得非常容易。
先说说这个技巧的想法吧,我们想通过一个方法来把窗口移动的所有参数信息都传入对象,而把实际移动的过程交给update去完成。在运行过程中,一旦update方法发现了该对象需要进行刷新,就会计算每一步的坐标(在这里采用了一次函数插值方式),并且严格按照等待帧数进行移动。移动完成后,把移动中的标志设置为false以便进行第二次移动。淡入淡出效果同理。
module做好后,直接用include接入到对应的类中,如果是接入到Sprite类中,可以考虑增加一个rotate_effect方法,因为Sprite是能够旋转的。
这样,在使用的时候,只需要输入obj.move_effect(目标X, 目标Y, 持续帧数)即可看到移动效果,当然,由于更改了update方法,在Scene类update方法中对这些窗口对象进行update是必要的。
需要注意的问题以及可扩展的空间:
1.计算新坐标时,为了使得坐标准确,尽量不要使用整数除法,而是要用浮点数除法后再取整为好。最好直接计算坐标然后赋值,而不要采取'+='这样的形式(虽然这样看上去比较直观)
2.判断效果是否结束时,应该用确定的整数变量作比较,因此使用@target_x == self.x && @target_y == self.y的话有可能会出问题(很明显我们是严格通过时间来控制效果的)
3.如果不允许在窗口移动中对界面进行操作,可以利用moving?和fading?两个方法进行判断,因此可以做优化和扩展。
4.可以扩展成按照给定折线进行移动,这里就不多说了。



技巧4:装备附带技能——简洁到了极致
【更新】2015.01.23
【摘要】装备附带技能是一个很久远的问题,要解决它也不是很困难。但是,实现装备附带技能的方法却非常重要。这个技巧实际上没有什么新鲜的东西,在上文中也不断提及。其核心就是“永远不要更改原始数据”。注意到了这一点,我们就能用很短的篇幅来完成装备附带技能的脚本。
【正文】
点击展开/收起
要实现这一效果,最容易想到的办法如下:
当角色穿上装备时,自动学习相关技能。
当角色卸下装备时,自动忘记相关技能。
这种方法实现起来比较简单,但是需要修改的地方较多,如果是做成插件脚本的话,需要复制equip等方法,脚本会变得十分啰嗦,兼容性也不好。
而且此方法有个重大的问题,它无法区分一个技能是武器中习得还是通过其他方式习得。具体来说,如果角色本身已经学会技能1,某武器也附带技能1,那么当卸下武器时,按照以上的思路,技能1会被遗忘。但是技能1是角色通过其他方式习得,从某种程度上来讲不能忘记。这就带来了一点小麻烦。
同样,如果多个装备附带相同的技能,利用以上处理方法,遗忘技能时还需要判断其他装备是否也附加了该技能,如果没有其他装备附加该技能,那么才移除这个技能。
总之,这个思路比较容易想到,但是实现起来太多的细节要处理了。稍微处理不好就会有许多BUG。
分析上面的方法,实际上上面的方法对Game_Actor中的变量@skills做出了更改,这才导致无法区分特技的来源。因此,我们不要修改这个原始数据,而是在调用skill时,返回一个算好的新的数组。
假设,我们已经通过某种方法,使得武器具有#attached_skills属性(这是一个数组)
[pre lang="Ruby"]class Game_Actor
  def skills
    output = []
    output |= @skills
    output |= $data_weapons[@weapon_id].attached_skills if @weapon_id != 0
    output.sort
  end
end[/pre]
这样,我们对某个Game_Actor对象调用skills方法,我们得到的并不是@skills的内容,而是通过一系列计算获得的新内容。
当然,我们还需要对另一个方法做更改,否则武器附带的技能将无法使用。
[pre lang="Ruby"]class Game_Actor
  def skill_learn?(skill_id)
    return skills.include?(skill_id)  # 这里原来是return @skills.include?(skill_id)
  end
end[/pre]
这样,我们不需要再改什么东西,就可以做出武器附带技能的效果了。运用以上方法可以做出更多装备附带技能,状态附带技能的效果,这个留给大家自己扩展吧。
注:以上内容参考自 英顺的马甲 的脚本



技巧5:事件和脚本结合——给地图事件做个计时器
【更新】2015.01.24
【摘要】相信一定有人想利用RMXP做一个类似于农场的游戏,希望达到的最基本效果是让地图上的某些事件随着游戏时间的变化而变化。例如,玩家在某一时刻种下一颗种子,然后游戏时间经过15分钟后,终于长出了果实,这就是一个简易的农场事件。由于这个效果涉及到了地图上的事件,因此在实现的过程中,必须要考虑地图事件的刷新机制。如果完全通过事件实现,制作起来比较麻烦,而且会带来一些效率上的问题。下面这个技巧,将事件和脚本配合使用,利用脚本制作核心的计时器功能,利用事件完成地图上的刷新,比较有参考价值。
【正文】
点击展开/收起
首先我们考虑一下计时器的实现。
在游戏中,涉及到的有关事件都必须要关联一个独立的计时器。但是我们并不是将计时器作为事件的属性直接附加到事件当中,而是把所有的计时器放在一起,统一管理。利用技巧2,我们选择$game_system作为存储计时器的对象。因此,我们就要对Game_System类定义一些方法,以便我们直接使用计时器。
[pre lang="Ruby"]class Game_System
  def current_time
    return Graphics.frame_count / Graphics.frame_rate
  end
  def set_timer(event_id, map_id = nil)
    @event_timers ||= {}
    map_id = $game_map.map_id if map_id.nil?
    key = [map_id, event_id]
    @event_timers[key] = current_time
  end
  def compare_time(event_id, map_id = nil)
    @event_timers ||= {}
    map_id = $game_map.map_id if map_id.nil?
    key = [map_id, event_id]
    return @event_timers[key].nil? ? 0 : @event_timers[key]
  end
end[/pre]
上述脚本的原理非常简单:确定一个事件需要两个参数,地图ID和事件ID,因此,采用一个Hash表来存储不同事件的开始时间。开始计时后,可以随时查看当前游戏时间和计时器开始时间的差值,以便做出刷新等操作。

然后我们考虑地图上事件的实现。
以种种子——收获果实为例,我们可以熟悉刚才脚本的用法。
在种下种子事件发生时,在事件中使用事件脚本,调用$game_system.set_timer(@event_id)即可记录当前种下种子的游戏时间。
随后,给这个事件换一张行走图,打开某独立开关,这是做植物成长的状态。
在成长状态下,需要给事件内容设置判定条件,例如你想要60秒之后收获果实,那么可以如下设置:
条件分歧:脚本:$game_system.compare_time(@event_id) >= 60
       独立开关的操作:B为ON
分歧结束
并且把事件的开始条件设置为:并行处理
独立开关B打开之后,即可设置收获果实的事件了。



技巧6:二周目制作
【更新】2015.02.18
【摘要】二周目的设计在RPG游戏中是个很不错的设定,它增加了游戏的可玩性,适度延长游戏的时间。但是,RMXP默认的系统中不具有二周目/多周目的设定,因此我们要想办法在玩家一周目通关之后,选择“新游戏”进入游戏,就会进入二周目的剧情。一般来说,二周目的信息不会保存在存档中,因此需要借助其他文件来存储这个信息。这个技巧利用游戏原有数据库文件,巧妙地将二周目信息隐藏在其中。为我们制作游戏提供了新的思路。
【正文】
点击展开/收起
我们将有关二周目的信息隐藏在数据库文件中,而不是新生成一个单独的文件。虽然生成单独文件能够比较方便地解决问题,但是需要在脚本上更改的东西略多,因此在这里不加说明。如果利用数据库文件,将二周目信息存入,这样不破坏游戏的整体性,而且读取二周目信息能够更加方便。
具体来说,我们先挑选一个数据库文件,在这里我选择“物品”文件Items.rxdata。
第一步,先在数据库——物品里面预留一个位置(请记住这个位置),并且给这个地方的物品随便输入一个名字。输入名字是必要的,这样在RMXP存储数据库的时候,会把这个位置上的物品初始化。否则,如果什么都不写,那么这个位置上的道具有可能为nil。
1.png
我们就要利用这个位置上的道具,来帮我们存储二周目信息。
第二步,准备一个存储二周目信息的脚本,利用RPG::Item对象里面的属性,来把二周目信息存进去。
[pre lang="Ruby"]module RB
  def self.save_item
    $data_items[33].description = "123"
    filename = "Data/Items.rxdata"
    file = File.open(filename, "wb")
    Marshal.dump($data_items, file)
    file.close
  end
end
[/pre]
在这里,我们最好把这个方法做成模块方法,目的是为了避免命名冲突。
上面这段脚本的作用其实就是在游戏中修改Items.rxdata文件,将第33号物品的说明修改成指定的文字。
准备工作就相当于做完了,这个方法使用起来也是非常简单的。
只需要在游戏一周目通关的事件中,插入事件脚本:
[pre lang="Ruby"]RB.save_item[/pre]
这样,玩家通关一周目这一信息就会被写入数据库文件。
然后就是识别二周目信息,这个也非常简单。一般的游戏在点击“新游戏”后,并不会直接进入主角视角,而是进入前言部分,而通常在这个部分会进行游戏里面的一些初始化。通常情况下,二周目的地图上会出现新的事件,事件的内容可能会改变一些。如果直接使用脚本,在事件出现条件方面会不太方便。因此在游戏初始化的事件里面,最好把这个信息转换成开关或者变量。
3.png
在这里把初始化做成一个公共事件,然后在游戏开始的地图里面直接调用这个公共事件。初始化工作已经完毕,接下来安心完成剧情即可。
不过,如果想要在二周目条件下,游戏的画面或者外观要有变化,那么就要动用脚本了。由于数据库载入的时机比较靠前,所以Items.rxdata里的信息应该可以被其他部分利用上。



技巧7:多项选择
【更新】2015.03.19
【摘要】大家都知道,RMXP的事件指令的第二条就是“显示选择项”,当主角和NPC对话之后,NPC会让主角做个选择。但是这个事件指令的不好的地方就是它只能设置四个选择项,当选择多于这个数目时,就要想各种办法了。例如,通过反复使用“显示选择项”事件指令,牺牲2个选项槽,把这个选项改为“上一页”和“下一页”,然后再考虑多个选项之间的衔接问题。这样做不是很方便,也使得选项过于臃肿。如果单纯利用脚本,那么如何设置选择之后的分歧,也是一个问题。这个技巧运用了脚本事件相结合的办法,能够制作出使用起来比较方便的多项选择系统。
【正文】
点击展开/收起
首先,要想选项数超过4个,最好的办法就是利用默认系统的Window_Selectable。因为它自带翻页功能,而且可扩展性也不错。在这里不推荐用Window_Command,因为Window_Command会根据内容自动改变窗口大小,当选项过多时,窗口就会被拉得特别长,影响美观性。当然,Window_Selectable不能直接使用,我们需要单独写出一个子类出来。有了一定的脚本基础,写出来个窗口应该不是什么困难的事情。
[pre lang="Ruby"]class Window_Choice < Window_Selectable
  def initialize
    super(0, 0, 480, 160)
    self.x = 320 - width / 2
    self.y = 240
    self.opacity = 160
    @data = []
    @esc_mode = 0
    self.visible = false
    self.active = false
    refresh
  end
  def refresh
    if self.contents != nil
      self.contents.dispose
      self.contents = nil
    end
    @item_max = @data.size
    if @item_max > 0
      self.contents = Bitmap.new(width - 32, @item_max * 32)
      @data.each_with_index do |item, i|
        draw_item(item, i)
      end
    end
  end
  def draw_item(item, index)
    x = 4
    y = 32 * index
    self.contents.draw_text(x, y, width - 36, 32, item)
  end
end[/pre]
大家可能看到,我写的脚本都是不带注释的,这是因为这个脚本和默认的Window_Item特别像,使用思路也差不多,所以随便就写出来了。在这里唯一需要注意的就是那个@esc_mode变量(Line 8)。在默认选择项窗口中,应该有一个选项规定了“取消的场合”,即按下Esc键对应的相应行为。我们希望这个多项选择插件也能有相同的功能,所以预留了一个变量用于实现此功能。
另外,选择完毕后,如何设置分歧呢?在默认的选择项窗口中,可以直接在事件编辑器中设定不同选项的分歧,但是事件编辑器可不能为我们写的这个插件也提供一套指令。不过,我们仍然希望选择之后的事情能够在事件编辑器中继续处理,这就需要利用条件分歧。
一个很自然的想法是,先规定一个变量,这个变量用于存储选择之后的结果。然后再利用条件分歧,针对此变量的不同值来进行相应处理。这样,事件的后续处理就会回到事件的编辑器中了。
[pre lang="Ruby"]module RB
end
module RB::Choice
  # 接受选择项编号的变量ID
  Choice_Result_Var = 20
end
class Window_Choice < Window_Selectable
  def start_choice_select(choices, esc_mode = 0)
    @data = choices
    @esc_mode = esc_mode
    refresh
    if @data.size != 0
      self.index = 0
      self.visible = true
      self.active = true
    end
  end
  def end_choice_select
    self.visible = false
    self.active = false
  end
  def update
    super
    if Input.trigger?(Input::B)
      case @esc_mode
      when -1
        $game_system.se_play($data_system.buzzer_se)
      when 0
        $game_system.se_play($data_system.cancel_se)
        $game_variables[RB::Choice::Choice_Result_Var] = 0
        end_choice_select
      else
        $game_system.se_play($data_system.cancel_se)
        $game_variables[RB::Choice::Choice_Result_Var] = @esc_mode
        end_choice_select
      end
      return
    end
    if Input.trigger?(Input::C)
      $game_system.se_play($data_system.decision_se)
      $game_variables[RB::Choice::Choice_Result_Var] = self.index + 1
      end_choice_select
      return
    end
  end
end[/pre]
刚才我们做的Window_Choice只是完成了图形部分,现在我们加上了对各种输入的反应。在这里还是要养成定义写命名空间的习惯,把一个常量放在外面是不好的。别的地方都很简单,没用什么好说的。至于@esc_mode变量,当其值为-1时,Esc键无效;当其值为0时,Esc键对应“分歧”,即额外的选项;当其值为正数时,等同于选择对应位置的选项。
脚本核心的部分完成了,接下来考虑如何接入游戏。
最简单的方法就是使用alias直接接入到Scene_Map中,当然为了调用方便,还可以加个与之等价的简略调用写法。
[pre lang="Ruby"]module RB::Choice
  def self.start(choices, esc_mode = 0)
    if $scene.is_a?(Scene_Map)
      $scene.choice_window.start_choice_select(choices, esc_mode)
    end
  end
end
class Scene_Map
  attr_reader :choice_window
  unless method_defined? :rb_main_20150319
    alias rb_main_20150319 main
    def main
      @choice_window = Window_Choice.new
      rb_main_20150319
      @choice_window.dispose
    end
  end
  unless method_defined? :rb_update_20150319
    alias rb_update_20150319 update
    def update
      if @choice_window.active
        @choice_window.update
        return
      end
      rb_update_20150319
    end
  end
end[/pre]
最后我们看看实际的使用效果,假如我想要做一个5个选项的选择框,需要先使用脚本打开我们的多项选择插件。在选择完毕后,我们利用条件分歧,对选择项进行后续处理。
1.png
在这里需要注意下面几个地方:
1.在说明文字和开启选择项的脚本之间,需要设置“等待”,长度建议为3帧。如果不设置,则说明文字来不及消失,选择项就会出现。
2.在开启选择项脚本和后续条件分歧之间,需要设置“等待3帧”。如果不设置,事件解释器则会把上下指令连起来,导致未选择任何选项就会进入条件分歧的判断。
3.使用脚本打开选择项时,这里采用了一种“奇葩”的方式。利用“设置移动路线”里面的“脚本”命令进行激活,而不是直接使用事件指令的“脚本”。这是因为事件指令的脚本的长度受限制,如果选项过多,事件指令的脚本是写不下的。而移动路线里面的脚本命令,里面能容纳特别长的脚本(这点是我在看某侠的XP教程后发现的),所以,如果有特别长的脚本,可以试着写在移动路线里面的脚本中,不过缺点自然也很明显,那就是这里面的脚本不能换行,给阅读造成了麻烦。



技巧8:椭圆形状的blt
【更新】2015.06.28
【摘要】众所周知,Bitmap类的#blt方法有类似于截图传送的功能,它可以把一张bitmap的部分复制到另一个bitmap当中。这个方法很方便,但是它也有很多的拓展空间。比如说,blt方法只能截取矩形区域,而不能截取其他形状的区域。如果我们对区域有所要求的话,原始的blt就不能满足了。这个技巧为Bitmap类新定义了一个#ellipse_blt方法,它可以将传送源位图的一个椭圆区域截取到目标bitmap上。
【正文】
点击展开/收起
我们先设计一下这个方法的函数头。为了和原来blt的参数一致,新方法ellipse_blt仍然接收4个参数。
def ellipse_blt(x, y, src_bitmap, src_rect)
x                 :传送目标的 x 坐标
y                 :传送目标的 y 坐标
src_bitmap :传送源位图对象
src_rect       :传送源的截取区域(Rect对象)
大家可能看到,虽然我们截取的是椭圆形区域,但是参数却用的是Rect(矩形)对象。按理来说,应该传入一个“椭圆”对象作为参数,但是,一来RGSS没有椭圆的类;二来由于椭圆可以完全嵌入到一个矩形中,利用矩形表示未尝不可。所以我们没有必要再写一个椭圆的类,而是直接利用一个矩形表示椭圆。具体的嵌入形式如下图所示:

Ellipse

Ellipse

图中矩形的左上角为原点(请无视掉原图中的坐标),如果假设矩形的半长轴是 a,半短轴是 b,那么椭圆方程为

Equation

Equation

那么,知道这个方程有什么用呢?因为我们没办法直接绘制椭圆,所以我们采用微积分的办法,利用细长条矩形来逼近椭圆。即每次只绘制高度为1的矩形长条,而绘制的宽度和位置恰恰要利用方程算出。
在这里略去了推导过程,直接写出代码如下:
[pre lang="Ruby"]class Bitmap
  def ellipse_blt(x, y, src_bitmap, src_rect)
    a = 1.0 * src_rect.width / 2
    b = 1.0 * src_rect.height / 2
    src_rect.height.times do |i|
      dx = a - a * (1 - (i / b  - 1) ** 2) ** 0.5
      w = 2 * (a - dx)
      self.blt(x + dx, y + i, src_bitmap, Rect.new(src_rect.x + dx, src_rect.y + i, w, 1))
    end
  end
end[/pre]
接下来我们就可以试试效果了,在下面附上一个做好的图吧!

Effect

Effect




技巧9:地图事件数据的扩展
【更新】2015.06.29
【摘要】技巧9实际是对技巧1的进一步补充,在技巧1中,我们对数据库的功能进行了扩展。但是地图上的事件某种意义上讲也是游戏数据的一部分,那么如何在地图事件上附加额外信息呢?下面就要介绍两种附加信息的方法:利用事件的名字和事件当中的注释。
【正文】
点击展开/收起我们需要知道,在事件中存储额外信息,必须“毫无痕迹”。事件不同于数据库,它和玩家互动的频率非常高。为了避免存储的信息出现在它不该出现的地方上,我们可以利用事件名字进行储存。
事件名字出现在事件编辑器的左上角,可以自行编辑。它的作用仅仅是为了编写事件的方便,而并不是用于区分事件(因为事件ID是区分事件的唯一标识)。我们编写事件名字,可以在类似于[显示动画][设置移动路线]这样的指令中更准确地找到需要操作的事件对象。
要获取事件名字,我们需要知道RGSS1的事件结构。
首先,游戏中,地图上的事件是用Game_Event对象表示的,每个Game_Event对象都会有一个RPG::Event的参照元,事件名称的信息就存储在这个参照元里面。为此我们定义方法:
[pre lang="Ruby"]class Game_Event < Game_Character
  def event_name
    # @event 为 RPG::Event 类的对象
    return @event.name
  end
end[/pre]
这样我们便可以轻松地获得事件名称了。类似地,利用正则表达式我们也可以将存储在事件名里面的信息提取出来。例如:
[pre lang="Ruby"]class Game_Event < Game_Character
  def special_tag
    regex = /%tag\[(\d+)\]/
    return regex =~ self.event_name ? $1.to_i : nil
  end
end[/pre]
上面那段脚本就是提取事件的特殊标签(这个是我自定义的属性),当事件名里面含有'%tag[数字]'的字样时,它就会把里面的数字提取出来。
这种利用事件名称存储信息的方法简便易行,它的特点就是一个信息被整个事件公用,换句话说,这个信息与事件到底在第几页没有关系。
下面说的第二个方法,是利用[注释]的事件指令来存储信息。
为了能够利用[注释]里面的信息,我们必须要弄清楚[注释]存储的方式。由于[注释]本身也是一种事件指令,有关它的内容就应该能在Interpreter类里面找到。既然是事件指令,那么就一定有对应的指令代码(command code)。但是找遍了RGSS1里面Interpreter的7个脚本,也看不到有关[注释]的代码。实际上,由于注释本身并没有什么作用,所以在RGSS1做事件解释器的时候就对事件这个指令不加处理,我们也看不到相关的代码。但是,在RGSS3中,又恢复了对[注释]的处理。在查看RGSS3的脚本之后,我们知道了[注释]的指令代码是108和408。
我们暂且不管为什么注释有两个指令代码,我们只需要按图索骥,把这两个代码代表的指令从事件页里面找出来即可。
[pre lang="Ruby"]module Event_Comments_Fetcher
  def all_comments
    return [] if @list.nil?
    result = []
    @list.each do |command|
      if command.code == 108 || command.code == 408
        command.parameters.each{|line| result << line}
      end
    end
    return result
  end
end[/pre]
上面这一组代码就是把整个事件当前页的所有注释都摘录出来,这是一个基本的代码框架,如果需要特殊要求,也请利用正则表达式的办法摘录。使用的时候,要把具体的模块mix-in到Game_Event类中。
由于[注释]里面能够存储大量文本,所以这种方法也非常不错。而且这种方法的特点是一个信息只被一个事件页单独享用,而同一个事件里面不同事件页里面的信息是不同的。读者在这里要注意体会两种方法的差别。
作为技巧9的应用,将在下面的技巧10中给出。



技巧10:记录事件位置
【更新】2015.07.01
【摘要】RGSS1中,切换地图之后,重新进入该地图会导致事件的位置重置。究其原因,是因为游戏并没有储存各个地图的信息,而是在每次进入地图的时候,重新载入原始的地图数据,这导致事件归位。所以,我们必须要在主角移动之前将地图上的事件位置储存起来,这需要利用我们技巧2。当然,并不是所有事件都需要记录位置,我们利用技巧9来对特殊的事件进行设置,让RGSS系统知道哪些事件需要记住位置,哪些事件不需要。
【正文】
点击展开/收起我们先做一些准备工作。由于不同人的游戏性质不同,有的游戏中,可能大部分事件都需要记录位置,而有的游戏中,只有少数特殊事件需要记录位置。在这里,我们默认多数事件都是不需要记录位置的,只有少部分事件才需要记录位置。但是,事件记录位置与否和地图有很大的关系,所以除了上面的方式以外,我们也可以规定,再哪些地图上需要记录全部的事件位置。这个功能和上面的并不冲突,基于这样的设想,我们可以写出命名空间如下:
[pre lang="Ruby"]module RB
end
module RB::Event_Pos_Memorize
  # 是否记录位置的标志,事件内部名字使用
  Mem_Regex = /@mempos/i
  # 需要记录的地图 ID 数组,只要地图 ID 含在下列当中,该地图的事件位置都会被记录
  Maps_Need_Memorize = []
end[/pre]
接下来,我们要设计如何存储事件的位置。事件位置有三个指标:x坐标,y坐标,朝向。而确定一个事件,只需要知道它的事件 ID 和它所在地图 ID即可。保存事件的位置可以直接利用数组,也可以新设计一个类来存储。这些点的信息将被存储在一个Hash表中,Hash表的主键是数字,表示地图ID。而事件ID,我们也放在事件位置里面记录。我们再将这个Hash表存储在$game_system中,这样便完成了数据的存储。
在这里也顺便完成了设置区域的制作,我们利用事件名来表示是否要记录该事件的位置,例如,事件名字里面含有'@mempos'字样的事件将会被记录。在这里没有用到事件页的注释方法,考虑到同一个事件表示的“实际物体”相类似,所以记录位置的规则也应该是类似的,因而利用事件名字记录比较好。
[pre lang="Ruby"]class Event_Pos
  attr_accessor :id
  attr_accessor :x
  attr_accessor :y
  attr_accessor :d
  def initialize(id, x = 0, y = 0, d = 2)
    @id = id
    @x = x
    @y = y
    @d = d
  end
end
class Game_System
  alias rb_initialize_20150701 initialize
  def initialize
    @event_positions = {}
    rb_initialize_20150701
  end
  def event_positions
    @event_positions ||= {}
    return @event_positions
  end
end
class Game_Event
  def need_memorize
    return RB::Event_Pos_Memorize::Mem_Regex === @event.name
  end
end
[/pre]
最后,我们看看如何在切换地图的时候记录/还原事件的位置。
初步想一下,似乎这个操作应该是在地图切换的时候完成的,切换地图的指令应该是Scene_Map#transfer_player。但是,实际上,我们只需要在地图加载的地方做做手脚就够了。为此,我们需要改写Game_Map#setup方法。下面的代码中添加了注释,请读者仔细阅读。
[pre lang="Ruby"]class Game_Map
  alias rb_setup_20150701 setup
  def setup(map_id)
    # 在重新装载地图之前,此时的地图ID还是移动之前的ID
    # 清除旧信息
    $game_system.event_positions[@map_id] = []
    # 对旧地图的事件进行遍历
    if @events !=nil
      @events.values.each do |event|
        # 如果地图 ID 需要记忆,或者是这个地图的某个事件需要记忆
        if RB::Event_Pos_Memorize::Maps_Need_Memorize.include?(@map_id) || event.need_memorize
          # 记录事件的ID,坐标,朝向
          $game_system.event_positions[@map_id].push(Event_Pos.new(event.id, event.x, event.y, event.direction))
        end
      end
    end
    # 装载新地图
    rb_setup_20150701(map_id)
    # 还原事件位置,此时的@map_id为移动之后的地图ID
    positions = $game_system.event_positions[@map_id]
    return if positions.nil?
    positions.each do |pos|
      event = self.events[pos.id]
      next if event.nil?
      # 如果地图 ID 需要记忆,或者是这个地图的某个事件需要记忆
      if RB::Event_Pos_Memorize::Maps_Need_Memorize.include?(@map_id) || event.need_memorize
        # 还原事件的坐标,朝向
        event.moveto(pos.x, pos.y)
        case pos.d
        when 2
          event.turn_down
        when 4
          event.turn_left
        when 6
          event.turn_right
        when 8
          event.turn_up
        end
      end
    end
  end
end[/pre]
这样,事件位置记忆就基本做完了。只要输入需要记忆的地图ID,或者是在某个事件的名字上写下'@mempos'的字样,对应的事件的位置就会被记忆,下次进入该地图的时候,事件的位置就会保持上一次退出的位置了。
  
技巧11:另类的alias实现
【更新】2015.09.15
【摘要】在自己编写脚本中使用alias关键字,能够节省插件脚本的篇幅,还有减少脚本冲突的可能性的效果。在这个技巧里面,我则要介绍另一种alias的实现方式,利用这个方法,起到的效果和alias差不多,但是完全不用使用alias关键字,而且也不必想方设法给新方法起不同的名字了(从而达到防止Stack Level Too Deep的错误)。 注:本技巧参考了这里的帖子。
【正文】
点击展开/收起回想一下,使用alias关键字对方法的拓展需要保留拓展前方法的完整性,这也是为什么我们推荐使用alias而不去直接重定义某个方法的原因。因此,如果我们能通过某种方法备份原来的方法,然后在原方法的基础上定义新的方法,那么这种方式就能基本代替alias的使用。在Ruby中,万物皆对象,所以一个“类”也不例外。我们注意到这样的语句是合法的:
[pre lang="Ruby"]$gm_temp = Game_Temp.clone[/pre]
上面的意思就是将"Game_Temp"这个对象(它是一个类!)clone一下,放到全局变量中。也就是说,$gm_temp里面包含了Game_Temp全部的方法和类变量。这样做有什么好处呢?这样做其实就相当于把Game_Temp里面的内容备份了一下。所以,这就是我刚才提到的“通过某种方法备份原来的方法”,现在不用说原来的方法了,整个类都让你备份了,当然是没有问题的。
然后,我们要在此方法的基础上,定义新的方法。这就需要继承旧方法的功能,并且添加新方法进去。在这里我特地用了“继承”两个字,这就暗示着我们要构造一个子类来完成这个效果。在Ruby中,一个类不能定义为它本身的子类,但是,由于我们使用clone方法对原有类进行备份,$gm_temp和Game_Temp是两个类(只不过它们里面的东西一样罢了),所以我们对Game_Temp进行重新定义,把它定义为$gm_temp的子类,这样,新定义的Game_Temp就具有了$gm_temp的一切方法,而原来的Game_Temp,可以认为是因重定义而消失了。
[pre lang="Ruby"]class Game_Temp < $gm_temp[/pre]
可是,这有什么用?绕了一圈,我们得到了一个一模一样的类,原有的方法也没有变化。在这里提醒大家,我们注意到,此时Game_Temp已经成为了别的类的一个子类,所以调用父类的方法只需要关键字super。所以,我们可以写下面的代码:
[pre lang="Ruby"]$gm_temp = Game_Temp.clone
class Game_Temp < $gm_temp
  attr_accessor :additional_property_a
  def initialize
    super
    @additional_property_a = 0
  end
end[/pre]
这种写法,和下面的alias写法的效果基本等同:
[pre lang="Ruby"]class Game_Temp
  attr_accessor :additional_property_a
  alias rb_initialize_20150915 initialize
  def initialize
    rb_initialize_20150915
    @additional_property_a = 0
  end
end[/pre]
因此我在摘要中提到,这种方法几乎可以代替alias的用法。
同时,如果在上面的代码基础上,我继续添加下面的代码:
[pre lang="Ruby"]$gm_temp = Game_Temp.clone
class Game_Temp < $gm_temp
  attr_accessor :additional_property_b
  def initialize
    super
    @additional_property_b = nil
  end
end[/pre]
那么这种写法相当于在上面alias的基础上添加下面的代码:
[pre lang="Ruby"]class Game_Temp
  attr_accessor :additional_property_b
  alias rb_initialize_20150915_b initialize
  def initialize
    rb_initialize_20150915_b
    @additional_property_b = nil
  end
end[/pre]
大家请观察两种方式,这里面出现的对比是比较明显的。使用第一种方式,不需要对$gm_temp进行重新命名,而使用第二种方式,必须对alias后面的新方法重新命名,否则必会发生Stack Level Too Deep。
另外,如果在一个类中,需要重新定义很多方法,那么按第一种方式,写完$gm_temp = Game_Temp.new之后,在子类定义里面可直接定义新的方法;而按照第二种使用alias的方式,每定义一个依赖于旧方法的新的方法,都需要写一遍alias。

技巧12:简单的数据库破限
【更新】2016.07.14
【摘要】众所周知 RMXP 对数据库元素的数量基本上都有 999 的上限,那么总有这个数量不够的情况。遇到这种情况我们该怎么办呢?你可能想到 DKRM,但是 DKRM 的资源不是那么好找,你也可能想到用脚本将多余的数据补上去,但是总觉得写起来很麻烦。这里介绍了一种在不改动编辑器的条件下轻松突破这个限制的一种办法,可能以前有人已经做过,不过我还是在这里更新一下吧。
【正文】
点击展开/收起实际上,RMXP 的 999 上限是编辑器的硬性要求,即使这个数字换成别的,编辑器也会识别对应的上限值,并不是说数据库编辑器最多只能读 999 条数据。每次打开一个工程时,RMXP 总会载入当前工程 Data 文件夹下的数据,然后在这个基础上进行编辑。利用这个性质,我们可以对 Data 里面的文件动一些手脚,让编辑器“误以为”我们的某组数据有 1000+ 条的记录。

在改动数据之前,我们有必要了解 RMXP 的内部数据结构。数据库中的每一个标签都存储在 Data 文件夹中的某一个文件中,例如角色数据存储在 Actors.rxdata 中,技能数据存储在 Skills.rxdata 中。每个文件中只有一个数组,数组的 0 号单元为 nil,而其他单元则存储对应 ID 的数据。这个数组的长度就是数据库中的最大值,如果我们能够想办法改变这个长度,那么我们就能突破 999 的限制。

这样的修改并不难,利用几行脚本就可以做到
[pre lang="Ruby"]def set_item_max(label, max)
  filename = sprintf("Data/%ss.rxdata", label)
  data = load_data(filename)
  # 备份原文件
  save_data(data, "#{filename}.bak")
  # 根据原数据大小调整
  dsize = data.size - 1
  if dsize >= max
    data = data[0..max]
  else
    tmp = ((dsize+1)..max).map do |id|
      formula = sprintf("@item = RPG::%s.new", label)
      eval formula
      @item.id = id
      @item
    end
    data = data + tmp
  end
  # 存储新文件
  save_data(data, filename)
end[/pre]
上面代码的作用就是重新设置对应 $data_xxxx 的大小。如果想设置特技的上限为 1200,就在事件脚本里面输入
[pre lang="Ruby"]set_item_max("Skill", 1200)[/pre]
之后关闭游戏,并重新打开这个工程,数据库的上限就变成 1200 了!在数据库中尽情编辑吧!
RPGXP_2016-07-14_14-49-27.png
同样,在其他事件中,和特技有关的选项的上限也都变成 1200 了。
RPGXP_2016-07-14_14-50-22.png
需要注意的是,除非你想将数据库最大值改到 999 以下,否则请不要使用“更改最大值”按钮进行更改,这样超过范围的数据仍然会丢失。如果想再次更改数据,应当再次使用脚本 set_item_max


技巧13:简易召唤系统——我们当中出现了一个叛徒
【更新】2017.01.05
【摘要】这个技巧巧妙利用了敌人出现/隐身的设定,制作了一个简易的召唤系统。并且需要极少量的脚本代码,非常适合事件党学习(以及出成考场题目)。
【正文】
点击展开/收起
现在考虑最简单的一种召唤系统。使用召唤特技之后,队伍中出现了一名特殊的成员。该成员不能被玩家控制,也不能被敌人所攻击到。在战斗过程中,该名召唤的角色会随机释放技能(可以给角色释放增益技能或者攻击敌人)。由于召唤的角色不能被敌人攻击到,所以当可控角色全灭的时候,战斗依然会被判负。

这种召唤系统中,相当于召唤的角色独立于角色队伍和敌人队伍之外,实际上是协助我方的“第三阵营”的角色。但是,我们是否也需要设置这样的一个数据逻辑关系呢?当然不需要!下面我们就可以看到,仅仅使用默认的数据结构,我们就能很容易地做出这个效果。

原则上,召唤的角色应当属于“我方阵营”,似乎应该把它定义为 Game_Actor 更合理一些。然而,熟悉 XP 脚本的醋醋们都知道,想给默认角色队伍扩容是相当不容易的,这并不是我们的锅,而是 XP 默认战斗系统写得太!烂!于是,我们反其道而行之,能不能用一个敌人来表示这个“召唤角色”呢?使用敌人(RPG::Enemy)的好处,是可以设置其战斗图和在画面中的位置,还可以直接在数据库中设置该名敌人的技能以及释放的概率,。而这些技能的处理,我们只要把技能的使用范围反过来就可以达到我们的效果!举例来说,假如召唤的角色拥有一个自动给所有队员加血的技能,那我们只需要按照正常方式设置该技能的数据(威力为负数,MDEF-F设置为0即可),不同的是,这个技能的释放范围是敌人全体。因为这个召唤技能的设定是“敌人”,敌人的敌人那自然就是角色队伍了。

然而,一个重要的问题就是如何让这个召唤的角色成为“特殊的敌人”。即不会被敌人打到,不会被角色打到,不会影响战斗胜负的判定。这个地方看似很麻烦,似乎要改很多东西。不过我们想想,什么样的敌人具有这样的特点呢?那就是不存在的敌人!一个敌人不存在时,它不可被任何角色攻击,也不能进行行动,当然也不会影响胜负的判定。顺着这个思路,我们找到了 XP 战斗系统中两个重要的概念:可行动(movable)和存在(exist),这两个概念定义如下:
[pre lang="Ruby"]
class Game_Battler
  def movable?
    return (not @hidden and restriction < 4)
  end
  def exist?
    return (not @hidden and (@hp > 0 or @immortal))
  end
end
[/pre]
从代码里面我们可以看到,隐藏的(中途出现)敌人不能行动,也不能当作存在。而存在的含义是非中途出现,并且是存活的战斗者。通常来讲,隐藏的敌人会被看做是不存在的,那么我们考虑这样一个有趣的现象:假如一个战斗者不是隐藏的(hidden 是 false),但是是不存在的(exist? 返回 false),那么这个战斗者会具有以下特性
  • 可以正常进行战斗行动,例如释放技能,普通攻击。
  • 无法被敌人或者自己人选中。
  • 群体攻击的技能不会打到这名角色的身上。
  • 不会影响战斗胜负判定。首先,由于我们将召唤的角色设置为RPG::Enemy,所以当我方角色全灭时,即使召唤角色还在,依然会被判负(符合我们的要求);然后,当敌人角色全灭时(召唤角色除外),由于 exist? 返回 false,因此战斗系统会判断敌方角色全部不存在,因此会判定我方胜利。

这些特性恰恰就是我们需要的。我们只要为这个特殊角色设置一下 exist? 的判定即可,代码只有简单的几行
[pre lang="Ruby"]
class Game_Enemy
  Summon_ID = 10
  alias rb_exist_20170102 exist?
  def exist?
    return false if id == Summon_ID
    rb_exist_20170102
  end
end
[/pre]
以上代码只是更改了 exist? 的判定,当敌人是特殊 ID(也就是召唤)时,exist? 将永远返回 false。

最后,就是设定召唤的技能。此部分不需要脚本。

首先,将召唤的角色绑定到一个ID(在我的例子中是10)。然后创建一个敌人队伍,并且首先添加这个特殊的敌人,并将这个敌人的属性设置为中途出现。
RPGXP_2017-01-05_20-27-45.png
然后,设置一个召唤技能,该技能绑定一个公共事件:
RPGXP_2017-01-05_20-29-20.png
这个公共事件只有“敌人出现”这一条有效语句。我们注意到这里固定设置1号敌人(不是敌人ID,而是敌人在队伍中的编号)出现,因此在每个敌人队伍中,我们必须将召唤的角色第一个加入队伍中。

现在所有步骤都已经完成,可以打开 RMXP 测试效果了。
2017-01-02_18-47-17.gif
2017-01-02_18-49-12.gif

最后我们总结一下这种方法的缺点
  • 需要将每个敌人队伍中都设置这样一个特殊的敌人,并且一定要把编号设置为1。这样增加了许多重复的工作。
  • 召唤的角色不能释放复活类型的技能,因为默认的技能范围里,没有“敌人(HP0)”这一选项。
  • 不能在召唤物的战斗图上显示动画(这个在我的 GIF 演示中是可以的,请思考一下如何处理)

点评

接收更新通知。  发表于 2015-9-15 08:03
接收更新提示  发表于 2015-7-18 11:57
显示选项的一个不自然的思路可以见我的事件转译器-_-#  发表于 2015-3-19 12:50
更新时求自顶啊= = 点评接收更新通知  发表于 2015-2-8 21:10

评分

参与人数 8星屑 +525 赞 +2 收起 理由
七夜弦 + 1 精品文章
SixRC + 1 看完了 r7漏洞+1
gonglinyuan + 20 精品文章
余烬之中 + 200
7408 + 15 精品文章
你最珍贵 + 60 赞
kuerlulu + 30 精品文章
taroxd + 200 永远不要更改原始数据+1

查看全部评分

梦石
0
星屑
9292
在线时间
2504 小时
注册时间
2011-5-20
回帖
14521

开拓者

发表于 2015-1-17 13:04:35 | 显示全部楼层
其实用
  1. Module ML end
  2. Module ML::Skillcd
  3. Skill={
  4. 1=>2,#1号技能2回合CD
  5. }
  6. end
复制代码
和
  1. Module ML end
  2. Module ML::Color
  3. Skill={
  4. 10=>4,#10号技能4号颜色
  5. }
  6. end
复制代码
什么的读取的时候用ML::Color::Skill[X]就好了吧?

点评

比如我带1级红杖和5级红杖都是显示带来800伤害能量冲击的介绍0.0  发表于 2015-1-17 13:15
话说要是装备介绍会改变的呢···比如说达贡之神力附带能量冲击,一个角色身上是6格子装备可以带6红杖···红杖的介绍按照身上带的那根最高级的统一   发表于 2015-1-17 13:14
主要是觉得写介绍和名称的话还是不够0.0  发表于 2015-1-17 13:13
我在一开始就说了尽量不要再脚本编辑器里设置  发表于 2015-1-17 13:11
好歹认真看一下摘要吧……  发表于 2015-1-17 13:10
[img]http://service.t.sina.com.cn/widget/qmd/5339802982/c02e16bd/7.png
回复

使用道具 举报

…あたしは天

梦石
0
星屑
2308
在线时间
4033 小时
注册时间
2010-10-4
回帖
10548

开拓者贵宾

发表于 2015-1-17 13:10:02 | 显示全部楼层
本帖最后由 taroxd 于 2015-1-17 15:03 编辑

<del>以下内容均为卖萌扯淡</del>

没记错的话 XP 修改内部脚本的话是需要 F12 guard 的吧

所以不管怎么说,我现在连 alias 关键字都完全不用了

[pre lang="ruby" line="1"]

class Module

  new_name = :alias_method_without_F12_guard
  old_name = :alias_method

  unless method_defined? new_name
    alias_method new_name, old_name
  end

  def alias_method(new_name, old_name)
    unless method_defined? new_name
      alias_method_without_F12_guard new_name, old_name
    end
  end
end[/pre]

---

看到 merge_party 好亲切~ 咳咳

https://rpg.blue/thread-367044-1-1.html

你的代码里也是考虑到了存档里不存在的情况(@reserved_parties ||= {}),不过这个东西实在是不需要每次都写啊- -
甚至根本不需要重定义 initialize。可以直接定义一个(可以私有)的方法 reserved_parties。

class Game_System
  private
  def reserved_parties
    @reserved_parties ||= {}
  end
end

这样之后每次访问都调用 reserved_parties 方法就可以了

点评

如果这样的话initialize确实是多余的。不过||=在一个方法里面执行一次就够了,定义成私有方法的话每次引用都要执行一次(虽然效率影响不大)  发表于 2015-1-17 15:08
其实我是通过AC的回帖找到你这个帖子的,当时想都没想果断用这个当方法名  发表于 2015-1-17 14:25
后面的代码也没必要判断。Game_Party 也不是内部类啦  发表于 2015-1-17 14:18
method_defined? 是接受*符号*参数的方法,不是 alias 那样的关键字啦……(因为是方法所以才更灵活)  发表于 2015-1-17 14:16
第一个已经修改,不过后面的代码为了省事就不再判断了,留给写脚本的人自己控制好了  发表于 2015-1-17 14:14

评分

参与人数 1星屑 +60 收起 理由
RyanBern + 60 来吃糖

查看全部评分

回复

使用道具 举报

故九江太守

梦石
0
星屑
314
在线时间
2158 小时
注册时间
2012-12-5
回帖
4323
发表于 2015-1-18 19:45:06 | 显示全部楼层
希望有个怎么把脚本写得简便短小精悍的教程

点评

元编程  发表于 2015-1-18 21:40
翻 Ruby 文档。没有更好的办法  发表于 2015-1-18 19:54
回复

使用道具 举报

…あたしは天

梦石
0
星屑
2308
在线时间
4033 小时
注册时间
2010-10-4
回帖
10548

开拓者贵宾

发表于 2015-1-18 19:50:04 | 显示全部楼层
本帖最后由 taroxd 于 2015-1-18 19:59 编辑

关于窗口精灵的运动,@余烬之中 写过一个更加通用的:
https://github.com/ShadowMomo/Sm ... ter/Smomo%20Core.rb 中的 transition 方法

我也写过一个不那么通用的(没有上面那个通用,需要手工 update):
https://rpg.blue/thread-365971-1-1.html 中的 Taroxd::Transition 类。

总之,这个变化的模式不应该局限于 X 坐标和 Y 坐标。

---
另外,我记得坐标是不需要变成整数的。就算赋值为整数似乎也会在内部转成浮点数的。

点评

实践是jQuery的easing和其他类似的,既然你提到了推广到通用所以我就更推广一下  发表于 2015-1-19 17:52
嗯,有想过,但是毕竟没怎么用到,所以没有付诸实践  发表于 2015-1-19 17:17
更通用的是移动的tween方式写成公式,和update分离  发表于 2015-1-19 01:10
回复

使用道具 举报

路人党员

梦石
0
星屑
52
在线时间
2276 小时
注册时间
2010-12-30
回帖
3011
发表于 2015-1-19 10:18:07 | 显示全部楼层
本帖最后由 英顺的马甲 于 2015-1-19 10:51 编辑

很多时候与其重定义method不如重定义变量

额...好吧,就放个例子
https://rpg.blue/forum.php?mod=r ... 990&pid=2575524
与其
[pre lang="ruby" line="1"]class Game_Party
  def item_number(id)
    @items[@actors[0].id] ? (@items[@actors[0].id].include?(id) ? @items[@actors[0].id][id] : 0) : 0
  end
  def weapon_number(id)
    @weapons[@actors[0].id] ? (@weapons[@actors[0].id].include?(id) ? @weapons[@actors[0].id][id] : 0) : 0
  end
  def armor_number(id)
    @armors[@actors[0].id] ? (@armors[@actors[0].id].include?(id) ? @armors[@actors[0].id][id] : 0) : 0
  end
  def gain_item(id, n)
    @items[@actors[0].id] = {} unless @items[@actors[0].id]
    if item_id > 0
      @items[@actors[0].id][id] = [[item_number(id) + n, 0].max, 99].min
    end
  end
  def gain_weapon(id, n)
    @weapons[@actors[0].id] = {} unless @weapons[@actors[0].id]
    if id > 0
      @weapons[@actors[0].id][id] = [[weapon_number(id) + n, 0].max, 99].min
    end
  end
  def gain_armor(id, n)
    @armors[@actors[0].id] = {} unless @armors[@actors[0].id]
    if id > 0
      @armors[@actors[0].id][id] = [[armor_number(id) + n, 0].max, 99].min
    end
  end
end[/pre]
不如[pre lang="ruby" line="1"]class Bag
  def initialize
    @data = {}
  end
  def method_missing(name, *args, &block)
    @data[$game_party.actors[0].id] ||= {}
    begin
      @data[$game_party.actors[0].id].send(name, *args, &block)
    rescue Exception
      raise($!.class, $!.message, caller)
    end
  end
end
class Game_Party
  alias multi_bag_init initialize unless defined?(multi_bag_init)
  def initialize
    multi_bag_init
    @items = Bag.new
    @weapons = Bag.new
    @armor = Bag.new
  end
end[/pre]

点评

就三个方法就上 method_missing 大法啊= = 话说 @armors 少写了一个 s。另外 Game_Actor 没有定义 hash 方法,所以可以直接拿来做哈希表的键(少打几个字-_-)  发表于 2015-2-8 21:15
第二种方法还能把冲突降到最低  发表于 2015-1-19 20:32
第一种方法实际上更改了原有背包信息的储存方式,所以它改了方法和变量结构。第二种方法是直接引入新的结构,自然要比第一种要好。  发表于 2015-1-19 20:00
其实我想要说的是多背包脚本...  发表于 2015-1-19 10:38
其实就是你的那个“装备附带技能”脚本让我坚信了改脚本要改method的想法。话说我还想把你那篇帖子引用到这里呢  发表于 2015-1-19 10:20

评分

参与人数 1星屑 +60 收起 理由
RyanBern + 60 吃糖了

查看全部评分

本人擅长XP,如果有脚本或者Ruby方面的问题欢迎发电邮到engshun@1ppl.me咨询,本人很少检查电邮所以不一定会及时回复,本人不会直接出手解决问题只会提供一个方向,所以谢绝伸手党
回复

使用道具 举报

…あたしは天

梦石
0
星屑
2308
在线时间
4033 小时
注册时间
2010-10-4
回帖
10548

开拓者贵宾

发表于 2015-2-8 21:45:13 | 显示全部楼层
本帖最后由 taroxd 于 2015-2-9 07:46 编辑

第四个,其实 VA 的 skill_learn? 并没有考虑附加的技能。
这里有我稍稍做的改动(让怪物也有技能):http://taroxd.github.io/rgss/Tar ... AE%BE%E7%BD%AE.html

第五个,我会更倾向于这么做(删去了并行事件,效率更高,事件页更简洁美观):
[pre lang="ruby" line="1"]
# 事件脚本的调用方式:this_event.set_timer(3600, 'B')
# this_event 容易实现,因此不再赘述
# 其实我本来想用这种更优雅的方式:
#     this_event.set_timer(3600) do
#       self_switch.b = true
#     end
# 但考虑到 block 不便存档,于是还是算了。

class Game_Event

  # *args: 帧数, 独立开关对应的字母
  def set_timer(*args)
    self_switch['timer'] = args # self_switch 对象很容易实现,见 http://taroxd.github.io/rgss/%E4 ... 89%A9%E5%B1%95.html
  end

  alias_method :timer_update, :update
  def update
    timer_update
    args = self_switch['timer']
    return unless args
    if args[0] > 0
      args[0] -= 1
    else
      self_switch[args[1]] = true
      self_switch['timer'] = nil
    end
  end
end
[/pre]

点评

即使 Game_SelfSwitches 没有自带刷新,这个刷新也可以包装在 SelfSwitch 对象里面。总之这种细节不是这个脚本的事情  发表于 2015-2-9 09:25
VA 里的 Game_SelfSwitches#[] 自带刷新  发表于 2015-2-9 09:23
去掉并行事件是个关键的想法,放在脚本里面刷新不错。貌似XP还要在独立开关打开后加一句$game_map.need_refresh = true  发表于 2015-2-9 09:09

评分

参与人数 1星屑 +60 收起 理由
RyanBern + 60 精品文章

查看全部评分

回复

使用道具 举报

…あたしは天

梦石
0
星屑
2308
在线时间
4033 小时
注册时间
2010-10-4
回帖
10548

开拓者贵宾

发表于 2015-2-18 11:44:46 | 显示全部楼层
技巧6里面,自己测试游戏的时候保存一下游戏就没有了吧……还不如保存在另一个文件里呢。

点评

不会没有啊,而且实际测试的时候,二周目信息已经转成开关或者变量了。测试二周目可以调开关啊。  发表于 2015-2-18 11:50
回复

使用道具 举报

梦石
0
星屑
60
在线时间
705 小时
注册时间
2007-12-23
回帖
809
发表于 2015-2-19 16:32:57 | 显示全部楼层
很有营养的东西。。

不过对于技巧6最好能够提供把二周目存档放在另一个文件的做法(类似Global_save之类的),不然不仅影响了严谨性(把item拿来做存档),而且也不方便转移存档呢(比如把存档拷到另一台电脑发现二周目又变回了一周目)

点评

生成独立文件是个好方法,这里其实只是记录了一个比较“另类”的方法。我是觉得两种方法各有特色,而且实现起来的难度都差不多。  发表于 2015-2-25 10:25
还是直接删档简单吧……这个真的,放在一个 Global_save 之类的存档里比较好  发表于 2015-2-25 10:08
只有当玩家选择“二周目”时才打开相应开关  发表于 2015-2-25 09:56
在选择“新游戏”后,如果二周目打通可以设置一个选项,让玩家选择是玩几周目。因为在实际游戏中负责表示几周目的是开关或者变量  发表于 2015-2-25 09:56
那么,如果玩家想要把存档删掉,回到一周目重新玩呢?  发表于 2015-2-25 08:47
买了正版RMMV的同学进来看一下,谢谢~
https://rpg.blue/thread-393237-1-1.html
回复

使用道具 举报

梦石
0
星屑
9557
在线时间
5074 小时
注册时间
2013-6-21
回帖
3459

开拓者贵宾剧作品鉴家

 楼主| 发表于 2015-3-19 15:22:50 | 显示全部楼层
更新技巧6、7,自顶。
技巧6是二周目的制作思路,虽然少用但是值得借鉴一下。

技巧7是一个多项选择插件的简易实现版,接入后再结合事件编辑器可以达到不错的效果,而且没有违和感。
借助这个思路,或许可以做出道具选择插件(即NPC向主角索要一个道具,主角选择道具之后对话继续)之类的效果。
回复

使用道具 举报

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

本版积分规则

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部