3.1.3 地图角色Game_Character及它的两个子类Game_Player(角色)和Game_Event(事件)[/size]
这些脚本都是处理地图画面上角色以及NPC移动的类,我们先从Game_Character看起。
attr_reader :id # ID
attr_reader :x # 地图 X 坐标 (理论坐标)
attr_reader :y # 地图 Y 坐标 (理论坐标)
attr_reader :real_x # 地图 X 坐标 (实际坐标 * 128)
attr_reader :real_y # 地图 Y 坐标 (实际坐标 * 128)
attr_reader :tile_id # 元件 ID (0 为无效)
attr_reader :character_name # 角色 文件名
attr_reader :character_hue # 角色 色相
attr_reader :opacity # 不透明度
attr_reader :blend_type # 合成方式
attr_reader :direction # 朝向
attr_reader :pattern # 图案
attr_reader :move_route_forcing # 移动路线强制标志
attr_reader :through # 穿透
attr_accessor :animation_id # 动画 ID
attr_accessor :transparent # 透明状态
attr_reader :id # ID
attr_reader :x # 地图 X 坐标 (理论坐标)
attr_reader :y # 地图 Y 坐标 (理论坐标)
attr_reader :real_x # 地图 X 坐标 (实际坐标 * 128)
attr_reader :real_y # 地图 Y 坐标 (实际坐标 * 128)
attr_reader :tile_id # 元件 ID (0 为无效)
attr_reader :character_name # 角色 文件名
attr_reader :character_hue # 角色 色相
attr_reader :opacity # 不透明度
attr_reader :blend_type # 合成方式
attr_reader :direction # 朝向
attr_reader :pattern # 图案
attr_reader :move_route_forcing # 移动路线强制标志
attr_reader :through # 穿透
attr_accessor :animation_id # 动画 ID
attr_accessor :transparent # 透明状态
在这里我们看到了Game_Character的一些属性,这里的x和y相当于我们地图编辑器中的这个:
是理论的坐标值,实际坐标值和理论坐标值有个换算关系,实际坐标 = 理论坐标 * 128,这个记住就好。我们写脚本的时候,理论坐标用得最多。Character_name和character_hue表示的是角色行走图的文件名和色相,这个和我们刚才讲Game_Actor里面的属性几乎是一样的,不过Game_Character作为角色行走图类,这个属性和Game_Actor的还是有本质区别。
pattern指的是角色行走图的图案序号,这是个什么东西呢?我们知道角色走动的时候,会有踏步动画,一个角色行走图文件是4*4个图形,不同横行代表不同方向,同一横行的四个图案就是用来做成动画的,这个pattern就是指同一横行的图形号码。
direction指的是行走图的朝向,在RGSS中,数字2、4、6、8分别代表下,左,右,上。这个怎么记忆呢?我们看看电脑上的数字小键盘,这个对应关系,正好是小键盘数字和方向的对应。
接下来我们看下这里面的各种方法。RGSS有一套默认的角色移动规则,这类规则我们也不用做太多改变,已经是比较合理的了。因此下面的函数我们看看形式即可。
def screen_z(height = 0)
# 在最前显示的标志为 ON 的情况下
if @always_on_top
# 无条件设置为 999
return 999
end
# 通过实际坐标和地图的显示位置来求得画面坐标
z = (@real_y - $game_map.display_y + 3) / 4 + 32
# 元件的情况下
if @tile_id > 0
# 元件的优先不足 * 32
return z + $game_map.priorities[@tile_id] * 32
# 角色的场合
else
# 如果高度超过 32 就判定为满足 31
return z + ((height > 32) ? 31 : 0)
end
end
def screen_z(height = 0)
# 在最前显示的标志为 ON 的情况下
if @always_on_top
# 无条件设置为 999
return 999
end
# 通过实际坐标和地图的显示位置来求得画面坐标
z = (@real_y - $game_map.display_y + 3) / 4 + 32
# 元件的情况下
if @tile_id > 0
# 元件的优先不足 * 32
return z + $game_map.priorities[@tile_id] * 32
# 角色的场合
else
# 如果高度超过 32 就判定为满足 31
return z + ((height > 32) ? 31 : 0)
end
end
例如,我们看一下这个画面z坐标的方法。因为我们是调查的行走图在画面上的z坐标,而不是元件,因此我们不要随便引入优先级这个概念。首先,我们知道,事件选项里面有一个“总在最前面显示”,这种事件的z坐标是最高的。另外,我们要注意,表示角色的Game_Player是没有这个属性的,因此角色永远也不能成为画面最上层的东西。下面我们来看其他情况的z坐标。这里先计算了一下画面的y坐标,这是为什么呢?因为我们知道表示角色和事件的图形都有一定的高度,而在地图上,一个格子的大小是32*32,超出的部分就要向上下左右扩张,而我们知道,一个NPC图形的大小是32*48,RGSS中,默认把图形的中下方的32*32区域放在指定坐标的格子中,因此角色头部就会覆盖他上方的格子。
比方说上图这种情况,我们要让y坐标大的角色z坐标也稍微大一点,这样可以覆盖他上方的角色图形。当然我们知道,角色图形可以是元件,那么元件的z坐标就和它的优先度有关。在最后一行我们可以看到,1个优先度=32个z坐标,也就是说,角色和事件图形的优先度是大于0小于1的,因此只要优先度超过1的元件,都会把事件盖住。
Game_Character下面两页几乎都是写移动和跳跃规则的方法。因此,我们除了在事件中运用“设置移动路线”来让角色移动外,还可以直接用脚本来让角色运动。例如写$game_player.move_left就可以让主角左移动一格。
然后我们看Game_Player。
因为在主角移动时,画面会始终跟随主角移动,因此Game_Player里面多了个center(x,y)方法。这个大家看看就行了,没必要完全搞懂。
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
end
end
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
end
end
在这个方法中,我们终于知道了游戏为什么要记录主角的步数。很多人认为新工程里面主菜单显示“步数”完全是个多余之举,确实我也认为没有显示的必要。步数是用来生成遇敌计数和检查连续伤害用的,这里,地图用的连续伤害是每走2步就减去1%的HP,当然你可以对此进行更改。
def refresh
# 同伴人数为 0 的情况下
if $game_party.actors.size == 0
# 清除角色的文件名及对像
@character_name = ""
@character_hue = 0
# 分支结束
return
end
# 获取带头的角色
actor = $game_party.actors[0]
# 设置角色的文件名及对像
@character_name = actor.character_name
@character_hue = actor.character_hue
# 初始化不透明度和合成方式子
@opacity = 255
@blend_type = 0
end
def refresh
# 同伴人数为 0 的情况下
if $game_party.actors.size == 0
# 清除角色的文件名及对像
@character_name = ""
@character_hue = 0
# 分支结束
return
end
# 获取带头的角色
actor = $game_party.actors[0]
# 设置角色的文件名及对像
@character_name = actor.character_name
@character_hue = actor.character_hue
# 初始化不透明度和合成方式子
@opacity = 255
@blend_type = 0
end
在这里我们明白了,地图上角色的图形和队伍中第一名角色的图形是一致的。因此,我们完全可以把它改成别的。在Game_Party中我们可以增加“领队”这一属性,然后把中间的actor = $game_party.actors[0]换成actor = $game_party.leader即可。当然Game_Party的leader还需要定义。另外,我们经常遇到主角变更交通工具的情况,比方说航海的时候,主角图形则要显示为一艘船。这个我们纯用事件是无法做到的(我们可以通过改变队伍中第一个角色的行走图来获得这个效果,但是你打开菜单,会发现角色的脸谱也变成了一艘船,这个显然很不合理),我们仍然可以在这里进行改动。例如,设置一个航海中的标志,如果队伍在航海中,则直接设置@character_name和@character_hue即可。
在Game_Player的update方法中,我们看到了输入的处理。
case Input.dir4
when 2
move_down
when 4
move_left
when 6
move_right
when 8
move_up
end
case Input.dir4
when 2
move_down
when 4
move_left
when 6
move_right
when 8
move_up
end
这是对4方向输入的处理,用Input.dir4,游戏中还有一个对8方向的输入处理Input.dir8,增加了4个斜向的移动,1、3、7、9分别表示,左下,右下,左上,右上,还是跟小键盘的方位是一样的(注:F1再次出现错误,F1原来写的是数字1到8表示8方向输入,这是不对的,应该是1、2、3、4、6、7、8、9才对)。
case Input.dir8
when 1
move_lower_left
when 2
move_down
when 3
move_lower_right
when 4
move_left
when 9
move_upper_right
when 6
move_right
when 7
move_upper_left
when 8
move_up
end
case Input.dir8
when 1
move_lower_left
when 2
move_down
when 3
move_lower_right
when 4
move_left
when 9
move_upper_right
when 6
move_right
when 7
move_upper_left
when 8
move_up
end
这个是修改后8方向是输入,大家可以试一下。
关于Game_Xxxx类脚本我们就要说这些,大家有兴趣的话可以多看看脚本,会有更多意想不到的发现。
3.2 自制游戏对象
看了这么多游戏对象的脚本,是不是有自制游戏对象的念头了呢?那我们在这一小节就说说如何自制属于你自己的游戏对象脚本。
3.2.1 起步
首先要明白,你自己想自定义的游戏数据到底属不属于游戏对象,如果不属于(比方说你要建立一个数据库的类,这就不属于游戏对象),那么就不应该用游戏对象的思路。总的来说,随着游戏进行,随时都有改变可能,并且跟玩家进行直接互动的对象,大多都是游戏对象。下面我们来看一个例子:
例如,制作一个真实商店,要求是商店尽可能还原真实生活中的商店,有库存限制,你卖给这个商店的物品,商店也会原封不动显示出来。这就是一个游戏对象。
3.2.2 分析属性和方法
下面我们要分析一下,我们建立的游戏对象应该有哪些内容和方法。
还是以真实商店为例。
我们需要的是商店的最大库存(所有商品的最大数量,为了方便起见,对于同一个商店,所有商品的最大库存都是一样的),以及商店里面各种商品的数量。除此以外,暂时还未想到其他的属性。
真实商店的方法很简单,就是出货和进货,不过考虑到物品分为道具,武器,防具3种,我们可能要定义很多方法。
暂时想到这么多,然后我们就可以开工了。
3.2.3 写出游戏对象脚本
我们现在,就可以根据我们的分析,来写出真实商店脚本的游戏对象部分了。先是定义各种属性,考虑到道具,武器,防具的不同性质,我们把它们定义在三个表中。
class Game_VisualShop
attr_reader :max_number
attr_reader :shop_goods_item
attr_reader :shop_goods_weapon
attr_reader :shop_goods_armor
end
class Game_VisualShop
attr_reader :max_number
attr_reader :shop_goods_item
attr_reader :shop_goods_weapon
attr_reader :shop_goods_armor
end
接下来定义初始化:
def initialize(max_number,items,weapons,armors)
@max_number = max_number
@shop_goods_item = {}
@shop_goods_weapon = {}
@shop_goods_armor = {}
for j in items
@shop_goods_item[j] = @max_number
end
for j in weapons
@shop_goods_weapon[j] = @max_number
end
for j in armors
@shop_goods_armor[j] = @max_number
end
end
def initialize(max_number,items,weapons,armors)
@max_number = max_number
@shop_goods_item = {}
@shop_goods_weapon = {}
@shop_goods_armor = {}
for j in items
@shop_goods_item[j] = @max_number
end
for j in weapons
@shop_goods_weapon[j] = @max_number
end
for j in armors
@shop_goods_armor[j] = @max_number
end
end
这个初始化就完成了,我们注意,传进去的参数有4个,分别是库存最大数量,各种物品的数组,数组里面是物品的ID,我们这里用ID而不是用物品的实例,是为了模仿游戏主角背包的存储模式,这个一定要注意。然后就是把商店各种物品数量初始化为库存最大值。这个也是看个人需要。我们用的是Hash表来表示现有的库存,这个也是借鉴了Game_Party中对于队伍中各种物品武器防具的刻画。写到这里,我们可以考虑给真实商店增加一个变量,表示商店中可以显示库存为0的物品(其余的库存为0的物品就直接不显示)。这是因为你卖给这个商店的物品和这个商店原有的物品本质是不一样的。这是一个二维数组,含有3个一维数组,分别表示总显示的道具、武器、防具。因此在initialize的最后,加入
@always_shown = []
@always_shown[0] = @shop_goods_item.keys
@always_shown[1] = @shop_goods_weapon.keys
@always_shown[2] = @shop_goods_armor.keys
@always_shown = []
@always_shown[0] = @shop_goods_item.keys
@always_shown[1] = @shop_goods_weapon.keys
@always_shown[2] = @shop_goods_armor.keys
这个显然我们不用把它公开化,放在这里就可以了。
然后是各种方法,也是非常简单的。
def import(type,id,number)
case type
when 0
if @shop_goods_item[id] == nil
@shop_goods_item[id] = 0
end
@shop_goods_item[id] += number
if @shop_goods_item[id] > @max_number
@shop_goods_item[id] = @max_number
end
if @shop_goods_item[id] <= 0
@shop_goods_item[id] = 0
unless @always_shown[0].include?(id)
@shop_goods_item.delete(id)
end
end
when 1
…..
end
end
def import(type,id,number)
case type
when 0
if @shop_goods_item[id] == nil
@shop_goods_item[id] = 0
end
@shop_goods_item[id] += number
if @shop_goods_item[id] > @max_number
@shop_goods_item[id] = @max_number
end
if @shop_goods_item[id] <= 0
@shop_goods_item[id] = 0
unless @always_shown[0].include?(id)
@shop_goods_item.delete(id)
end
end
when 1
…..
end
end
这是一个通用的商店进货的方法,type表示种类,0为道具,1为武器,2为防具。id是对应类型的id,number是数量。添加的时候,考虑到某物品的Hash个数可能不存在,因此要进行一步判断。然后再进行增加操作,并修改范围。如果最终个数是0并且不是永远显示的话,直接从相应Hash表中删除。这里只给出了道具部分的定义,武器和防具部分定义是类似的。
有了这个东西,出货的方法就很容易写了。
def export(type,id,number)
import(type,id,-number)
end
def export(type,id,number)
import(type,id,-number)
end
直接调用import方法,并将number取成负值即可。
到这里我们所有的基本方法就定义完了,Game_VisualShop也告一段落,不过,现在它只是一个游戏对象,如何将它和玩家互动起来,我们还需要学习后面的内容。
第3章的讲解就到这里,在下一章我们会将到窗口的使用,大家敬请期待吧。
作者: 火烧兔子 时间: 2013-10-19 22:16
我不知道为什么不论是RGSS还是C语言,都是从计算加减乘除开始的,这样很容易让我们丧失继续学习下去的耐心,我们想要做的是游戏而非计算!
作者: RyanBern 时间: 2013-10-20 08:25
火烧兔子 发表于 2013-10-19 22:16
我不知道为什么不论是RGSS还是C语言,都是从计算加减乘除开始的,这样很容易让我们丧失继续学习下去的耐心 ...
写游戏切忌急功近利,学脚本也是。只有先弄清楚基础知识,才能够深刻理解游戏的抽象数据类型。但是考虑到实际的运用价值,这些计算确实很难让人有学习下去的欲望。大概是大家对计算都熟悉得很吧,因此我这个帖子没有涉及很多计算和语法方面,而是侧重于理解游戏的数据结构。不过,如果要踏踏实实成为“脚本党”的话,计算方面的东西是马虎不得的。
作者: lucifer4223 时间: 2013-10-20 20:09
自称做游戏的,一般两类人,一是改素材的,利用现有引擎,几乎不懂脚本。
二是偏向做程序的,甚至直接用 C++全部自己写最基本的窗口渲染。
楼主这个文章我认真地看了,拨开了我心中的云雾。力挺!
我是一个完全没程序基础的,本来想学 C++ 或者 Java之类的,打发时间娱乐而已。
但苦于无法入门。非常偶然的机会发现,RMXP。并且发现 66这个好地方,
一大堆免费,并且直观的教程。
很多身边的朋友劝我先学 Java,其实我觉得不然,有兴趣才有动力。
先学高级的,再在漫长的过程中挖掘基础的,也是很好的。
短短两个月里,我自己也总结出不少菜鸟心得。要再等几个月我有完整作品了再一起发布。
最近在拆解 66 fan 的 RTAB,很长很复杂,但我决定把它完全拆开,
将各个优秀的功能分解成外挂脚本,方便"脚本党"的使用。
这几天卡在相机移动这个地方了,不过也快了。
期待楼主早点讲讲 RMXP里面 update 循环刷新的机制。
作者: RyanBern 时间: 2013-10-20 21:06
lucifer4223 发表于 2013-10-20 20:09
自称做游戏的,一般两类人,一是改素材的,利用现有引擎,几乎不懂脚本。
二是偏向做程序的,甚至直接用 C+ ...
感谢你的支持!一直以来,我认为掌握了脚本就是掌握了写游戏的主动权,否则始终也不明白游戏运行的机理。当然,我觉得很多人写脚本纯粹是兴趣所在,这也是我们能继续把游戏创作下去的动力。这个帖子本身难度不高,而且只要你看过F1的基础篇,就可以从这里收获点什么。
这个帖子,最适合一些渴望写脚本,有一些基本知识,但是写出的脚本总也通不过的朋友们。我想,职业写脚本的人毕竟是少数,大多数人也是一点点摸索,一点点理解,经过不断尝试——犯错的反复中,摸清RMXP的脾气。我其实就是这样过来的……
分解RTAB系统,这个真是很好的计划。早就听说RTAB是个神器,里面的任何一部分都有我们借鉴的机会,因此要加油啊。
我写脚本时间不算太短,但是游戏却一个也没做出来,这也算是遗憾吧,毕竟,业余脚本党专注于脚本,对真正完成一个游戏的事情还是欠缺很多动力的。
作者: RyanBern 时间: 2013-10-22 21:40
本帖最后由 RyanBern 于 2015-8-2 13:24 编辑
[box=RoyalBlue]第4章节:窗口的使用
[/box]
接下来我们终于要学习窗口和精灵了,这我想也是大家特别想知道的。因为在这之前,我们都是同抽象的数据打交道,难免有些枯燥,那么在这一章,我们就要把我们自己脑中所想,全部展现在画面中。下面我们就开始吧。
4.1 窗口的使用
4.1.1各种概念
在最开始,我们要明白一个窗口到底是什么,它到底有哪些属性。因此,我们必须要先介绍下面几个类。
①rect:矩形的类
顾名思义,这个类表示矩形,但这种东西是不能直接显示出来的。矩形是一种基本的类,它的作用在很多高级的类中体现尤其明显。矩形有四个属性,x坐标,y坐标,宽度,高度。其中x坐标和y坐标是相对而言的。
②viewport:视口的类
这个类是一个比较难理解的类,我在最开始也没明白这是什么东西。在屏幕上生成可见的对象都必须要指定视口viewport。简单说来,你面前有一堵墙,墙上有一个窗户可以看到墙对面的东西,那么这个窗户就可以类比成视口,你只能看到视口内有的东西,视口之外,即使有内容存在,你是看不到的。
我们举个简单的例子,大家打开脚本编辑器,在Window_BattleStatus中的initialize方法中,输入self.back_opacity = 0
然后随便找一个640*480的战斗图片(注意大小是640*480),设置为一个地图的战斗图,然后进入战斗场景,画面会变成这样:
下面的640*160的矩形区域全部都是黑色的,这就说明图片显示不全,只有上半部分显示了出来。我们说过图片是640*480的,因此出现这种情况是我们视口的设置问题。打开Spriteset_Battle,我们在前面发现了这个:
我们看到生成战斗背景的视口是640*320,因此我们的640*480图片才显示不出来。
因此,无论图片是什么,生成的可视对象都必须在视口之内。
视口的提出为我们管理界面上的可显示元素提供方便,因为视口的移动,色相改变,闪烁只会影响视口内部的元素,而视口外部的元素不受影响。
③bitmap:位图的类
我们可以认为这一个类专门表示图片,但是要注意,Bitmap对象只是一个数据而已,并不能直接作为显示在屏幕中的对象。所以,bitmap是没有x坐标和y坐标这一属性的,这个要千万注意。但是,作为游戏的一种数据,在游戏运行的时候,必须要进入到游戏的内存中,RGSS里面有个高速缓存,专门用来保存这些临时的图片,用的时候就要在这里读取。因为高速缓存空间有限,因此我们要学会将不用的bitmap释放掉。
我们来看看bitmap里面都有什么方法。
dispose:释放位图,即将不用的位图从高速缓存中释放掉,我们在写程序时,必须有及时释放的好习惯,当然如果一个位图被频繁的使用,就不用释放了。
clear:清除位图全体,即将位图的内容变成一个空的“画布”
blt(x,y,src_bitmap,src_rect,opacity):这个方法非常重要,大家一定要熟练掌握。这个方法的作用有点类似于两个位图之间的拷贝,意思就是将位图A指定矩形的内部内容拷贝到本位图中,放到指定坐标的位置。下图更加直观说明了这个事情。
这样大家应该理解了吧。
fill_rect(x,y,width,height,color)
fill_rect(rect,color):上面两个都是在位图中绘制实心矩形,可以用矩形的各种信息,也可以直接把矩形放在参数中,要指定填充的颜色,利用这两个函数,我们可以在bitmap中绘制矩形,然后把它显示出来(要用到精灵)。朴素的HP条SP条就是这样描绘的。
draw_text(x,y,width,height,str,align):在位图的指定区域描绘文字,align是对齐方式。这有点类似于Windows画图中的创建文字。这个大家也要熟练使用。F1中也说,这个处理要花费时间,尽量不要重复描绘字符串(意思就是只有在文字改变的时候才可描绘,比较省CPU,防止游戏卡顿)。
这些方法大家一定要熟练掌握,下面我们正式开始窗口的学习。
4.1.2 窗口的基本知识 一般窗口的实现
脚本编辑器中,所有窗口的父类Window没有给出,我们也无法得知它的代码,但是我们打开F1,搜索Window,就会了解Window类的方法和属性,这对我们来说是个很好的参考。
窗口实际上是一种可以在屏幕上显示的图片,因此生成必须要指定视口。但是由于系统默认的缘故,不用额外加以设置。窗口以大量精灵构成,因此要有dispose方法。我们使用一个窗口完毕后,一定要把它释放掉。这个我们在介绍场景的时候会有说明。
F1中将各种方法已经说得很详细,这个我就不用多说。不过还是强调几个地方吧。
visible是可见与否的属性,如果你已经生成了一个窗口,但是不想显示它,就要用到visible属性,注意,隐藏不意味着释放。
active是活动状态属性,主要出现在有光标的窗口中,处于活动状态的窗口可以接受命令,进行光标闪烁等,游戏不能同时处理2个以上活动的窗口(你没见过按一下方向键两个窗口跟着一起变的吧?),因此同一个场景中,活动的窗口至多1个。
ox,oy是窗口内容原点的坐标,在这里我们可以把窗口看作一个小小的视口,里面的内容只能通过窗口显示出来。
注意:游戏默认窗口内容显示的视口是窗口上下左右边界向内16个像素,因此窗口实际内容和窗口边界总有16个像素的间隔,这个就尽量不要更改了。
作为所有Window的父类,Window里面可用的方法很少,我们来看看真正能作为显示窗口类的Window_Base:
先看initialize部分:
def initialize(x, y, width, height)
super()
@windowskin_name = $game_system.windowskin_name
self.windowskin = RPG::Cache.windowskin(@windowskin_name)
self.x = x
self.y = y
self.width = width
self.height = height
self.z = 100
end
def initialize(x, y, width, height)
super()
@windowskin_name = $game_system.windowskin_name
self.windowskin = RPG::Cache.windowskin(@windowskin_name)
self.x = x
self.y = y
self.width = width
self.height = height
self.z = 100
end
这里,我们生成一个窗口对象要指定它的位置和大小,并且设定窗口外观(这里默认是数据库的窗口外观),最后是z坐标,窗口的高度。
紧接着后面是个释放的方法,重定义:
def dispose
# 如果窗口的内容已经被设置就被释放
if self.contents != nil
self.contents.dispose
end
super
end
def dispose
# 如果窗口的内容已经被设置就被释放
if self.contents != nil
self.contents.dispose
end
super
end
在这里我们看到,对窗口释放同时也释放了作为窗口内容显示的位图,这样的好处是让我们的操作变得简单。例如写@a.dispose这样的语句,不但释放了窗口本身,也释放了窗口内容位图,一举两得。
后面是定义获取各种颜色的方法,这我就不多说了。
刷新update:
def update
super
# 如果窗口的外关被变更了、再设置
if $game_system.windowskin_name != @windowskin_name
@windowskin_name = $game_system.windowskin_name
self.windowskin = RPG::Cache.windowskin(@windowskin_name)
end
end
def update
super
# 如果窗口的外关被变更了、再设置
if $game_system.windowskin_name != @windowskin_name
@windowskin_name = $game_system.windowskin_name
self.windowskin = RPG::Cache.windowskin(@windowskin_name)
end
end
这个update方法是刷新窗口,在RGSS中,有刷新作用的方法,原则上1帧调用一次,这个一定要注意。我们看到Window_Base的刷新很简单,就是要判定窗口外观的改变,并没有涉及内容的处理。因此我们在描绘内容时,尽量不要刷新它,我们刚才说到了draw_text需要花费时间,因此要避免每1帧都重新描绘文字。
下面则是一些基本的描绘:
def draw_actor_graphic(actor, x, y)
bitmap = RPG::Cache.character(actor.character_name, actor.character_hue)
cw = bitmap.width / 4
ch = bitmap.height / 4
src_rect = Rect.new(0, 0, cw, ch)
self.contents.blt(x - cw / 2, y - ch, bitmap, src_rect)
end
def draw_actor_graphic(actor, x, y)
bitmap = RPG::Cache.character(actor.character_name, actor.character_hue)
cw = bitmap.width / 4
ch = bitmap.height / 4
src_rect = Rect.new(0, 0, cw, ch)
self.contents.blt(x - cw / 2, y - ch, bitmap, src_rect)
end
角色图形的描绘,有3个参数,角色,描绘x坐标,描绘y坐标。
第二行是取得角色的位图,把它存储在高速缓存中。用的模块方法RPG::Cache,这个大家自行F1就好。然后是设定显示的内容,我们不能把整个图片显示出来,只需要显示左上角那个正脸的图就可以了,因此后面3行全部在设置截取图片的位置,随后,用blt方法,把截取的位置原封不动传送到self.contents中,window的contents就是表示窗口内容的位图,这恰恰就是blt完成的工作:把一个位图里面指定位置内容传给另一个位图中。在窗口中描绘图片就是这么简单。
下面我们再来看一个描绘文字的:
def draw_actor_hp(actor, x, y, width = 144)
# 描绘字符串 "HP"
self.contents.font.color = system_color
self.contents.draw_text(x, y, 32, 32, $data_system.words.hp)
# 计算描绘 MaxHP 所需的空间
if width - 32 >= 108
hp_x = x + width - 108
flag = true
elsif width - 32 >= 48
hp_x = x + width - 48
flag = false
end
# 描绘 HP
self.contents.font.color = actor.hp == 0 ? knockout_color :
actor.hp <= actor.maxhp / 4 ? crisis_color : normal_color
self.contents.draw_text(hp_x, y, 48, 32, actor.hp.to_s, 2)
# 描绘 MaxHP
if flag
self.contents.font.color = normal_color
self.contents.draw_text(hp_x + 48, y, 12, 32, "/", 1)
self.contents.draw_text(hp_x + 60, y, 48, 32, actor.maxhp.to_s)
end
end
def draw_actor_hp(actor, x, y, width = 144)
# 描绘字符串 "HP"
self.contents.font.color = system_color
self.contents.draw_text(x, y, 32, 32, $data_system.words.hp)
# 计算描绘 MaxHP 所需的空间
if width - 32 >= 108
hp_x = x + width - 108
flag = true
elsif width - 32 >= 48
hp_x = x + width - 48
flag = false
end
# 描绘 HP
self.contents.font.color = actor.hp == 0 ? knockout_color :
actor.hp <= actor.maxhp / 4 ? crisis_color : normal_color
self.contents.draw_text(hp_x, y, 48, 32, actor.hp.to_s, 2)
# 描绘 MaxHP
if flag
self.contents.font.color = normal_color
self.contents.draw_text(hp_x + 48, y, 12, 32, "/", 1)
self.contents.draw_text(hp_x + 60, y, 48, 32, actor.maxhp.to_s)
end
end
描绘角色HP。这里面参数多了一个宽度width,大家注意观察就可以发现,在菜单中描绘HP,是有最大HP的,在战斗中则没有,就是由于二者宽度不同导致的。后面的draw_text大家看看,基本上都会用。在这里提示一下,draw_text能描绘字符串,而不能描绘数字本身,因此draw_text(0, 0, 32, 32, 250)这样来描绘数字250是行不通的,Object类有个方法叫做to_s,把本身转换为字符串,因此要写250.to_s,这样等同于"250"。
另外,很多人被那些坐标还有宽度设置困扰,总也描绘不到合适的位置。这个其实也比较好解决,整个画面是640*480的,简单进行一些运算就会知道各种位置。还有,在窗口中进行的x,y都是指相对窗口内容位图的原点位置,并不是屏幕的位置,这个大家要注意。游戏默认字的高度是24个像素,一行的高度是32个像素,因此,如果5个默认大小的字连起来时,长度就是120(一个汉字的宽度=2个英文字母的宽度),因此根据这个就可以设定描绘文字矩形的宽度了。最后要注意,draw_text是不能换行的,如果文字太长,它会一直描绘下去,可能会出边界。
有了这些知识,我们就可以来刻画一个最基本的窗口了。下面来小小测试一下。
class Window_Test < Window_Base
def initialize
super(0,0,196,64)
self.contents = Bitmap.new(width – 32, height - 32)
refresh
end
def refresh
self.contents.clear
self.contents.draw_text(0,0,144,32,”RMXP is good”)
end
end
class Window_Test < Window_Base
def initialize
super(0,0,196,64)
self.contents = Bitmap.new(width – 32, height - 32)
refresh
end
def refresh
self.contents.clear
self.contents.draw_text(0,0,144,32,”RMXP is good”)
end
end
这样一个简单的窗口就定义好了。
但是这样是无法在屏幕上显示的,要让它显示出来,通常要借助场景Scene的帮助。
我们先学一点后面的东西,让我们的窗口显示出来。
在Scene_Map的main函数里面添加这样的语句:
然后再运行一下。效果如图。
在这里我们改动了Scene_Map脚本,但是也很简单,先创建一个窗口,在场景结束后再释放它,就是这样。
但是现在我们生成的窗口内容不能自动变化,如果我们要生成一个内容可能变化的窗口,这样就不行了。比方说,我们要在地图上显示角色的金钱,但是如果通过事件增加了队伍的金钱,窗口的内容是没有变化的。这就需要我们对update进行下改装,如果内容变更了,就重新描绘内容。我们知道draw_text是不适合反复调用的,因此我们只有在需要的时候,才能重新描绘窗口内容。首先要在refresh方法加上
然后我们修改update方法:
def update
super
if $game_party.gold != @gold
refresh
end
end
def update
super
if $game_party.gold != @gold
refresh
end
end
这里,我们可以设置一个不公开的变量@gold,表示当前窗口描绘的金钱数量。然后对update进行重定义,让它能自动检查内容的变化。当然,我们也要对场景脚本进行改变,场景脚本里面有个update方法,在里面,我们加入对窗口的刷新@a.update,在这里我就不给出所有的代码了,作为练习,希望读者能写出一个在地图上显示金钱的窗口。
当然,这样简单的窗口,我们还是有方法能判断内容是否变更的,但是对于复杂的窗口,我们就无法判断了,因此,我们还需要掌握其他方法。
4.1.3 滚动窗口的实现
下面我们学习如何使用滚动窗口。滚动窗口要用Window_Selectable。
打开这个脚本,我们发现它是Window_Base的子类。
#--------------------------------------------------------------------------
# ● 定义实例变量
#--------------------------------------------------------------------------
attr_reader :index # 光标位置
attr_reader :help_window # 帮助窗口
#--------------------------------------------------------------------------
# ● 初始化对像
# x : 窗口的 X 坐标
# y : 窗口的 Y 坐标
# width : 窗口的宽
# height : 窗口的高
#--------------------------------------------------------------------------
def initialize(x, y, width, height)
super(x, y, width, height)
@item_max = 1
@column_max = 1
@index = -1
end
#--------------------------------------------------------------------------
# ● 定义实例变量
#--------------------------------------------------------------------------
attr_reader :index # 光标位置
attr_reader :help_window # 帮助窗口
#--------------------------------------------------------------------------
# ● 初始化对像
# x : 窗口的 X 坐标
# y : 窗口的 Y 坐标
# width : 窗口的宽
# height : 窗口的高
#--------------------------------------------------------------------------
def initialize(x, y, width, height)
super(x, y, width, height)
@item_max = 1
@column_max = 1
@index = -1
end
这里多了两个属性,光标位置(索引)和帮助窗口。考虑到很多滚动窗口(比方说物品和技能窗口)都要关联帮助窗口,因此需要设置这个属性。
初始化也没什么特别的,多了2个内部的实变量@item_max和@colunm_max,分别是描绘项目的最大值和描绘列数的最大值。
继续向下看,里面有计算窗口项目位置的各种方法。包括开头行,一页的最大项目数,一页的最大行数,都非常简单,这里就不说了。
我们在这里看到,RMXP里面默认一行的高度是32,也就是说,上面的所有设置位置的方法,都是按照这个标准进行的。当然,这个功能并不完善,我们可以对Window_Selectable进行修改,让它能显示任意行高的项目。
然后就是对输入的处理和光标矩形的更新,这里简单看看就可以了,不能完全理解也没有关系,只要记住这是干什么用的就好了。另外,Window_Selectable里面并没有给出光标明灭变化的方法,那个是在Window中定义的,只是我们看不到而已。
下面我们以Window_Item为例,来看看滚动窗口该怎样制作。
class Window_Item < Window_Selectable
#--------------------------------------------------------------------------
# ● 初始化对像
#--------------------------------------------------------------------------
def initialize
super(0, 64, 640, 416)
@column_max = 2
refresh
self.index = 0
# 战斗中的情况下将窗口移至中央并将其半透明化
if $game_temp.in_battle
self.y = 64
self.height = 256
self.back_opacity = 160
end
end
#--------------------------------------------------------------------------
# ● 获取物品
#--------------------------------------------------------------------------
def item
return @data[self.index]
end
#--------------------------------------------------------------------------
# ● 刷新
#--------------------------------------------------------------------------
def refresh
if self.contents != nil
self.contents.dispose
self.contents = nil
end
@data = []
# 添加项目
for i in 1...$data_items.size
if $game_party.item_number(i) > 0
@data.push($data_items[i])
end
end
# 在战斗中以外添加武器、防具
unless $game_temp.in_battle
for i in 1...$data_weapons.size
if $game_party.weapon_number(i) > 0
@data.push($data_weapons[i])
end
end
for i in 1...$data_armors.size
if $game_party.armor_number(i) > 0
@data.push($data_armors[i])
end
end
end
# 如果项目数不是 0 就生成位图、重新描绘全部项目
@item_max = @data.size
if @item_max > 0
self.contents = Bitmap.new(width - 32, row_max * 32)
for i in 0...@item_max
draw_item(i)
end
end
end
#--------------------------------------------------------------------------
# ● 描绘项目
# index : 项目编号
#--------------------------------------------------------------------------
def draw_item(index)
item = @data[index]
case item
when RPG::Item
number = $game_party.item_number(item.id)
when RPG::Weapon
number = $game_party.weapon_number(item.id)
when RPG::Armor
number = $game_party.armor_number(item.id)
end
if item.is_a?(RPG::Item) and
$game_party.item_can_use?(item.id)
self.contents.font.color = normal_color
else
self.contents.font.color = disabled_color
end
x = 4 + index % 2 * (288 + 32)
y = index / 2 * 32
rect = Rect.new(x, y, self.width / @column_max - 32, 32)
self.contents.fill_rect(rect, Color.new(0, 0, 0, 0))
bitmap = RPG::Cache.icon(item.icon_name)
opacity = self.contents.font.color == normal_color ? 255 : 128
self.contents.blt(x, y + 4, bitmap, Rect.new(0, 0, 24, 24), opacity)
self.contents.draw_text(x + 28, y, 212, 32, item.name, 0)
self.contents.draw_text(x + 240, y, 16, 32, ":", 1)
self.contents.draw_text(x + 256, y, 24, 32, number.to_s, 2)
end
#--------------------------------------------------------------------------
# ● 刷新帮助文本
#--------------------------------------------------------------------------
def update_help
@help_window.set_text(self.item == nil ? "" : self.item.description)
end
end
class Window_Item < Window_Selectable
#--------------------------------------------------------------------------
# ● 初始化对像
#--------------------------------------------------------------------------
def initialize
super(0, 64, 640, 416)
@column_max = 2
refresh
self.index = 0
# 战斗中的情况下将窗口移至中央并将其半透明化
if $game_temp.in_battle
self.y = 64
self.height = 256
self.back_opacity = 160
end
end
#--------------------------------------------------------------------------
# ● 获取物品
#--------------------------------------------------------------------------
def item
return @data[self.index]
end
#--------------------------------------------------------------------------
# ● 刷新
#--------------------------------------------------------------------------
def refresh
if self.contents != nil
self.contents.dispose
self.contents = nil
end
@data = []
# 添加项目
for i in 1...$data_items.size
if $game_party.item_number(i) > 0
@data.push($data_items[i])
end
end
# 在战斗中以外添加武器、防具
unless $game_temp.in_battle
for i in 1...$data_weapons.size
if $game_party.weapon_number(i) > 0
@data.push($data_weapons[i])
end
end
for i in 1...$data_armors.size
if $game_party.armor_number(i) > 0
@data.push($data_armors[i])
end
end
end
# 如果项目数不是 0 就生成位图、重新描绘全部项目
@item_max = @data.size
if @item_max > 0
self.contents = Bitmap.new(width - 32, row_max * 32)
for i in 0...@item_max
draw_item(i)
end
end
end
#--------------------------------------------------------------------------
# ● 描绘项目
# index : 项目编号
#--------------------------------------------------------------------------
def draw_item(index)
item = @data[index]
case item
when RPG::Item
number = $game_party.item_number(item.id)
when RPG::Weapon
number = $game_party.weapon_number(item.id)
when RPG::Armor
number = $game_party.armor_number(item.id)
end
if item.is_a?(RPG::Item) and
$game_party.item_can_use?(item.id)
self.contents.font.color = normal_color
else
self.contents.font.color = disabled_color
end
x = 4 + index % 2 * (288 + 32)
y = index / 2 * 32
rect = Rect.new(x, y, self.width / @column_max - 32, 32)
self.contents.fill_rect(rect, Color.new(0, 0, 0, 0))
bitmap = RPG::Cache.icon(item.icon_name)
opacity = self.contents.font.color == normal_color ? 255 : 128
self.contents.blt(x, y + 4, bitmap, Rect.new(0, 0, 24, 24), opacity)
self.contents.draw_text(x + 28, y, 212, 32, item.name, 0)
self.contents.draw_text(x + 240, y, 16, 32, ":", 1)
self.contents.draw_text(x + 256, y, 24, 32, number.to_s, 2)
end
#--------------------------------------------------------------------------
# ● 刷新帮助文本
#--------------------------------------------------------------------------
def update_help
@help_window.set_text(self.item == nil ? "" : self.item.description)
end
end
这里的初始化大家应该都没问题了,然后在这里定义了一个方法item,返回当前光标所指的物品。这个方法是必要的,原因我们要到场景的地方再解释。最后就是refresh方法,用来描绘窗口内的所有内容。
refresh里面大概分为以下几个层次。
首先释放窗口内容,这样做的目的是节省内存。考虑到窗口内容可能比较多,用self.contents.clear反倒不如释放再重新生成来得直接。而且描绘的最大项目数也会随着队伍中物品的变化而变化,如果设置固定的contents大小,不便于后面的处理。
接下来,就是制作窗口内容显示数据,把要显示的物品,武器,防具添加在一个@data数组里面,这是窗口的内部信息,没必要公开化。
接下来就是描绘窗口内容,在窗口存在项目的时候,一个个描绘。
描绘单个物品的方法draw_item(i)在后面定义,i指的是物品在@data中的索引,里面的结构清晰直观,大家看看就可以了。里面那个case语句希望大家熟练使用,case语句可以对实例进行类的判定,有了这个机制,写代码会比较方便。
最后,定义刷新帮助文本的方法(Window_Selectable里面已经有说明)。
怎么样?现在你可以写出有滚动光标的窗口了。
在这里我们留下一个小练习
就是我们刚才提到的,Window_Selectable默认每一行高度是32,能否对其进行优化,让用户创建自定义行高的窗口,并且和原来的脚本不发生冲突?
4.2 一个滚动窗口的例子——真实商店
不知道大家对之前介绍游戏对象的创建还有没有印象,在这一章节里面,我们要把商店的货物在窗口中描绘出来。如果忘了前面的内容的话,还请翻看下前面的帖子哦。
4.2.1 准备工作
我们想想这个窗口要描绘出什么。肯定要描绘各种物品,它的剩余量和它的价格。我们利用Window_Selectable的滚动功能,来实现滚动处理。
4.2.2 Ruby代码
有了上面的准备我们就可以写出代码了。
class Window_Visual_ShopBuy < Window_Selectable
def initialize(shop_id)
super(0,128,368,480-128)
self.index = 0
@column_max = 1
@shop_current = $game_visual_shops[shop_id]
refresh
end
# 取得当前光标的物品
def item
return @data[self.index]
end
def refresh
# 内容被设置就释放,重新设置
if self.contents != nil
self.contents.dispose
self.contents = nil
end
# 设置各种项目数据
@data_items = []
@data_weapons = []
@data_armors = []
# 添加物品
for item_id in @shop_current.shop_goods_item.keys
@data_items.push($data_items[item_id])
end
# 排序按照物品ID
@data_items.sort!{|a,b| a.id – b.id}
# 添加武器
for weapon_id in @shop_current.shop_goods_weapon.keys
@data_weapon.push($data_weapons[item_id])
end
# 排序按照武器ID
@data_weapons.sort!{|a,b| a.id – b.id}
# 添加防具
for armor_id in @shop_current.shop_goods_armor.keys
@data_armors.push($data_armors[item_id])
end
# 排序按照物品ID
@data_armors.sort!{|a,b| a.id – b.id}
# 合并数组
@data = @data_items + @data_weapons + @data_armors
@item_max = @data.size
if @item_max > 0
self.contents = Bitmap.new(width - 32, row_max * 32)
for index in 0…@item_max
draw_item(index)
end
end
end
end
class Window_Visual_ShopBuy < Window_Selectable
def initialize(shop_id)
super(0,128,368,480-128)
self.index = 0
@column_max = 1
@shop_current = $game_visual_shops[shop_id]
refresh
end
# 取得当前光标的物品
def item
return @data[self.index]
end
def refresh
# 内容被设置就释放,重新设置
if self.contents != nil
self.contents.dispose
self.contents = nil
end
# 设置各种项目数据
@data_items = []
@data_weapons = []
@data_armors = []
# 添加物品
for item_id in @shop_current.shop_goods_item.keys
@data_items.push($data_items[item_id])
end
# 排序按照物品ID
@data_items.sort!{|a,b| a.id – b.id}
# 添加武器
for weapon_id in @shop_current.shop_goods_weapon.keys
@data_weapon.push($data_weapons[item_id])
end
# 排序按照武器ID
@data_weapons.sort!{|a,b| a.id – b.id}
# 添加防具
for armor_id in @shop_current.shop_goods_armor.keys
@data_armors.push($data_armors[item_id])
end
# 排序按照物品ID
@data_armors.sort!{|a,b| a.id – b.id}
# 合并数组
@data = @data_items + @data_weapons + @data_armors
@item_max = @data.size
if @item_max > 0
self.contents = Bitmap.new(width - 32, row_max * 32)
for index in 0…@item_max
draw_item(index)
end
end
end
end
由于窗口显示的是三种不同的物品,因此设置数据要花一些时间,但是基本思路跟描绘道具一样,因为商店里面的物品在变化,因此不要一开始就创建bitmap。
下面就是我们描绘物品的方法draw_item(index)了,注意,这个方法仍然定义在这个窗口内部,只不过是在这个帖子里面,写到外面了而已。
def draw_item(index)
item = @data[index]
# 取得持有数量和库存
case item
when RPG::Item
number = $game_party.item_number(item.id)
number_left = @current_shop.shop_goods_item[item.id]
when RPG::Weapon
number = $game_party.weapon_number(item.id)
number_left = @current_shop.shop_goods_weapon[item.id]
when RPG::Armor
number = $game_party.armor_number(item.id)
number_left = @current_shop.shop_goods_armor[item.id]
end
# 设置颜色
if item.price <= $game_party.gold and number < 99 and number_left > 0
self.contents.font.color = normal_color
else
self.contents.font.color = disabled_color
end
# 开始描绘
x = 4
y = index * 32
rect = Rect.new(x, y, self.width - 32, 32)
self.contents.fill_rect(rect, Color.new(0, 0, 0, 0))
bitmap = RPG::Cache.icon(item.icon_name)
opacity = self.contents.font.color == normal_color ? 255 : 128
self.contents.blt(x, y + 4, bitmap, Rect.new(0, 0, 24, 24), opacity)
self.contents.draw_text(x + 28, y, 168, 32, item.name, 0)
self.contents.draw_text(x + 196, y, 80, 32, item.price.to_s, 2)
self.contents.draw_text(x + 276, y, 48, 32, “剩:”)
self.contents.draw_text(x + 308, y, 32, 32, number_left.to_s, 2)
end
def draw_item(index)
item = @data[index]
# 取得持有数量和库存
case item
when RPG::Item
number = $game_party.item_number(item.id)
number_left = @current_shop.shop_goods_item[item.id]
when RPG::Weapon
number = $game_party.weapon_number(item.id)
number_left = @current_shop.shop_goods_weapon[item.id]
when RPG::Armor
number = $game_party.armor_number(item.id)
number_left = @current_shop.shop_goods_armor[item.id]
end
# 设置颜色
if item.price <= $game_party.gold and number < 99 and number_left > 0
self.contents.font.color = normal_color
else
self.contents.font.color = disabled_color
end
# 开始描绘
x = 4
y = index * 32
rect = Rect.new(x, y, self.width - 32, 32)
self.contents.fill_rect(rect, Color.new(0, 0, 0, 0))
bitmap = RPG::Cache.icon(item.icon_name)
opacity = self.contents.font.color == normal_color ? 255 : 128
self.contents.blt(x, y + 4, bitmap, Rect.new(0, 0, 24, 24), opacity)
self.contents.draw_text(x + 28, y, 168, 32, item.name, 0)
self.contents.draw_text(x + 196, y, 80, 32, item.price.to_s, 2)
self.contents.draw_text(x + 276, y, 48, 32, “剩:”)
self.contents.draw_text(x + 308, y, 32, 32, number_left.to_s, 2)
end
最后别忘了定义update_help,这样就大功告成了。不过,我们暂时无法检验它的对错,有兴趣的朋友可以利用场景来检验一下。
上图就是我们期待的效果,不过我们做的仅仅是左下角这个窗口哦,而且各种命令的处理还没有设定。
窗口的学习我们告一段落,接下来我们将要学习场景的使用。大家就敬请期待吧。
作者: RyanBern 时间: 2013-10-29 17:51
本帖最后由 RyanBern 于 2015-8-24 11:57 编辑
[box=RoyalBlue]第5章节:场景的使用(一)
[/box]
场景的内容我们拆开来讲,第5章节主要说场景实现的一般过程,解读一些简单场景。第6章节我们主要是DIY一个我们自己想要的场景,并解读一个比较复杂的场景。
5.1 精灵的使用
这个本来应该在上一章节就应该讲到的,但是写上一章的时候一时脑抽,忘记了,因此在这里补上吧。
5.1.1 精灵是什么
所谓精灵(Sprite),就是一种能在屏幕上显示各种可见对象的类。例如我们上一章节提到的窗口,还有游戏地图显示的实现,都是靠精灵完成的。另外,拥有动画播放效果的精灵是RPG模块下的Sprite类,也是精灵的一种。在这里,我们只需要学习一下精灵的最基本使用方法就可以了,相信大家一定会举一反三的。
5.1.2 精灵的属性和方法
既然是一种特殊的类,那我们有必要了解精灵的使用方法。翻开F1,输入 Sprite 搜素,我们便看到了精灵(Sprite)的原型。
首先是类方法,生成一个新的精灵Sprite.new([viewport]),这里需要指定生成的视口Viewport,在第四章我们已经比较详细讲过视口的含义,和视口不同造成的显示区别。如果忘记了还请翻看19L的帖子哦。当然,这里viewport可以省略,这就默认精灵将直接显示在640*480的游戏窗口中。
接下来是各种属性和方法:
- dispose:方法,释放精灵,这个跟释放窗口的作用一样,如果一个精灵你不再使用它,那么需要把它从内存中释放。
- bitmap:属性,作为精灵所显示的位图。注意,这个位图只是作为精灵显示的数据,实际显示在屏幕上的可能和这个位图略有不同。当然,在释放精灵之前,你有可能还要释放这个位图。释放的顺序则是先释放这个位图,再释放精灵本身。如果这个位图是利用Bitmap.new生成,则释放精灵之前,必须释放此位图;如果这个位图是利用RPG::Cache模块读取高速缓存,则可不必释放此位图。释放位图+精灵的代码一般写成这样:
sprite.bitmap.dispose
sprite.dispose
sprite.bitmap.dispose
sprite.dispose
- src_rect:属性,传送位图的矩形,这个矩形的作用相当于“截取”原来位图的一部分,然后用精灵去显示。
- visible:属性,精灵是否可见,如果想隐藏一个精灵而不是去释放它,请将visible置为false
- ox,oy:属性,精灵原点坐标,我们可以把精灵占据的屏幕空间看作是一个矩形,默认这个原点在矩形的左上角。进行坐标变换和旋转的时候,需要参考这个原点。
- zoom_x,zoom_y:属性,横向和纵向拉伸比例,浮点数。
- angle:属性,表示逆时针旋转的角度,单位是度。
我们可以用下面几个图片来表示精灵的各种属性:
5.1.3 精灵的具体使用
了解了精灵的各种属性,现在我们可以使用它了。
我们要在屏幕上显示各种对象,需要借助Graphics模块。我们不必掌握Graphics的具体内容,在每个场景脚本中,已经在main方法中含有对Graphics模块的方法调用,在这里我们只需照葫芦画瓢就可以了。
核心的方法,是通过Graphics.update完成的,我们翻开Scene类的脚本,也可以看到这一句。
在Graphics.update之前,我们必须先生成一个精灵,然后Graphics会自动把它算在画面窗口里面显示的东西,然后通过调用update就可以显示出来了(同理,窗口对象也是,因为窗口本身就由大量精灵组成)。
简单显示一个图片的代码如下:
a = Sprite.new
a.bitmap = RPG::Cache.picture("123.png") # 这里输入Graphics/Pictures下文件名
loop do
Graphics.update
end
a.dispose # 跳出循环后释放,注意,这里利用RPG::Cache载入位图,可不必释放bitmap
a = Sprite.new
a.bitmap = RPG::Cache.picture("123.png") # 这里输入Graphics/Pictures下文件名
loop do
Graphics.update
end
a.dispose # 跳出循环后释放,注意,这里利用RPG::Cache载入位图,可不必释放bitmap
总结下过程就是设置——显示——释放,是不是很简单?
5.2 场景的简单使用
下面我们就要来学习场景的使用了,这也是最重要的技术之一,希望大家能好好掌握。所谓场景,就是把一些游戏对象(如窗口,精灵)组合起来的一个综合画面,每一个场景都可以看作是一个小系统。可以反馈信息,接收信息,跟玩家互动。下面我们就来具体解读一下。
5.2.1 场景实现的一般步骤
我们翻开任意一个场景脚本,都会发现开头有相似之处。先定义了main方法,有的是要定义initialize方法(有些则没有)。这个main方法是必要的,因为在前面我已经说过,Main组脚本就是不断调用场景的main方法来进行游戏的。而initialize是初始化场景类的某些具体的信息,而不是进行主处理,这点要记住。
一般的场景main方法主要分3部分。
设置(包括初始化)——(画面)刷新,更新——退出后的处理。
以Scene_Title为例,下面是它的main方法。
#--------------------------------------------------------------------------
# ● 主处理
#--------------------------------------------------------------------------
def main
# 战斗测试的情况下
if $BTEST
battle_test
return
end
# 载入数据库
$data_actors = load_data("Data/Actors.rxdata")
$data_classes = load_data("Data/Classes.rxdata")
$data_skills = load_data("Data/Skills.rxdata")
$data_items = load_data("Data/Items.rxdata")
$data_weapons = load_data("Data/Weapons.rxdata")
$data_armors = load_data("Data/Armors.rxdata")
$data_enemies = load_data("Data/Enemies.rxdata")
$data_troops = load_data("Data/Troops.rxdata")
$data_states = load_data("Data/States.rxdata")
$data_animations = load_data("Data/Animations.rxdata")
$data_tilesets = load_data("Data/Tilesets.rxdata")
$data_common_events = load_data("Data/CommonEvents.rxdata")
$data_system = load_data("Data/System.rxdata")
# 生成系统对像
$game_system = Game_System.new
# 生成标题图形
@sprite = Sprite.new
@sprite.bitmap = RPG::Cache.title($data_system.title_name)
# 生成命令窗口
s1 = "新游戏"
s2 = "继续"
s3 = "退出"
@command_window = Window_Command.new(192, [s1, s2, s3])
@command_window.back_opacity = 160
@command_window.x = 320 - @command_window.width / 2
@command_window.y = 288
# 判定继续的有效性
# 存档文件一个也不存在的时候也调查
# 有效为 @continue_enabled 为 true、无效为 false
@continue_enabled = false
for i in 0..3
if FileTest.exist?("Save#{i+1}.rxdata")
@continue_enabled = true
end
end
# 继续为有效的情况下、光标停止在继续上
# 无效的情况下、继续的文字显示为灰色
if @continue_enabled
@command_window.index = 1
else
@command_window.disable_item(1)
end
# 演奏标题 BGM
$game_system.bgm_play($data_system.title_bgm)
# 停止演奏 ME、BGS
Audio.me_stop
Audio.bgs_stop
# 执行过渡
Graphics.transition
# 主循环
loop do
# 刷新游戏画面
Graphics.update
# 刷新输入信息
Input.update
# 刷新画面
update
# 如果画面被切换就中断循环
if $scene != self
break
end
end
# 装备过渡
Graphics.freeze
# 释放命令窗口
@command_window.dispose
# 释放标题图形
@sprite.bitmap.dispose
@sprite.dispose
end
#--------------------------------------------------------------------------
# ● 主处理
#--------------------------------------------------------------------------
def main
# 战斗测试的情况下
if $BTEST
battle_test
return
end
# 载入数据库
$data_actors = load_data("Data/Actors.rxdata")
$data_classes = load_data("Data/Classes.rxdata")
$data_skills = load_data("Data/Skills.rxdata")
$data_items = load_data("Data/Items.rxdata")
$data_weapons = load_data("Data/Weapons.rxdata")
$data_armors = load_data("Data/Armors.rxdata")
$data_enemies = load_data("Data/Enemies.rxdata")
$data_troops = load_data("Data/Troops.rxdata")
$data_states = load_data("Data/States.rxdata")
$data_animations = load_data("Data/Animations.rxdata")
$data_tilesets = load_data("Data/Tilesets.rxdata")
$data_common_events = load_data("Data/CommonEvents.rxdata")
$data_system = load_data("Data/System.rxdata")
# 生成系统对像
$game_system = Game_System.new
# 生成标题图形
@sprite = Sprite.new
@sprite.bitmap = RPG::Cache.title($data_system.title_name)
# 生成命令窗口
s1 = "新游戏"
s2 = "继续"
s3 = "退出"
@command_window = Window_Command.new(192, [s1, s2, s3])
@command_window.back_opacity = 160
@command_window.x = 320 - @command_window.width / 2
@command_window.y = 288
# 判定继续的有效性
# 存档文件一个也不存在的时候也调查
# 有效为 @continue_enabled 为 true、无效为 false
@continue_enabled = false
for i in 0..3
if FileTest.exist?("Save#{i+1}.rxdata")
@continue_enabled = true
end
end
# 继续为有效的情况下、光标停止在继续上
# 无效的情况下、继续的文字显示为灰色
if @continue_enabled
@command_window.index = 1
else
@command_window.disable_item(1)
end
# 演奏标题 BGM
$game_system.bgm_play($data_system.title_bgm)
# 停止演奏 ME、BGS
Audio.me_stop
Audio.bgs_stop
# 执行过渡
Graphics.transition
# 主循环
loop do
# 刷新游戏画面
Graphics.update
# 刷新输入信息
Input.update
# 刷新画面
update
# 如果画面被切换就中断循环
if $scene != self
break
end
end
# 装备过渡
Graphics.freeze
# 释放命令窗口
@command_window.dispose
# 释放标题图形
@sprite.bitmap.dispose
@sprite.dispose
end
我们看到,在注释“准备过渡”之前,进行的都是一些设置的工作,比如载入数据库,生成各种图片和窗口,在设置完毕之后,准备过渡到画面状态(此时画面上什么也没有),然后接着是一个无限循环loop do,按照一定顺序刷新各种东西,在这里我们看到了Graphics.update,刷新游戏画面,然后是Input.update,这里Input也是内部模块,用来处理输入用的,最后是一个单独的update,这个其实是该场景内部的刷新方法,需要我们在后面进行追加定义。刷新完毕后,判断场景是否已经被切换,即$scene变量是否在update之后发生改变。如果改变,退出无限循环,场景结束,进行各种事后处理(各种释放和过渡)。
在这里,可能有些人注意到了,虽然这里的Sprite的位图是用RPG::Cache模块载入的,但是在释放精灵之前,仍然对此位图进行了释放操作。这似乎和我前面提到的释放原则不一致。在这里,我要仔细说明一下使用RPG::Cache模块的原则。首先,RPG::Cache模块存在的意义,就是将图片载入内存中,如果该图片需要重复被使用(或者被多个Sprite对象使用),那么将其放入缓存中可以节约内存,提高载入速度,所以RPG::Cache中的位图是可以不释放的。而对于一些使用频率较少的位图,则不必将其放入RPG::Cache中,或者是使用之后及时释放,以腾出更多空间来存储使用率更大的位图。
整个过程清晰明了,需要我们做的,就是丰富这个骨架的内容。
我们注意中间那个loop do的无限循环,原则上是1帧执行1次,因此里面放的方法都是定期需要重复执行的方法,这种方法通常我们命名为update(注意不是refresh),也就是说,放到这里的东西,都应该是我们需要反复刷新的,如果不需要反复刷新,则一般不要单独放进去(可以放到场景私有的update方法里,然后再进行判断)。
5.2.2 场景的具体实现
接下来我们就可以解读一下场景的具体实现了。我们以Scene_Item为例,来具体说明一下。
#--------------------------------------------------------------------------
# ● 主处理
#--------------------------------------------------------------------------
def main
# 生成帮助窗口、物品窗口
@help_window = Window_Help.new
@item_window = Window_Item.new
# 关联帮助窗口
@item_window.help_window = @help_window
# 生成目标窗口 (设置为不可见・不活动)
@target_window = Window_Target.new
@target_window.visible = false
@target_window.active = false
# 执行过度
Graphics.transition
# 主循环
loop do
# 刷新游戏画面
Graphics.update
# 刷新输入信息
Input.update
# 刷新画面
update
# 如果画面切换就中断循环
if $scene != self
break
end
end
# 装备过渡
Graphics.freeze
# 释放窗口
@help_window.dispose
@item_window.dispose
@target_window.dispose
end
#--------------------------------------------------------------------------
# ● 主处理
#--------------------------------------------------------------------------
def main
# 生成帮助窗口、物品窗口
@help_window = Window_Help.new
@item_window = Window_Item.new
# 关联帮助窗口
@item_window.help_window = @help_window
# 生成目标窗口 (设置为不可见・不活动)
@target_window = Window_Target.new
@target_window.visible = false
@target_window.active = false
# 执行过度
Graphics.transition
# 主循环
loop do
# 刷新游戏画面
Graphics.update
# 刷新输入信息
Input.update
# 刷新画面
update
# 如果画面切换就中断循环
if $scene != self
break
end
end
# 装备过渡
Graphics.freeze
# 释放窗口
@help_window.dispose
@item_window.dispose
@target_window.dispose
end
上面是Scene_Item的main方法,主要生成了3个窗口,帮助窗口,物品窗口,目标窗口。而最初,目标窗口是不可见的(因为你还没使用某个道具),而帮助窗口和物品窗口是互相关联的,必须设置好。然后就可以过渡了,整个过程非常清晰。
def update
# 刷新窗口
@help_window.update
@item_window.update
@target_window.update
# 物品窗口被激活的情况下: 调用 update_item
if @item_window.active
update_item
return
end
# 目标窗口被激活的情况下: 调用 update_target
if @target_window.active
update_target
return
end
end
def update
# 刷新窗口
@help_window.update
@item_window.update
@target_window.update
# 物品窗口被激活的情况下: 调用 update_item
if @item_window.active
update_item
return
end
# 目标窗口被激活的情况下: 调用 update_target
if @target_window.active
update_target
return
end
end
这就是我们在上面说的,场景私有update方法的定义,这个方法定义非常重要,这决定了你的场景是否能和玩家互动。
一般来说,update方法分为2部分,一是自动刷新,即无论玩家有没有操作,都必须刷新的对象;二是条件刷新,当玩家有一定操作时,进行刷新相应对象。执行的顺序是先自动刷新,再条件刷新,这个顺序不能变。我们在这里可以设置一个输入等待的机制,必须等待固定时间,才能接受玩家的输入(注意,自动刷新是一直进行的)。这种情况出现的时候有很多,比方说玩家按了一个按键,画面进行某种变换,变化持续时间是1秒(40帧),你不希望在画面变化的时候接受玩家的其他输入,这时候就有必要设置输入等待了。比方说 这里,我们先要在main方法内设置一个内部变量@wait_count,并初始化为0,表示等待计数的时间。然后在update的条件刷新之前,放上以下代码:
if @wait_count > 0
@wait_count -= 1
return
end
if @wait_count > 0
@wait_count -= 1
return
end
这就表示在等待时间不为0的情况下,将等待时间减去1,注意,update方法是1帧调用一次,@wait_count等于多少,就意味着要等待多少帧。随后立即结束update方法,注意,那个return不能少,这个return是避免update进行条件更新的,因此在@wait_count大于0的情况下,系统无法接受玩家的输入。这个问题就被我们解决了。
而在这里,条件刷新有两个地方,如果物品窗口被激活就调用update_item方法,如果目标窗口被激活就调用update_target方法。这个必须分开设置,因为不同状态下刷新的规则肯定是不一样的。另外还需要注意每一个分支下面,要有return,否则很可能出现在同一次update下进行两种以上更新的情况。
#--------------------------------------------------------------------------
# ● 刷新画面 (物品窗口被激活的情况下)
#--------------------------------------------------------------------------
def update_item
# 按下 B 键的情况下
if Input.trigger?(Input::B)
# 演奏取消 SE
$game_system.se_play($data_system.cancel_se)
# 切换到菜单画面
$scene = Scene_Menu.new(0)
return
end
# 按下 C 键的情况下
if Input.trigger?(Input::C)
# 获取物品窗口当前选中的物品数据
@item = @item_window.item
# 不使用物品的情况下
unless @item.is_a?(RPG::Item)
# 演奏冻结 SE
$game_system.se_play($data_system.buzzer_se)
return
end
# 不能使用的情况下
unless $game_party.item_can_use?(@item.id)
# 演奏冻结 SE
$game_system.se_play($data_system.buzzer_se)
return
end
# 演奏确定 SE
$game_system.se_play($data_system.decision_se)
# 效果范围是我方的情况下
if @item.scope >= 3
# 激活目标窗口
@item_window.active = false
@target_window.x = (@item_window.index + 1) % 2 * 304
@target_window.visible = true
@target_window.active = true
# 设置效果范围 (单体/全体) 的对应光标位置
if @item.scope == 4 || @item.scope == 6
@target_window.index = -1
else
@target_window.index = 0
end
# 效果在我方以外的情况下
else
# 公共事件 ID 有效的情况下
if @item.common_event_id > 0
# 预约调用公共事件
$game_temp.common_event_id = @item.common_event_id
# 演奏物品使用时的 SE
$game_system.se_play(@item.menu_se)
# 消耗品的情况下
if @item.consumable
# 使用的物品数减 1
$game_party.lose_item(@item.id, 1)
# 再描绘物品窗口的项目
@item_window.draw_item(@item_window.index)
end
# 切换到地图画面
$scene = Scene_Map.new
return
end
end
return
end
end
#--------------------------------------------------------------------------
# ● 刷新画面 (物品窗口被激活的情况下)
#--------------------------------------------------------------------------
def update_item
# 按下 B 键的情况下
if Input.trigger?(Input::B)
# 演奏取消 SE
$game_system.se_play($data_system.cancel_se)
# 切换到菜单画面
$scene = Scene_Menu.new(0)
return
end
# 按下 C 键的情况下
if Input.trigger?(Input::C)
# 获取物品窗口当前选中的物品数据
@item = @item_window.item
# 不使用物品的情况下
unless @item.is_a?(RPG::Item)
# 演奏冻结 SE
$game_system.se_play($data_system.buzzer_se)
return
end
# 不能使用的情况下
unless $game_party.item_can_use?(@item.id)
# 演奏冻结 SE
$game_system.se_play($data_system.buzzer_se)
return
end
# 演奏确定 SE
$game_system.se_play($data_system.decision_se)
# 效果范围是我方的情况下
if @item.scope >= 3
# 激活目标窗口
@item_window.active = false
@target_window.x = (@item_window.index + 1) % 2 * 304
@target_window.visible = true
@target_window.active = true
# 设置效果范围 (单体/全体) 的对应光标位置
if @item.scope == 4 || @item.scope == 6
@target_window.index = -1
else
@target_window.index = 0
end
# 效果在我方以外的情况下
else
# 公共事件 ID 有效的情况下
if @item.common_event_id > 0
# 预约调用公共事件
$game_temp.common_event_id = @item.common_event_id
# 演奏物品使用时的 SE
$game_system.se_play(@item.menu_se)
# 消耗品的情况下
if @item.consumable
# 使用的物品数减 1
$game_party.lose_item(@item.id, 1)
# 再描绘物品窗口的项目
@item_window.draw_item(@item_window.index)
end
# 切换到地图画面
$scene = Scene_Map.new
return
end
end
return
end
end
在这里我们终于看到了对各种输入的处理,知道了物品的使用效果和消耗,都是在update及其子方法里面实现的。注意,Window_Selectable中光标的移动不在此中,Window_Selectable内部的update上。这里大家粗略看一下就能明白大意,我就不多做介绍了。在这里注意refresh方法的调用,我们调用refresh方法,只是在窗口需要刷新的时候才进行调用。如果窗口需要刷新的原因是我们输入了某种指令,那么在指令的最后,一定要重新刷新窗口,否则一般不进行窗口内容再描绘的处理(因为这样太消耗时间了)。
最后我们说一下带initialize方法的场景类。Scene类可以不用带initialize方法,但是在某些场合下,我们需要设置initialize方法。
比方说Scene_Menu场景,就有initialize方法,这里的initialize方法非常简单。
def initialize(menu_index)
@menu_index = menu_index
end
def initialize(menu_index)
@menu_index = menu_index
end
这是设置菜单初始光标位置,考虑到从不同场景退回Scene_Menu,菜单光标位置不同,才考虑加的这个内部变量。比方说从Scene_Item返回,要写$scene = Scene_Menu.new(0),这是因为物品选项在菜单的第一个位置上。其余的同理。
场景类的基本知识就到这里,在第6章节,我们要亲自DIY一个场景,并解读一些复杂场景,大家敬请期待吧。
作者: RyanBern 时间: 2013-11-15 09:43
本帖最后由 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多年的体会,大家就敬请期待吧。
作者: RyanBern 时间: 2013-11-16 10:14
本帖最后由 RyanBern 于 2015-8-24 12:13 编辑
[box=RoyalBlue]第7章节:尾声
[/box]
呼~写了这么久,这个脚本教程贴总算要杀青了。不过打开脚本编辑器,我们还有一些部分没有说到,比如说Interpreter类的脚本,我们就没深入讨论。不过,这对于一般的需要来说,已经足够了。那么在最后一章,我们要说一些零碎的内容。说是零碎,其实是编程的死角,这也是一个脚本党的必备知识。
7.1 个人的几个体会
7.1.1 关于alias
这个词我们在很久之前就已经提到了,但是一直没有说它的用法,现在就补上吧。alias的意思是“别名”,在这里是给函数取别名。具体的使用方法是:alias 新方法名 旧方法名,新方法名和旧方法名用空格隔开。那么这个功能有什么用呢?主要是利用在方法的重定义上。当我们要重新定义一个方法,又不想覆盖原来的方法,那么alias就派上用场了。请看下面的例子:class Person
attr_accessor :name
def initialize(name)
@name = name
end
def hello
print "我是" + @name
end
alias old_hello hello
def hello
print “Hello, I am ” + @name
end
end
# 测试一下
ryan = Person.new("Ryan")
ryan.hello
ryan.old_hello
class Person
attr_accessor :name
def initialize(name)
@name = name
end
def hello
print "我是" + @name
end
alias old_hello hello
def hello
print “Hello, I am ” + @name
end
end
# 测试一下
ryan = Person.new("Ryan")
ryan.hello
ryan.old_hello
如果按照上述定义,第一个语句会在屏幕上显示“Hello, I am Ryan”,第二个语句会在屏幕上显示“我是Ryan”,可见alias具有保留原有方法的功能。
在这里,我们看到alias的威力还不算很大。这个语句一般用作整合脚本上,特别是给不会脚本的人写脚本时,我们要写出一个完整的脚本,让用户贴在Main前面即可,但是有时候并不是这样,假设我们要修改前面默认脚本的内容,如果不用alias,那么只能进行重定义。但是对于一些方法,我们可以这样:
假如你给Game_Temp增加一个名叫test_value的属性,并在initialize中将它初始化,我们可以用三种方法。一是直接在Game_Temp上进行修改,不过,如果写脚本给伸手党,那就不太好,二是把整个Game_Temp需要重定义的地方定义一遍,对于Game_Temp这样的脚本来说,重定义一遍无异于整体复制,脚本显得很不整洁,三就是利用alias,具体实现过程如下:
class Game_Temp
attr_accessor :test_value
alias old_initialize initialize
def initialize
@test_value = 0
old_initialize
end
end
class Game_Temp
attr_accessor :test_value
alias old_initialize initialize
def initialize
@test_value = 0
old_initialize
end
end
在这里,我们是先对initialize进行别名,然后重定义,在定义过程中,调用了原方法。因为这里的initialize做的工作就是赋值初始化,因此这样写是没有问题的。
不过,需要注意的是,不能给同一个方法起两个相同的别名,这样说似乎比较别扭,我们来看下面的例子:
假如上面的代码已经定义好,那么我现在继续往Game_Temp里面增加一个叫test_bool的属性(Game_Temp中已经有了我们刚刚增加的test_value,利用的也是alias),由于我们现在的initialize的功能已经能够初始化test_value,因此我们利用alias时,进行重定义的肯定是initialize方法而非old_initialize方法,但是我们不能写下面的:
class Game_Temp
attr_accessor :test_bool
alias old_initialize initialize
def initialize
@test_bool = false
old_initialize
end
end
class Game_Temp
attr_accessor :test_bool
alias old_initialize initialize
def initialize
@test_bool = false
old_initialize
end
end
这样写的话,脚本肯定会出错,因为你又将initialize别名为old_initialize,而刚才那个名字已经有了对应的方法,这样就会出现一个符号表示两个方法的情况,引发内部冲突。因此这个时候就不能用old_initialize做别名了,应该换成别的。
7.1.2 关于类变量@@xxxx
类变量我们在讲类的时候顺便也提了一句,这种变量我们一般不会用到,不过好歹也说一下吧。类变量和通常我们讲的类当中的实例变量不同,类变量作用于整个类的上面。作为一个类生成的实例,这个实例本身可以访问该类的类变量。这样说有些模糊,我们看下面的例子:
class A
# 定义类变量 @@a
@@a = 1
def initialize
# 定义实例变量 @a
@a = 1
end
def a
return a
end
def a=(a)
@a = a
end
def ab
return @@a
end
def ab=(ab)
@@a = ab
end
end
a1 = A.new
a2 = A.new
a1.a = 2
a2.a = 3
a1.ab = 5
print a1.a # => 2
print a2.a # => 3
print a1.ab # => 5
print a2.ab # => 5
class A
# 定义类变量 @@a
@@a = 1
def initialize
# 定义实例变量 @a
@a = 1
end
def a
return a
end
def a=(a)
@a = a
end
def ab
return @@a
end
def ab=(ab)
@@a = ab
end
end
a1 = A.new
a2 = A.new
a1.a = 2
a2.a = 3
a1.ab = 5
print a1.a # => 2
print a2.a # => 3
print a1.ab # => 5
print a2.ab # => 5
在这里,@a表示我们通常用到的实例变量,@@a表示类变量,在这里我们看到,@a这个变量是每个实例私有的变量,而@@a这个变量是两个实例公用的变量。因此我们得到结论,只要是属于同一个类的同一个类变量,无论用这个类的哪个实例修改它,都会有同样的效果,无论用这个类的哪一个实例访问它,都会得到同样的值。
值得注意的是,类本身,也可以对类变量进行修改,不过要来借助类方法来完成。类方法和模块方法比较类似,定义和使用的模式基本相同。
class A
# 定义类变量 @@var
@@var = 0
# 定义类方法
def self.var
return @@var
end
def self.var=(var)
@@var = var
end
end
A.var = 5
p A.var # => 5
class A
# 定义类变量 @@var
@@var = 0
# 定义类方法
def self.var
return @@var
end
def self.var=(var)
@@var = var
end
end
A.var = 5
p A.var # => 5
7.1.3 相同和相等•clone•变量和指针
这个也是我们经常说的一个问题,Ruby是以C语言为基础语言编写的,实际上是对C做的一个优化。我们都知道C语言中有指针这个东西,而Ruby中我们却看不到它。不是说Ruby中没有指针,而是Ruby已经采取某种方式将指针优化掉了。我们在编写程序的同时,就在使用大量的指针,只是我们从未发觉而已。为什么又说起这个事情来呢,这是源于本人最近的一道作业题,我嫌用C太复杂,于是就用Ruby了,但是有一个地方怎么也通不过,一时想破脑袋也没想明白。不过后来好在经过F1指点,终于明白了问题所在,接下来我们就看一下。
我们在前面说了,赋值运算符“=”的作用是将右边的值赋给左边,传递过去的就是对象的引用。所以,当你把一个数组或者一个类的实例赋给一个变量,那么实际传递过去的就是变量的引用,如果你把这个值又赋给另外一个变量,那么依然是传递引用,对一个变量进行的操作必定会影响另一个。因此,有时候我们需要生成一模一样的一个对象,又不想让它们有任何关系,就要用到clone方法,这是Object类(最大的类)的方法,任何类的实例都可以调用,因此,写b = a.clone,就能把a和b区别开来,而且他们的内容也完全一样。我们知道利用比较运算符“==”可以判断两个对象是否相等,而这个运算符只能判断两个对象的值是否相等,例如,有b = a.clone,然后判断b == a,这个结果通常是true,不过,a和b并不相同,在Object类里面也有一个判断是否相同的方法equal?,如果执行b.equal?(a),我们得到的结果将会是false,因为equal?是判断二者是否相同的方法,在这里,a和b仅仅是内容相同,而它们实际上是两个“变量”(即内存的地址不同,这并不稀奇,就好比你和你的双生同胞不是同一个人一样)。
但是,即使是这样,也会遇到一些费解的问题。例如,有下面的定义:
class A
attr_accessor :data
def initialize
@data = []
end
end
x1 = A.new
x1.data[0] = 0
x2 = x1.clone
x2.data[0] = 1
print x1.data[0]
class A
attr_accessor :data
def initialize
@data = []
end
end
x1 = A.new
x1.data[0] = 0
x2 = x1.clone
x2.data[0] = 1
print x1.data[0]
在这里,我们看到屏幕上显示的是1,这个最初看起来是非常奇怪的事情。我们明明对x1做了复制,按理说x1和x2的应该只是内容一样而已,为什么对x2的修改也影响到x1呢?这里我们注意的是,clone进行的只是浅层次的复制,它只能复制对象里面所有变量的内容,而不能复制变量引用的内容。我们知道,一个变量如果表示数组,那实际上就是指向数组的引用,在这里@data就是一个数组,表示的就是数组的引用,而clone做复制时,仅仅把这个引用的值复制了过去,因此它们表示的还是同一个东西。除非你将x2的data重新定向,否则在x2的data上面的改动还是会影响x1。
7.1.4 sprintf表达式
说实话,学C语言的人在Ruby里面看到这个,应该感到无比亲切吧。这可以说是为数不多被保留下来的语句啊。sprintf这个名字蛮奇怪的,print是“打印、输出”的意思,那么后面的f,缩写的是format,表示“格式”,连在一起就表示“格式输出”,当然printf也是C的基本函数之一,前面的s,是表示输出的去向,s缩写的是string,表示这个函数将一定的内容输出到一个字符串内。在C语言中,sprintf的第一个参数是表示接受输入的字符串(字符数组指针),但是在Ruby里面,这个字符串直接作为sprintf的返回值。具体使用方法,如果学过C了,肯定都知道,如果不清楚就F1看看,其中“%”表示格式转换描述,例如写a = sprintf("%d %d", x1, x2),就是把x1和x2的值分别替换两个%d,然后做成字符串送到a中,我们注意%d是整数转换描述,如果x1表示的不是整数,那么就会出错的,因此,格式输出一定要严格控制,千万不能出现转换错误。
7.2 写在最后
这个RGSS1教程帖就这样结束了,不知道能坚持看到最后的你们,能不能有所收获呢?我不敢期望这个教程的效果有多么强大,但是,只要它能为大家写游戏做出一丝一毫的贡献,我就知足了。
每次写完一章,我都很期待大家的回帖,不过从第四章开始,这个帖子基本没有什么回帖的了,于是我就一直连帖下去,一直连帖,直到最后我发现大家似乎已经没有心情看下去了。我想这也是我个人方面的原因,第5章和第6章中间间隔将近一个月,或者说是我才疏学浅,但总之,我写这个东西的目的就是让大家能学会分拆脚本,根据默认脚本或者其他大神的作品的构造来提炼出自己的东西。
很多人看到脚本教程,给出的评价,大多都是“变量计算、类那里还行,到后面就什么也看不懂了”、“看完教程后就学会了一个p函数,别的都没学会”,我其实还满希望我的教程能摆脱这个评价,但是现在想想,或许这个教程帖也难逃此厄运。毕竟,如果没有经历系统的学习,没有理解计算机工作的原理,只是走马观花地看教程,恐怕也没什么效果。这样的教程写出来,结果往往都是,会脚本的人更厉害了,不会脚本的人依然没学到什么……总之,虽然截稿了,但是很郁闷。
6R站上已经有了很多RMXP的经典教程,而且侧重点各有不同。推荐大家在阅读教程帖子的时候,在多个教程之间比对,发现其中的不同,从而提出问题。毕竟所有的教程都会有疏漏和错误,写出一个完美的教程也实在是太过于困难。
最后,建议想要学习脚本的同学,一定要多看,多提问,多练习,少伸手。学习的初期可能做不出自己想要的东西,这没有关系,学习脚本切忌急功近利,心急往往很容易冲淡学习脚本的热情。起初建议大家模仿默认脚本来修改和练习,慢慢磨合,今后使用脚本才能更加得心应手。
作者: 芯☆淡茹水 时间: 2013-11-16 18:06
本帖最后由 芯☆淡茹水 于 2013-11-16 18:10 编辑
在一篇 Ruby 语言教学里看到的,觉得很经典。
Ruby 的理念是:一切都是对象。包括:数值,字符串,数组,哈希,类。
其中讲到 类 的概念,觉得很形象。
比如:
@SEX @age @height- #==============================================================================
- # 定义一个“人”的类(概念),也就是说,在大家的脑海里,“人”是怎样怎样的。
- # “人”有名字;有性别;有身高;年龄,,等,这些是属于一个“人”的参数。
- # “人”会说话;会行走;会吃饭,,,等,这些就属于是方法。
- #==============================================================================
- class Person
- #------------------------------------------------------------------------
- # 定义一些参数,除了 “性别” 只能读取不可改变外,其它都能变动,比如“姓名”,
- # “年龄”,,,,。当然,一个“人”的参数是很多的,这里只列举一部分。
- #------------------------------------------------------------------------
- attr_accessor :name # 姓名
- attr_reader :sex # 性别
- attr_accessor :age # 年龄
- attr_accessor :height # 身高
- #-------------------------------------------------------------------------
- # 初始化。生成一个新的“人”时,需要指定生成的这个“人”的一些参数并代入。
- #-------------------------------------------------------------------------
- def initialize(name, sex, age, height)
- @name = name
- [url=home.php?mod=space&uid=103045]@SEX[/url] = sex
- [url=home.php?mod=space&uid=6132]@age[/url] = age
- [url=home.php?mod=space&uid=291977]@height[/url] = height
- end
- #--------------------------------------------------------------------------
- # 定义一个最简单的“说话”方法,比如:自我介绍。
- #--------------------------------------------------------------------------
- def talk
- return "我的名字叫:" + @name + ",性别:" + @sex + ",年龄:" + @age.to_s + "岁,身高:" + @height.to_s + "cm。"
- end
- end
- #===============================================================================
- #===============================================================================
- # 你是游戏的制作者,你就是这个游戏的上帝。现在上帝要创造一个新的“人”,
- # 名叫:RyanBern,性别:男,年龄:18,身高:175cm。首先用一个变量代入并表示
- # 这个人,比如用:rb 。以后要指定这个人,都用 rb 来表示。
- #===============================================================================
- rb = Person.new("RyanBern", "男", 18, 175)
- #------------------------------
- # 接下来显示和他的谈话。
- #------------------------------
- p rb.talk #“我的名字叫:RyanBern,性别:男,年龄:18岁,身高:175cm。”
- #==============================================================================
复制代码
作者: RyanBern 时间: 2013-11-16 20:19
芯☆淡茹水 发表于 2013-11-16 18:06
在一篇 Ruby 语言教学里看到的,觉得很经典。
Ruby 的理念是:一切都是对象。包括:数值,字符串, ...
嗯,说得很形象,感谢你的帮忙哈!
作者: DeathKing 时间: 2013-11-19 09:24
写教程是一件很苦逼的事。首先,很多人都不具有程序设计的基础。而如果要先讲授程序设计语言本身(Ruby),再讲解框架(RGSS),那整个教程就会成为一个非常庞大的工程(剖析RGSS系统就非常复杂了)。
程序设计语言里面的概念就非常杂,诸如赋值、作用域、面向对象、编程范式等,没有一定编程基础的人很难很快掌握这些概念。一方面,为了能使用脚本来达到自己想要的目的,玩家希望“最小化”学习Ruby,但这样又会造成部分知识的缺失,导致后面的内容无法理解。例如,就编写RGSS脚本来说,我认为完全没必要学习Fiber(纤程)这种东西,因为为了深刻理解这个东西,又需要引入大量的其它知识。然而,RGSS3中最重要的一块:Window_Message就是使用Fiber来实现的。这就又不得不要求读者学习Fiber。
最主要的问题不在于教程的好坏,而在于读者本身的出发点——这也是一个悖论。为了写脚本→急功近利的学→学不好。这也是为什么不太推荐新手玩家学脚本的原因。
程序设计是一门艺术,它涵盖了各种技艺,要想掌握这门艺术,确实没有我们原本想象中的那么简单。
作者: dukesward 时间: 2013-11-19 09:58
其实ruby还是很好上手的。。
作者: DeathKing 时间: 2013-11-22 00:56
@RyanBern。 Nope,没有渣与不渣,只有用心与不用心。
作者: zl52wcl66rpg001 时间: 2013-11-22 18:45
感谢楼主分享,学习了!
作者: 咲废棫 时间: 2014-6-27 18:27
kuerlulu 发表于 2013-10-13 12:57
好顶赞!本来我也想写的 看lz发了我就来看看
赋值符号=是让左右相同,不一定是右边给左边,只要其中一个 ...
中文变量……没试过
作者: 福林林 时间: 2014-6-30 02:11
楼主辛苦了
作者: sirenke 时间: 2014-7-2 01:36
都是经验之谈,最近努力尝试中
作者: taroxd 时间: 2014-7-2 15:26
本帖最后由 taroxd 于 2014-7-2 17:19 编辑
感谢更新哦~ 关于这个教程有很多问题。列在下方,希望可以尽快改正。
你让下面这些东西怎么活:
defined?
not
or, and
modifier-if, modifier-unless, modifier-while, modifier-until
{ } blocks
当然,常量也有作用的范围,不过我们使用常量时,把它定义在最外面的层次上就好了,不必深入到类当中。
相反,放在自己的命名空间(namespace,一般为模块)中才是一个好习惯。
一般出现的错误,要么是syntax error,要么就是No method error。前面是指你的语法有问题,可能的情况就是少了end(后面要讲),或者是你凭空造了没有的语法;后面指的是你进行的运算和对象不符合,即该类对象没有定义这个运算,通常是没有初始化或者概念模糊造成的,以后我们都要说到。
NoMethodError出现,通常是因为对nil调用了一些方法。其原因,一是没有初始化,二是数组越界。数组越界的一个常见原因是你取出了一个空数组的第一个元素,然后想要对它做事情。当然,方法名拼写错误也是有可能的
- class A
- attr_accessor :x
- attr_accessor :y
- attr_accessor :z
- def initialize(x,y,z)
- @x = x
- @y = y
- @z = z
- end
- end
复制代码 你的缩进跪了
变量x和y的地址相同,因为1是立即值,所有的1全部指向同一个对象。因此,我建议拿三个内容相同的字符串做演示。
- class Person #定义一个叫Person的类
- def initialize
- @name = “Ryan”
- [url=home.php?mod=space&uid=6132]@age[/url] = 0
- [url=home.php?mod=space&uid=103045]@SEX[/url] = 0
- end
- def hello
- print “I am ” + @name + “!”
- end
- end
复制代码 url 暂且不论,字符串什么时候可以用中文双引号了?页面搜索“,你会发现很多这样的中文引号。
- attr_accessor:name
- attr_reader:name
- attr_writer:name
复制代码 这三个语句都是方法,因此参数(:name)必须打上括号或者空一格
别小看这个东西,很多时候因为少了这句话,会引发NoMethodError for class Nil
1. 如果是局部变量,报错信息为 undefined local variable or method `a' for xxx
2. 如果是实例变量或全局变量,报错信息为 undefined method `[]=' for nil:NilClass
再怎么说Nil也不是个class啊
遍历整个数组
本来是用each方法,但是这里我们一般这样写(array是一个数组):
for i in array
Ruby和RGSS3中,each的写法更为推荐
由于主键之间无法排序,所以Hash表也是没有顺序的。
Ruby1.9(VA)之后,Hash表是有顺序的。也就是插入的顺序。
但是如果在这个类之外写print @name,那么你一定看到的是活生生的nil,因为这个区域内,计算机不认识@name这个变量,按照Ruby规定,未初始化的实变量都是nil。
实例变量访问的对象只看当前self的值。因此类也可以有实例变量,顶层的空间也可以有实例变量。
不信?你在顶层试试:
@name = "main"
p @name
在读取数据完毕之后,我们加入这个p函数来显示这两个对象的地址。注:所有对象都有to_s方法,如果不加重设的话,返回的是引用的地址。
p 输出的是 inspect 的结果,而 print 和 puts 才是输出 to_s 的结果
Ruby谈类型,而且是强类型。但Ruby中的类型不是由class,而是由一个对象可以实现什么方法来决定的。
Ruby的Duck Typing是一个非常好的实践。
细心的朋友会发现,在(1)中a和b两个变量是互相独立的,对a进行的操作对b没有丝毫的影响
a和b是数字的时候,也指向同一数据,而不是相同数据的拷贝
a和b本来指向相同的对象,只是你后来又对b赋了一次值,使变量b指向了另一个对象而已。况且,你可以轻易改变一个数组的内容,一般却没人想去改变1,2这种数字的内容吧?
a = [2]
b = a
a = [4]
你看,b不还是[2]吗?
后面的RGSS部分,我没见过XP脚本,所以不做评论。
我觉得我和楼主的想法许多都是一致的呢。我办活动的目的,和楼主写教程的目的大致相同。我也只是个业余脚本党。
所以,如果你愿意继续来VA区帮忙写一些东西的话,我会很感谢你的哦~ 毕竟我那边偏向Ruby,即使没用过VA应该也可以写的。
作者: taroxd 时间: 2014-7-5 15:45
本帖最后由 taroxd 于 2014-7-5 16:06 编辑
在Ruby中,实变量以@开头,通常表示一个类的实例或者是作用在一个类中的“小型全局变量”。实变量的作用范围是整个类,也就是说,在某个类别外使用定义在它上的实变量是无效的。例如:
实例变量没有作用域的问题,永远是根据self来访问的。
class T
@var = 0
singleton_class.send :attr_writer, :var
attr_writer :var
def initialize
@var = 1
end
p @var # => 0
end
t = T.new
p T.instance_variable_get(:@var) # => 0
T.instance_eval { p @var } # => 0
p t.instance_variable_get(:@var) # => 1
t.instance_eval { p @var } # => 1
T.var = 2
t.var = 3
T.instance_eval { p @var } # => 2
t.instance_eval { p @var } # => 3
class T
@var = 0
singleton_class.send :attr_writer, :var
attr_writer :var
def initialize
@var = 1
end
p @var # => 0
end
t = T.new
p T.instance_variable_get(:@var) # => 0
T.instance_eval { p @var } # => 0
p t.instance_variable_get(:@var) # => 1
t.instance_eval { p @var } # => 1
T.var = 2
t.var = 3
T.instance_eval { p @var } # => 2
t.instance_eval { p @var } # => 3
这和类的作用域完全没有关系
但你何曾看过Object.new这样创建Object对象的语句了?
怎么没有?余烬之中不就这么干的……
可以include。混入(Mix-In)时也会混入常量
====================补充==================
class Person
@name = "XX"
def pr_name
print @name
end
end
print @name
class Person
@name = "XX"
def pr_name
print @name
end
end
print @name
我觉得你这个例子就是在满满地误导啊……
三个@name没有两个是同一个东西的……
一个是类Person的实例变量
一个是类Person的实例的实例变量
一个是类Object的实例main的实例变量
作者: stella 时间: 2014-9-19 10:54
要是能把那么多东西综合成一个游戏范例的制作就更形象了。毕竟对于脚本盲来说,带着一点自由度地跟着完成一件作品比光看着学效率要高不少
作者: 繁星千羽 时间: 2014-10-2 01:13
谢谢楼主……
最近才开始接触RMXP和脚本,因为之前有过一些很基础的编程基础,所以对编程的运算顺序和局部和全局变量理解得差不多,偏生之前用的语言并没有怎么用到库这个内容,然后看教程都是,前面跳得很快,一到库就停了下来,看得不明不白,直到看到楼主的教程终于理解了库是什么。
觉得这个教程很有用,只是有的地方说的不太够细致(大概也有我理解力不够的问题?)
以及有的地方突然多了一些之前没提到的东西(如预备知识里面的重要补充attr_accessor……我看到第一章类的时候才看到说明,尽管当时看这说明还是看不懂,后来看了第一次作业答案以及与自己写的作业对比后才明白是,让在库外面创造这个库的实例时能够改变这个数?那么我的理解就是,这个数是实例的属性,用了attr_accessor后属性才能够改变这样……如果理解还是有错希望能指出,谢谢),导致作为新人的我看得不明不白,这大概是因为是补充而不是同时写的原因吧。
感觉作业太难以及要求有点模糊。
第一个作业水瓶,写了之后完全不对路,最后参考了楼主帮我写的随时显示图像那个脚本,以及答案,改完之后拿给朋友看,朋友说应该没错。但还是不敢发这里来,毕竟上次有人说了我暂时还没有资格做这个作业,有未学走先学跑的嫌疑。
以上都是废话。
主要是……第二个作业完全不懂。
题目是做一个小游戏,从地图一点走去另外一点,将指定的方格全部走一次而且不重复。要求只能用独立开关的脚本。
看到题目的瞬间想起的是年代记幽灵船的小游戏
那么思路是,
移动到除了出口和入口以外的格子时判断当前位置的$game_self_switches[[?,?,’A’]] == false?
是 set为true
否 直接退出游戏
在出口时用循环判断之前的所有$game_self_switches[[?,?,’A’]] == true?
是 离开房间
然后就想起,我要如何用脚本判断我走到的格子是哪一格,以及,只能用独立开关的脚本意思是,在事件编辑器里面编辑还是只能用脚本编辑器?
最后,如果言语或者语气之间有什么不敬的地方请原谅我。已经好多人说我一个劲说高冷的话但也不见做出什么实际的事了。
但也不知道怎么改。如果真的让楼主大大有这种感受的话,真的抱歉。不是故意的。谢谢楼主为我们带来这个教程。
作者: RyanBern 时间: 2014-10-2 10:03
繁星千羽 发表于 2014-10-2 01:13
谢谢楼主……
最近才开始接触RMXP和脚本,因为之前有过一些很基础的编程基础,所以对编程的运算顺序和局部 ...
首先感谢你的回复呢。那我们就一条一条来说了。
首先第一点,这个教程帖我个人也是非常不满意。有些地方没有说清楚,对有一定经验的人来说太简单,对于新人来说太难。其实这是我一年前写这个东西的时候,面向的读者就不是一点基础都没有的新人。所以可能会吓跑一堆新手吧。对于attr_accessor :的理解,初学Ruby(RGSS1)的话这样理解完全可以。实际上就是可以在对象外面访问(写入和读取)对象本身的数据(成员变量)。还有要说明的是你要理解Taroxd大大的“挫折式言语”,正是因为不断受挫才会不断成长,我也是这么熬过来的。
第二点,第二个作业的要求只是提示你用脚本来完成独立开关的控制,核心就是“在一个事件中也可以控制另一个事件的独立开关”,仅此而已,没有必要把它想象得太复杂。完成这个任务是需要事件,事件脚本,地图结合起来的。并不是限制你做游戏的方式和方法,只是给了你一个提示。
说到这里,本人在XP提问区办的考场第二轮也即将开始,里面就有这个题目。如果你有兴趣可以拿去做做,其实这确实是一个很经典的题目了。
作者: taroxd 时间: 2014-10-2 10:53
本帖最后由 taroxd 于 2014-10-2 11:18 编辑
繁星千羽 发表于 2014-10-2 01:13
谢谢楼主……
最近才开始接触RMXP和脚本,因为之前有过一些很基础的编程基础,所以对编程的运算顺序和局部 ...
我当时这么说的原因就是你对类的理解还不够。一个是 attr_accessor 这类的东西。另外类里面的逻辑,方法的参数也十分混乱。
这是你上次给出的代码:
你的水瓶到底有没有 name 呢?或者说,name 是一个字符串还是一个水瓶?
并且,Ruby 有关键字 self,因此不需要像 python 那样传入一个 self 作为参数。
从整段代码都可以看出来,你还远远没有适应“面向对象”的编程。而 Ruby 恰恰就是一个完全面向对象的语言,举个例子,对一个整数取绝对值,在其他语言中大部分都是 abs(n),而 Ruby 中是 n.abs。这个东西还需要你去适应。适应完之后,可以说之后的学习基本上都是一帆风顺的。
你自己也说过:
但是脚本的学习不就是应该从模仿,找错,吸取经验,改进,继续模仿这样的进程来走的吗……
所以,先模仿一下吧。这个帖子里并没有许多例子给你模仿;F1 也没有,6R的大部分教程都没有,而预置脚本的难度则偏高了。所以,我推荐找一本好一点的 Ruby 教程吧。Ruby 要入门还是很容易的。
下面的给你参考一下(第 6 行可以无视):
class Bottle
attr_reader :capicity
attr_accessor :volume
protected :volume=
def initialize(capicity, volume = 0)
@capicity = capicity
@volume = volume
end
def clear
@volume = 0
end
def full
@volume = capicity
end
def >>(other)
diff = [@volume, other.capicity - other.volume].min
other.volume += diff
@volume -= diff
end
end
bottle10 = Bottle.new(10)
bottle20 = Bottle.new(20)
bottle20.full
p bottle20.volume # 20
bottle20 >> bottle10
p bottle10.volume # 10
p bottle20.volume # 10
bottle20.clear
p bottle20.volume # 0
bottle10 >> bottle20
p bottle10.volume # 0
p bottle20.volume # 10
class Bottle
attr_reader :capicity
attr_accessor :volume
protected :volume=
def initialize(capicity, volume = 0)
@capicity = capicity
@volume = volume
end
def clear
@volume = 0
end
def full
@volume = capicity
end
def >>(other)
diff = [@volume, other.capicity - other.volume].min
other.volume += diff
@volume -= diff
end
end
bottle10 = Bottle.new(10)
bottle20 = Bottle.new(20)
bottle20.full
p bottle20.volume # 20
bottle20 >> bottle10
p bottle10.volume # 10
p bottle20.volume # 10
bottle20.clear
p bottle20.volume # 0
bottle10 >> bottle20
p bottle10.volume # 0
p bottle20.volume # 10
作者: 繁星千羽 时间: 2014-10-2 19:45
感谢taroxd大大和楼主大大的建议,我会努力的
作者: 月隐丶从云 时间: 2015-6-26 23:45
谢谢,学习学习
作者: 舞小朵儿 时间: 2015-7-29 22:39
表示从前学过一点C++的我来说看到这个还是很无力。。。
作者: RyanBern 时间: 2015-7-29 23:09
舞小朵儿 发表于 2015-7-29 22:39
表示从前学过一点C++的我来说看到这个还是很无力。。。
首先,我得承认这个教程帖子写的不是很好,因为很多东西都没说清楚。后面的应用部分也是马马虎虎,感觉完全没有做一个视频来得实在。
然后,虽然都是面向对象编程语言,C++和Ruby有着极大不同,因此很多Ruby的现象让C++程序员都不能理解,再加上语言风格,关键字使用上的不同,让C/C++程序员很不适应。
所以,要真正弄懂还得去看专门的Ruby教程,推荐Ruby Programming
作者: gonglinyuan 时间: 2015-7-30 21:43
写得太好了,LZ真是业界良心
以前看了好多脚本相关的教程,我可以把他们简单分为两类:一类是从语言一点一点讲起:变量、运算符、函数、类……结果呢?——写到后来弃坑了,即使没弃坑别人也看不下去。另一类是从实例讲起:教你做一个美化的标题画面、教你做一个XX系统之类,在效果上可能会好一点,但是拓展性、系统性不强。
这份教程简直是为有编程基础却对RGSS理解不够的人量身定做的,真不错!
作者: bugmenot 时间: 2015-8-19 06:58
因为数组是从0号单元开始,但是游戏设定里面,ID是从1开始的,因此这两个全局变量的0号单元都是nil,我们用的时候,不必担心这方面的问题。$game_switches[1]就是指的1号开关(事件编译器中的1号开关),$game_variables[1]就是指的1号变量。
其实这个很讨厌的,太不一致了。其实 RM 完全可以从 0 开始的。或者为了照顾日常生活习惯,$data_xxx 和 $game_xxx 等可以用哈希表储存,id 作 key。
有的时候要遍历这些数组,然后首项 nil 当然就没有相应的属性(RGSS 叫属性,其实是 Ruby 的方法) ,然后就报错,必须写 [1..-1] 才行。
类的变量 和 类变量,没听说过这种说法。。。直接说实例变量和类变量就是了。
实际上,= 不仅用于赋值, Ruby (以及很多语言中)还用于声明:- # 声明变量 a,并赋值 1
- a = 1
- # 赋值 2
- a = 2
复制代码 比如,换成 JavaScript 就是声明和赋值分开的- var a; # 声明
- a = 1; # 赋值
- a = 2; # 赋值
- var b = 2; 简写
复制代码但是赋过去的是什么,这要根据你赋值对象而确定。如果你把一个整数或者浮点数赋给一个变量,那么实际上传过去的就是变量的值,但是如果你把一个数组或者一个类的实例(当然Fixnum类的除外)赋给一个变量,那么实际传递过去的就是指针
其实就是按值传递(Ruby 算传递对象),或是按引用传递(传递对象的引用),窃以为不必要引入指针。C 中的数组就是指针,还有空指针,指针可以加减,还有 a[p] 和 p[a] 一样等等,和 Ruby 的引用还是不一样的。
作者: RyanBern 时间: 2015-8-19 08:55
bugmenot 发表于 2015-8-19 06:58
其实这个很讨厌的,太不一致了。其实 RM 完全可以从 0 开始的。或者为了照顾日常生活习惯,$data_xxx 和 ...
首先,非常感谢你的建议!提到的这些本来应该在第二版修正的(第一版的错误太多了),但是还是没有顾虑到。
不过我记得Ruby的一大特性就是“变量无需声明直接使用”,所以声明什么的,可以不必再提。
赋值那个地方其实讲得比较乱,完全可以用一句话代替:“赋值就是引用的重定向”。因为Ruby中并没有“值类型”和“引用类型”的说法,所以例如Fixnum类的对象,其实是即时值,赋值还是是改改引用而已。
作者: bugmenot 时间: 2015-8-19 09:49
不过我记得Ruby的一大特性就是“变量无需声明直接使用”,所以声明什么的,可以不必再提。
无需声明是 a = 1 赋值语句隐含声明。这样大部分时候可以省打字,但是有时候需要:- a = nil
- # a = nil,表面赋值,其实仅仅为了声明
- ......
- # 内层
- a = 1
- # 这是给外层的 a 赋值,并未声明一个 内层变量
复制代码 所以是没有独立的声明语句,还是有声明语义(暗含在赋值语句中)。
当然,不讨论作用域的前提下,不提声明比较简洁。
作者: a3738312 时间: 2015-8-19 19:53
看不太懂-.……那些def之类的语句不懂什么意思
作者: 玄胄武甲 时间: 2015-8-23 21:57
好久没上6R发现居然还有人在写RMXP的教程,支持一下咯
作者: 银羽凌云 时间: 2016-7-8 19:03
大神求教
class Person
attr_accessor:name,:sex,:age
def initialize(name,sex,age)
@name = name ; @SEX = sex ; @age = age
end
def introduce
print "我叫#{@name},我是#{@sex},我今年#{@age}岁。"
end
end
xiaoming = Person.new("小明","男生",6)
xiaoming.introduce
xiaoming.name=("小红")
xiaoming.sex=("女生")
xiaoming.age=(7)
xiaoming.introduce
xiaohong = Person.new("小红","女生",7)
xiaohong.introduce
输出结果是:我叫小明,我是男生,今年6岁。
我叫小红,我是女生,今年7岁。
我叫小红,我是女生,今年7岁。
怎样修改才能变成这样
输出结果:xiaoming:我叫小明,我是男生,今年6岁。
xiaoming:我叫小红,我是女生,今年7岁。
xiaohong:我叫小红,我是女生,今年7岁。
作者: 银羽凌云 时间: 2016-7-8 21:59
大神求教
class Cuboid
def initialize(number)
@cuboid_number = number
end
def shuchu
number = @cuboid_number
print "长方体#{number}"
end
end
cuboid_001 = Cuboid.new(001)
cuboid_001.shuchu
结果:长方体1
怎样才能得到结果:长方体001
作者: RyanBern 时间: 2016-7-8 22:13
001表示一个八进制数,因此不建议你这样给实变量赋值。
一个可行的办法是这样
[pre lang=Ruby]def count
print sprintf("长方体%03d", @cuboid_number)
end[/pre]
然后调用cuboid_001 = Cuboid.new(1)
利用sprintf可以进行格式化输出。
至于第一个问题,我不会写。你可以考虑使用eval来计算表达式的值。
name='xiaoming'
eval name+'.name = "小红"'
作者: 喵呜喵5 时间: 2016-7-8 22:37
本帖最后由 喵呜喵5 于 2016-7-8 22:39 编辑
没有任何正常的做法能够实现你所希望的效果,因为 Ruby 是动态类型的,所以和 C 语言等静态类型语言不同下面这段代码执行时不会报错
而也正是因为Ruby是动态类型的,赋值操作实际上是传递一个引用,上面这段代码中,a=1时,a是一个指向1的值,a="hello"时,a是一个指向hello的值,a 并不是一个具体的数值,而更像是某一个具体数值的一个“外号”或者“别名”
所以,在下面这段代码中,你会看到一个奇怪但符合这一规则的结果
- class Person
- attr_accessor :name,:nickname
- def initialize(name,nickname)
- @name = name
- @nickname = nickname
- end
- end
- xiaoming = Person.new("小明","xiaoming")
- xiaoming2 = xiaoming
- xiaoming2.name = "小明明"
- print xiaoming.name
复制代码
修改 xiaoming2 的名字时,xiaoming 的名字也跟着发生改变了
不论是 xiaoming 还是 xiaoming2,他们都只是别人给 Person("小明") 起的一个外号,有些外号是你直接起了告诉小明的(@nickname),有些外号是你背地里偷偷起的(xiaoming2 = xiaoming),
直接起的外号小明知道,起外号的你也知道,你提到这个外号便知道说的是小明,你问小明他有什么外号他也会告诉你
背地里偷偷起的外号你自己知道,提到这个外号也知道说的是小明,但是你问小明知不知道这个外号时,小明自然就懵逼了
另外,实际写代码过程中一般不会出现真的需要获取小明外号的情况
作者: zwlstc109 时间: 2017-1-21 19:29
本帖最后由 zwlstc109 于 2017-1-21 19:41 编辑
支持下
作者: megaman 时间: 2017-4-20 10:23
版主大大,请问我看完了第四章节就能把战斗菜单改成新仙剑奇侠传那样了吗?
作者: tsukishinu 时间: 2018-3-6 14:47
版主大大你好,想请教一个问题。我照着这个帖子写完了真实商店,虽然功能基本实现了,但是商店里的数据没法存档,毕竟真实商店的库存和玩家操作有关。数据写在了$game_visualshop数组里面,如果在Scene_Title里面初始化就没法保存了,不知道改怎么写才能让数据保存下来。
如果版主大大看到了,希望能指点一下(跪),非常感谢版主大大写了这个教程,学到了很多,终于能看懂默认脚本了。
ps:不知道算不算挖坟啊,但是没权限发消息对不起((
作者: RyanBern 时间: 2018-3-7 09:11
一个比较方便的做法是将真实商店变成 $game_system 对象里面的一个实变量。因为 $game_system 这个对象是可以自动存储的,所以以它为载体存储额外对象是个重要的技巧。
代码只需要添加:
class Game_System
attr_accessor :visualshop
end
class Game_System
attr_accessor :visualshop
end
而后在生成真实商店的时候使用 $game_system.visualshop = Game_VisualShop.new(args) 即可
作者: tsukishinu 时间: 2018-3-7 16:36
改完可以存储了=w=谢谢版主大大!看起来$game_system里面可以塞好多东西……
作者: 765111039 时间: 2023-5-4 12:14
教程看完了,学会了P函数
开玩笑的,感谢楼主和各位大神,留名支持一下
这不算挖坟吧。
欢迎光临 Project1 (https://rpg.blue/) |
Powered by Discuz! X3.1 |