赞 189
VIP 627
好人卡 188
积分 95
经验 171230
最后登录 2024-7-3
在线时间 5073 小时
Lv4.逐梦者 (版主 )
梦石 0
星屑 9532
在线时间 5073 小时
注册时间 2013-6-21
帖子 3580
本帖最后由 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。
module RPG
class Journal
attr_accessor :name # 名称(String)
attr_accessor :description # 描述(String 数组)
attr_accessor :difficulty # 难度(Integer)
attr_accessor :reward_gold # 奖励金钱(Integer)
attr_accessor :reward_items # 奖励物品(数组,内部元素依然是数组,格式为[物品ID, 物品数量])
attr_accessor :reward_weapons # 奖励武器,格式同上
attr_accessor :reward_armors # 奖励防具,格式同上
def initialize
@name = ""
@description = [ "" ,"" ,"" ]
@difficulty = 0
@reward_gold = 0
@reward_items = [ ]
@reward_weapons = [ ]
@reward_armors = [ ]
end
end
end
module RPG
class Journal
attr_accessor :name # 名称(String)
attr_accessor :description # 描述(String 数组)
attr_accessor :difficulty # 难度(Integer)
attr_accessor :reward_gold # 奖励金钱(Integer)
attr_accessor :reward_items # 奖励物品(数组,内部元素依然是数组,格式为[物品ID, 物品数量])
attr_accessor :reward_weapons # 奖励武器,格式同上
attr_accessor :reward_armors # 奖励防具,格式同上
def initialize
@name = ""
@description = [ "" ,"" ,"" ]
@difficulty = 0
@reward_gold = 0
@reward_items = [ ]
@reward_weapons = [ ]
@reward_armors = [ ]
end
end
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):
module RPG
# 定义模块方法 get_journal_data
def self .get_journal_data
data = [ nil ]
# 1号任务
journal = RPG::Journal .new
journal.name = "第一个任务"
journal.description [ 0 ] = "随便玩玩就可以啦"
journal.difficulty = 0
journal.reward_items = [ [ 1 ,1 ] ,[ 2 ,1 ] ]
data.push ( journal)
# 1号任务完毕
return data
end
end
module RPG
# 定义模块方法 get_journal_data
def self .get_journal_data
data = [ nil ]
# 1号任务
journal = RPG::Journal .new
journal.name = "第一个任务"
journal.description [ 0 ] = "随便玩玩就可以啦"
journal.difficulty = 0
journal.reward_items = [ [ 1 ,1 ] ,[ 2 ,1 ] ]
data.push ( journal)
# 1号任务完毕
return data
end
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。
# 显示当前任务的窗口
class Window_Journal < Window_Selectable
# 对象初始化
def initialize
super ( 0 , 64 , 240 , 416 )
self .index = 0
refresh
end
# 取得当前光标选中的任务信息
def journal
return @data [ self .index ]
end
# 刷新
def refresh
# 如果内容被设置了就释放
if self .contents != nil
self .contents .dispose
self .contents = nil
end
@data = [ ]
# 添加当前任务
for i in $game_variables [ JOURNAL]
@data .push ( $data_journals[ i] )
end
# 取得最大项目数
@item_max = $game_variables [ JOURNAL] .size
# 最大项目数不为 0 就开始描绘
if @item_max > 0
self .contents = Bitmap.new ( width - 32 , @item_max * 32 )
for i in 0 ...@item_max
draw_item( i)
end
else
self .contents = Bitmap.new ( width - 32 , 32 )
self .contents .draw_text ( 4 , 0 , width - 36 , 32 , "无任务" )
end
end
# 描绘项目
def draw_item( i)
x = 4
y = i * 32
bitmap = RPG::Cache .icon ( "Journal.png" )
name = @data [ i] .name
self .contents .blt ( x, y, bitmap, Rect.new ( 0 ,0 ,24 ,24 ) )
self .contents .draw_text ( x+28 , y, width-x-36 -24 , 32 , name)
end
end
# 显示当前任务的窗口
class Window_Journal < Window_Selectable
# 对象初始化
def initialize
super ( 0 , 64 , 240 , 416 )
self .index = 0
refresh
end
# 取得当前光标选中的任务信息
def journal
return @data [ self .index ]
end
# 刷新
def refresh
# 如果内容被设置了就释放
if self .contents != nil
self .contents .dispose
self .contents = nil
end
@data = [ ]
# 添加当前任务
for i in $game_variables [ JOURNAL]
@data .push ( $data_journals[ i] )
end
# 取得最大项目数
@item_max = $game_variables [ JOURNAL] .size
# 最大项目数不为 0 就开始描绘
if @item_max > 0
self .contents = Bitmap.new ( width - 32 , @item_max * 32 )
for i in 0 ...@item_max
draw_item( i)
end
else
self .contents = Bitmap.new ( width - 32 , 32 )
self .contents .draw_text ( 4 , 0 , width - 36 , 32 , "无任务" )
end
end
# 描绘项目
def draw_item( i)
x = 4
y = i * 32
bitmap = RPG::Cache .icon ( "Journal.png" )
name = @data [ i] .name
self .contents .blt ( x, y, bitmap, Rect.new ( 0 ,0 ,24 ,24 ) )
self .contents .draw_text ( x+28 , y, width-x-36 -24 , 32 , name)
end
end
在这里,我们用一个常量JOURNAL来表示队伍中含有的任务数组在$game_variables里面存放的位置,当然可以随便更改。这个窗口是仿照了Window_Item脚本,没写太多注释,大家可以对比着看一下,都比较简单。
然后就是显示任务具体内容的窗口,这个用Window_Base生成就好,不过还是有些地方需要大家注意一下。首先大家要清楚,这个窗口是为了描述某个具体任务的,因此必须要有一个私有的实变量来保存当前描绘的任务,当然你不必将它设置为属性。还有一点就是关于描述任务奖励的,考虑到任务奖励种类比较多,为了美观,我们把它们合并在一起来描绘。
# 描绘任务具体内容的窗口
class Window_Journal_Contents < Window_Base
def initialize
super ( 240 , 64 , 400 , 416 )
self .contents = Bitmap.new ( width - 32 , height - 32 )
@journal = nil
refresh
end
# 设置当前要描绘的任务
def journal=( journal)
# 如果任务和当前任务有差异
if @journal != journal
@journal = journal
refresh
end
end
def refresh
self .contents .clear
self .contents .font .size = 18
self .contents .font .color = system_color
self .contents .draw_text ( 0 , 0 , 72 , 24 , "具体内容" )
self .contents .draw_text ( 0 , 96 , 36 , 24 , "难度" )
self .contents .draw_text ( 0 , 144 , 72 , 24 , "任务奖励" )
if @journal != nil
self .contents .font .color = normal_color
( 0 ..2 ) .each do |i|
self .contents .draw_text ( 0 , 24 + 24 * i, width - 32 , 24 , @journal .description [ i] )
end
diff = "★" * @journal .difficulty + "☆" * ( 5 - @journal .difficulty )
self .contents .draw_text ( 0 , 120 , width - 32 , 24 , diff)
total = 0
for item in @journal .reward_items
x = 184 * ( total % 2 )
y = 168 + 24 * ( total / 2 )
name = $data_items [ item[ 0 ] ] .name
icon = RPG::Cache .icon ( $data_items[ item[ 0 ] ] .icon_name )
number = item[ 1 ]
self .contents .blt ( x, y, icon, Rect.new ( 0 ,0 ,24 ,24 ) )
self .contents .draw_text ( x + 28 , y, 126 , 24 , name)
self .contents .draw_text ( x + 28 + 126 , y, 30 , 24 , number.to_s , 2 )
total += 1
end
for item in @journal .reward_weapons
x = 184 * ( total % 2 )
y = 168 + 24 * ( total / 2 )
name = $data_weapons [ item[ 0 ] ] .name
icon = RPG::Cache .icon ( $data_weapons[ item[ 0 ] ] .icon_name )
number = item[ 1 ]
self .contents .blt ( x, y, icon, Rect.new ( 0 ,0 ,24 ,24 ) )
self .contents .draw_text ( x + 28 , y, 126 , 24 , name)
self .contents .draw_text ( x + 28 + 126 , y, 30 , 24 , number.to_s , 2 )
total += 1
end
for item in @journal .reward_armors
x = 184 * ( total % 2 )
y = 168 + 24 * ( total / 2 )
name = $data_armors [ item[ 0 ] ] .name
icon = RPG::Cache .icon ( $data_armors[ item[ 0 ] ] .icon_name )
number = item[ 1 ]
self .contents .blt ( x, y, icon, Rect.new ( 0 ,0 ,24 ,24 ) )
self .contents .draw_text ( x + 28 , y, 126 , 24 , name)
self .contents .draw_text ( x + 28 + 126 , y, 30 , 24 , number.to_s , 2 )
total += 1
end
if @journal .reward_gold > 0
str = "获得金钱:" + @journal .reward_gold .to_s
y = 168 + ( ( total-1 ) / 2 + 1 ) * 24
self .contents .draw_text ( 0 , y, width - 32 , 24 , str)
elsif total == 0
self .contents .draw_text ( 0 , 168 , 18 , 24 , "无" )
end
end
end
end
# 描绘任务具体内容的窗口
class Window_Journal_Contents < Window_Base
def initialize
super ( 240 , 64 , 400 , 416 )
self .contents = Bitmap.new ( width - 32 , height - 32 )
@journal = nil
refresh
end
# 设置当前要描绘的任务
def journal=( journal)
# 如果任务和当前任务有差异
if @journal != journal
@journal = journal
refresh
end
end
def refresh
self .contents .clear
self .contents .font .size = 18
self .contents .font .color = system_color
self .contents .draw_text ( 0 , 0 , 72 , 24 , "具体内容" )
self .contents .draw_text ( 0 , 96 , 36 , 24 , "难度" )
self .contents .draw_text ( 0 , 144 , 72 , 24 , "任务奖励" )
if @journal != nil
self .contents .font .color = normal_color
( 0 ..2 ) .each do |i|
self .contents .draw_text ( 0 , 24 + 24 * i, width - 32 , 24 , @journal .description [ i] )
end
diff = "★" * @journal .difficulty + "☆" * ( 5 - @journal .difficulty )
self .contents .draw_text ( 0 , 120 , width - 32 , 24 , diff)
total = 0
for item in @journal .reward_items
x = 184 * ( total % 2 )
y = 168 + 24 * ( total / 2 )
name = $data_items [ item[ 0 ] ] .name
icon = RPG::Cache .icon ( $data_items[ item[ 0 ] ] .icon_name )
number = item[ 1 ]
self .contents .blt ( x, y, icon, Rect.new ( 0 ,0 ,24 ,24 ) )
self .contents .draw_text ( x + 28 , y, 126 , 24 , name)
self .contents .draw_text ( x + 28 + 126 , y, 30 , 24 , number.to_s , 2 )
total += 1
end
for item in @journal .reward_weapons
x = 184 * ( total % 2 )
y = 168 + 24 * ( total / 2 )
name = $data_weapons [ item[ 0 ] ] .name
icon = RPG::Cache .icon ( $data_weapons[ item[ 0 ] ] .icon_name )
number = item[ 1 ]
self .contents .blt ( x, y, icon, Rect.new ( 0 ,0 ,24 ,24 ) )
self .contents .draw_text ( x + 28 , y, 126 , 24 , name)
self .contents .draw_text ( x + 28 + 126 , y, 30 , 24 , number.to_s , 2 )
total += 1
end
for item in @journal .reward_armors
x = 184 * ( total % 2 )
y = 168 + 24 * ( total / 2 )
name = $data_armors [ item[ 0 ] ] .name
icon = RPG::Cache .icon ( $data_armors[ item[ 0 ] ] .icon_name )
number = item[ 1 ]
self .contents .blt ( x, y, icon, Rect.new ( 0 ,0 ,24 ,24 ) )
self .contents .draw_text ( x + 28 , y, 126 , 24 , name)
self .contents .draw_text ( x + 28 + 126 , y, 30 , 24 , number.to_s , 2 )
total += 1
end
if @journal .reward_gold > 0
str = "获得金钱:" + @journal .reward_gold .to_s
y = 168 + ( ( total-1 ) / 2 + 1 ) * 24
self .contents .draw_text ( 0 , y, width - 32 , 24 , str)
elsif total == 0
self .contents .draw_text ( 0 , 168 , 18 , 24 , "无" )
end
end
end
end
注意那个journal方法的定义,只有在@journal和参数不相等的时候,才进行刷新,这样也是为了减少refresh调用的次数。
中间refresh定义得比较啰嗦,不过物品武器装备这三个在一起真的好烦啊,希望高手能精简一下哈。大家可能注意到了,我们现在创建的两个窗口的y坐标都是64而不是0,这是由于我们在最顶端要说明每个窗口是干什么用的。
6.1.3 任务场景的制作
有了这两个窗口,任务场景的制作就会相当简单。新加的东西不多,不过要注意这里在场景中生成Window_Base的方法,因为这个窗口内容太单一了,我们就不单独给设一个类了。
另外,在update中,要保持左右窗口描述的任务一致,因此我们要不定期执行这个语句:
@contents_window .journal = @journal_window .journal
@contents_window .journal = @journal_window .journal
在这里,我们假设在地图Scene_Map上可以查看任务,那么在Scene_Map脚本也要进行输入的处理,这里略去过程了,想必大家已经会怎么弄了吧。
# 查看任务的场景
class Scene_Journal
def main
@journal_window = Window_Journal.new
@contents_window = Window_Journal_Contents.new
@base1 = Window_Base.new ( 0 , 0 , 240 , 64 )
@base1 .contents = Bitmap.new ( 208 , 32 )
@base1 .contents .draw_text ( 0 , 0 , 96 , 32 , "任务名称" )
@base2 = Window_Base.new ( 240 , 0 , 400 , 64 )
@base2 .contents = Bitmap.new ( 368 , 32 )
@base2 .contents .draw_text ( 0 , 0 , 96 , 32 , "具体内容" )
Graphics.transition
loop do
Graphics.update
Input.update
update
if $scene != self
break
end
end
Graphics.freeze
@base1 .dispose
@base2 .dispose
@journal_window .dispose
@contents_window .dispose
end
def update
@base1 .update
@base2 .update
@journal_window .update
@contents_window .journal = @journal_window .journal
# 按下 B 键的情况下,返回地图
if Input.trigger ?( Input::B )
$game_system .se_play ( $data_system.cancel_se )
$scene = Scene_Map.new
end
end
end
# 查看任务的场景
class Scene_Journal
def main
@journal_window = Window_Journal.new
@contents_window = Window_Journal_Contents.new
@base1 = Window_Base.new ( 0 , 0 , 240 , 64 )
@base1 .contents = Bitmap.new ( 208 , 32 )
@base1 .contents .draw_text ( 0 , 0 , 96 , 32 , "任务名称" )
@base2 = Window_Base.new ( 240 , 0 , 400 , 64 )
@base2 .contents = Bitmap.new ( 368 , 32 )
@base2 .contents .draw_text ( 0 , 0 , 96 , 32 , "具体内容" )
Graphics.transition
loop do
Graphics.update
Input.update
update
if $scene != self
break
end
end
Graphics.freeze
@base1 .dispose
@base2 .dispose
@journal_window .dispose
@contents_window .dispose
end
def update
@base1 .update
@base2 .update
@journal_window .update
@contents_window .journal = @journal_window .journal
# 按下 B 键的情况下,返回地图
if Input.trigger ?( Input::B )
$game_system .se_play ( $data_system.cancel_se )
$scene = Scene_Map.new
end
end
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回合之后的自由战斗回合(第一阶段)立即执行。如果执行完毕,那么就进入第二阶段。第一阶段就是这样短暂。 第二阶段:同伴命令回合
实际上就是进行队伍总体操作的回合,这场战斗你是打还是不打,如果选择“战斗”则进入第三阶段,如果选择“逃跑”,则进行相应的处理。在这里我们看到了逃跑的基本处理,代码如下:def update_phase2_escape
# 计算敌人速度的平均值
enemies_agi = 0
enemies_number = 0
for enemy in $game_troop .enemies
if enemy.exist ?
enemies_agi += enemy.agi
enemies_number += 1
end
end
if enemies_number > 0
enemies_agi /= enemies_number
end
# 计算角色速度的平均值
actors_agi = 0
actors_number = 0
for actor in $game_party .actors
if actor.exist ?
actors_agi += actor.agi
actors_number += 1
end
end
if actors_number > 0
actors_agi /= actors_number
end
# 逃跑成功判定
success = rand ( 100 ) < 50 * actors_agi / enemies_agi
# 成功逃跑的情况下
if success
# 演奏逃跑 SE
$game_system .se_play ( $data_system.escape_se )
# 还原为战斗开始前的 BGM
$game_system .bgm_play ( $game_temp.map_bgm )
# 战斗结束
battle_end( 1 )
# 逃跑失败的情况下
else
# 清除全体同伴的行动
$game_party .clear_actions
# 开始主回合
start_phase4
end
end
def update_phase2_escape
# 计算敌人速度的平均值
enemies_agi = 0
enemies_number = 0
for enemy in $game_troop .enemies
if enemy.exist ?
enemies_agi += enemy.agi
enemies_number += 1
end
end
if enemies_number > 0
enemies_agi /= enemies_number
end
# 计算角色速度的平均值
actors_agi = 0
actors_number = 0
for actor in $game_party .actors
if actor.exist ?
actors_agi += actor.agi
actors_number += 1
end
end
if actors_number > 0
actors_agi /= actors_number
end
# 逃跑成功判定
success = rand ( 100 ) < 50 * actors_agi / enemies_agi
# 成功逃跑的情况下
if success
# 演奏逃跑 SE
$game_system .se_play ( $data_system.escape_se )
# 还原为战斗开始前的 BGM
$game_system .bgm_play ( $game_temp.map_bgm )
# 战斗结束
battle_end( 1 )
# 逃跑失败的情况下
else
# 清除全体同伴的行动
$game_party .clear_actions
# 开始主回合
start_phase4
end
end
逃跑成功与否取决于处于战斗中的敌人和角色速度平均值的大小(已经阵亡的战斗者和没有出现的战斗者不算在内),如果敌人和角色速度平均值相等,则由50%的几率成功逃跑。当然,如果逃跑失败了,游戏还是要继续的,这时候所有角色都没有行动,整个队伍只能被敌人痛扁一顿……当然,如果你不喜欢这种逃跑的设定,完全可以通过修改,跳过第二阶段,不妨自己试一下吧。 第三阶段:角色命令回合
这个阶段相对前两个阶段,比较复杂,但是思路还是很明确的。在这个阶段,玩家为各个角色设定行动(当然行动能否执行取决于主回合执行的情况)。按照角色在队伍中的顺序来为各个角色设定他们的行为,首先是角色的基本命令,然后根据基本命令来选择接下来要显示的窗口。
def update_phase3
# 敌人光标有效的情况下
if @enemy_arrow != nil
update_phase3_enemy_select
# 角色光标有效的情况下
elsif @actor_arrow != nil
update_phase3_actor_select
# 特技窗口有效的情况下
elsif @skill_window != nil
update_phase3_skill_select
# 物品窗口有效的情况下
elsif @item_window != nil
update_phase3_item_select
# 角色指令窗口有效的情况下
elsif @actor_command_window .active
update_phase3_basic_command
end
end
def update_phase3
# 敌人光标有效的情况下
if @enemy_arrow != nil
update_phase3_enemy_select
# 角色光标有效的情况下
elsif @actor_arrow != nil
update_phase3_actor_select
# 特技窗口有效的情况下
elsif @skill_window != nil
update_phase3_skill_select
# 物品窗口有效的情况下
elsif @item_window != nil
update_phase3_item_select
# 角色指令窗口有效的情况下
elsif @actor_command_window .active
update_phase3_basic_command
end
end
这就是第三阶段,根据窗口激活的不同来选择刷新方法。在这里,我们看到了特技窗口和道具窗口在这个地方生成。
def start_skill_select
# 生成特技窗口
@skill_window = Window_Skill.new ( @active_battler)
# 关联帮助窗口
@skill_window .help_window = @help_window
# 无效化角色指令窗口
@actor_command_window .active = false
@actor_command_window .visible = false
end
def start_skill_select
# 生成特技窗口
@skill_window = Window_Skill.new ( @active_battler)
# 关联帮助窗口
@skill_window .help_window = @help_window
# 无效化角色指令窗口
@actor_command_window .active = false
@actor_command_window .visible = false
end
上面的方法是开始选择特技,在最后的结束特技选择时,@skill_window会被释放掉。 第四阶段:主回合
实际上就是战斗真正进行的阶段,前面几个阶段都是准备工作。
在这个阶段,系统会自动生成敌人的作战行动(见方法start_phase4中的语句),而后决定行动的先后次序,代码如下。
def make_action_orders
# 初始化序列 @action_battlers
@action_battlers = [ ]
# 添加敌人到 @action_battlers 序列
for enemy in $game_troop .enemies
@action_battlers .push ( enemy)
end
# 添加角色到 @action_battlers 序列
for actor in $game_party .actors
@action_battlers .push ( actor)
end
# 确定全体的行动速度
for battler in @action_battlers
battler.make_action_speed
end
# 按照行动速度从大到小排列
@action_battlers .sort ! { |a,b|
b.current_action .speed - a.current_action .speed }
end
def make_action_orders
# 初始化序列 @action_battlers
@action_battlers = [ ]
# 添加敌人到 @action_battlers 序列
for enemy in $game_troop .enemies
@action_battlers .push ( enemy)
end
# 添加角色到 @action_battlers 序列
for actor in $game_party .actors
@action_battlers .push ( actor)
end
# 确定全体的行动速度
for battler in @action_battlers
battler.make_action_speed
end
# 按照行动速度从大到小排列
@action_battlers .sort ! { |a,b|
b.current_action .speed - a.current_action .speed }
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】开始行动
在这一步骤中,主要进行的是各种行动效果的判断,具体看代码:
def update_phase4_step2
# 如果不是强制行动
unless @active_battler .current_action .forcing
# 限制为 [敌人为普通攻击] 或 [我方为普通攻击] 的情况下
if @active_battler .restriction == 2 or @active_battler .restriction == 3
# 设置行动为攻击
@active_battler .current_action .kind = 0
@active_battler .current_action .basic = 0
end
# 限制为 [不能行动] 的情况下
if @active_battler .restriction == 4
# 清除行动强制对像的战斗者
$game_temp .forcing_battler = nil
# 移至步骤 1
@phase4_step = 1
return
end
end
# 清除对像战斗者
@target_battlers = [ ]
# 行动种类分支
case @active_battler .current_action .kind
when 0 # 基本
make_basic_action_result
when 1 # 特技
make_skill_action_result
when 2 # 物品
make_item_action_result
end
# 移至步骤 3
if @phase4_step == 2
@phase4_step = 3
end
end
def update_phase4_step2
# 如果不是强制行动
unless @active_battler .current_action .forcing
# 限制为 [敌人为普通攻击] 或 [我方为普通攻击] 的情况下
if @active_battler .restriction == 2 or @active_battler .restriction == 3
# 设置行动为攻击
@active_battler .current_action .kind = 0
@active_battler .current_action .basic = 0
end
# 限制为 [不能行动] 的情况下
if @active_battler .restriction == 4
# 清除行动强制对像的战斗者
$game_temp .forcing_battler = nil
# 移至步骤 1
@phase4_step = 1
return
end
end
# 清除对像战斗者
@target_battlers = [ ]
# 行动种类分支
case @active_battler .current_action .kind
when 0 # 基本
make_basic_action_result
when 1 # 特技
make_skill_action_result
when 2 # 物品
make_item_action_result
end
# 移至步骤 3
if @phase4_step == 2
@phase4_step = 3
end
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多年的体会,大家就敬请期待吧。