本教程尝试用一页讲完 RGSS 入门,但是讲归讲,多写、多试错才能真正学会一样东西。准备好了,那就开始吧 ——
所谓字面
,就是要认得
代码,解决XXX 是什么
的萌新问题。在阅读本文的过程中,虽然我们希望能够循序渐进
地把知识点展开,但是知识点之间存在相互的关联,有时你会在前文看到一些无法理解的词句,不要担心,继续往下读就可以了。
首先我们做一个读法上的约定,==
(两个 =
符号连着写)读作等于
,而 =
读作赋值为
(还记得吗,无法理解的东西可以先不纠结)。
nil
、true
、false
布尔值(bool),但是表达式有
真假:只有
nil
false
是假(指出现分支条件时判定为否),别的都是真(如 0
)。假设 T 为真,F 为假,x 为任意,那么有如下运算:与:T && x == x; F && x == F
或:T || x == T; F || x == x
非:!T == F; !F == T
3.14159265359
、42
、1e6
(就是106)……
3 + 5 == 8
5 - 3 == 2
3 * 5 == 15
5 / 3 == 1
(注意,整数运算会得到整数)5.0 / 3 == 1.6666666666666667
(至少有一个是小数,结果就会是小数)5 % 3 == 2
-5 % 3 == 1
。还有如与、或、亦或、移位等,请自行了解。'hello\n world'
、"hello\n world"
……转义,常见的转义符有
\n
(换行)\t
(制表符)等,内嵌表达式,
"3 + 5 = #{3 + 5}"
会被转义成 "3 + 5 = 8"
,注意当你写下这个字符串时转义就
已经完成了。
符号在中文里有太多意思,一般倾向于说 Symbol
:this_is_a_symbol
、:'me too'
、:"I'm #{3 + 5}!"
……[]
、[nil, 1, '2', :'3']
……下标来访问数组内部的东西,如
[1, 2, 3][0] == 1
[1, 2, 3][1] == 2
,也就是说下标从 0 开始,当你使用正数越界访问时,会得到
nil
。hash是算法,
表是数据结构
{}
、{ 1 => '2', :'3' => [true] }
、{ a: 42 }
(等价于 { :a => 42 }
)……=>
左边的叫做键(key),右边的叫做
值(value)。哈希表同样可以使用下标来访问:
{ 1 => 2 }[1] == 2
不要问我为什么 X 长成 X 这个样子
,这就像在问1 为什么是 1
。
上文中已经提到了一些运算符
,实际运算符的数量并不多,请自行查阅 F1 或者搜索引擎。运算符之间有一定的优先级关系(比如 */
比 +-
要更优先
,所以先算 */
),知道优先级可以帮助我们少写括号
,这并不是说不写括号是好事,但是你总得读懂别人的代码吧。
最后我们做个小测试,看看你能否说出下面这个表达式最终的结果?
[{ true => false, false => true }, { true => true, false => false }][5 / 3][nil]
答案(反白可见): nil
先讲变量
,它的基本用法就是 x = 3; x + 5 #=> 8
,读作先把 x 赋值为 3;然后计算 x + 5 # 返回 8(井号直到行尾是注释,不会执行,对执行代码的程序也没有意义)
,也没什么好说的。我这里不提数学
领域的变量,因为压根不是一回事,也请各位不要混淆。总结一下,变量只有两件事可以干:赋值、使用。
但即使是这样,也不得不举个例子:
a = 1; b = a; a = 2; b == ?
答案(反白可见):1
提示:名与实。
猜出来的别忙着高兴,还有一题:
a = [1]; b = a; a[0] = 2; b == ?
答案(反白可见):[2]
理解上面两个例子,就可以继续了。我先在桌上排出 5 种变量的写法:
举例 | 写法 | 读作 | 作用域 |
---|---|---|---|
a |
小写字母或 _ 开头,接任意大小写字母下划线 | (局部)变量 | 当前方法、和所属块同级 |
@a |
@ 开头,接任意大小写字母下划线 | 实例变量 | 当前对象 |
@@a |
@@ 开头,接任意大小写字母下划线 | 类变量 | 当前命名空间 |
$a |
$ 开头,接任意大小写字母下划线 | 全局变量 | 任何地方 |
A |
大写字母开头,接任意大小写字母下划线 | 常(变)量 | 当前命名空间 |
平时说的时候可能会省略括号内的内容。类变量和全局变量因为不推荐使用,我在这里也不会讲,需要的可以自己搜索和查阅文档。
def f()
a = 42
p a #=> 42
end
def g()
f()
p a # NameError (undefined local variable or method `a')
end
def f()
@a = 42
p @a #=> 42
end
def g()
f()
p @a #=> 42
end
B = 1
class A
B = 42
p B #=> 42
p ::B #=> 1
end
p B #=> 1
p A::B #=> 42
B = A::B #=> warning, previous B is 1
p B #=> 42
有些人已经注意到我在上面使用了 def
和 class
,那么它们是什么呢?
在 Ruby 的设计思想中,一切皆为对象
是很重要的一个原则,对象
在 Ruby 的语境中其实十分简单:拥有自己的数据
和行为
。数据就是实例变量,行为就是方法。类本质上是定义了某种对象应该拥有哪些方法,而实例变量则是在执行过程中动态产生的。
实例(Instance)
指的是由 某个类.new
产生的一个具体的对象,我们把 .new
产生新对象的行为叫做实例化
,实例变量就是作用域
在对象这个层面上的变量。
例如,有一种计时器
,它只记录一个自然数,每次更新时 +1,而且没有上限。我们该如何定义它?
class Timer
# initialize 方法会在 Timer.new 时被自动调用一次
def initialize
@counter = 0
end
def update
@counter += 1
end
end
t = Timer.new # 把 t 赋值为一个 Timer 的实例
loop { t.update } # 一直调用 t 的 update 方法
等等,我怎么知道 t 现在计到多少了呢?嗯,所以我们要再定义一些方法来告诉
我们这件事:
# loop { t.update }
class Timer
def counter
# return @counter
@counter # 没写 return 的话,方法执行到最后一行的返回值,会作为最终的返回值
end
end
loop { t.update; p t.counter }
能否让我直接指定 @counter
的值呢?
class Timer
def counter=(value) # 注意此处=
是方法名的一部分,= 左侧不要有任何空格
@counter = value # 而这里的=
是赋值为
end
end
t.counter = 233 # 在调用 counter= 方法时,我们可以在 = 左侧加空格,使其看起来更像赋值
t.counter=(233) # 上面一行等价于像这样调用了该方法
# 在不引起歧义的情况下,调用方法时的括号可以省略,如
f 1, 2 # 等价于 f(1, 2)
f 1, g 2 # 产生了歧义,会报错
方法可以有各种参数,
def f(a, *args, &blk)
args[-1] # 取变长参数中的最后一个作为返回值
end
f(1, 2, 3) #=> 3
方法可以被混入(Mixin)
,
module M
def a
42
end
end
class Timer
include M
end
t.a #=> 42
Timer.extend M
Timer.a #=> 42(类也是个对象)
一些运算符
的本质是方法,
class Timer
def +(other)
other
end
def [](*)
42
end
end
t[t + t] #=> 42
还有很多很多,建议结合一些具体的代码和文档来学习。
打开 RM 主界面,按下 F1 —— 这就是不花钱不用力能找到的第一份文档,点开最下面的这本参考手册
,里面的Ruby 语法
和标准库
是关于 Ruby 的文档,并没有多长,可以浏览一下。
遗憾的是,F1 里的这份 Ruby 文档显然是不全的,有时你会在别人的代码里看到从未见过的写法和用法,这时你可以使用这本Ruby 1.9.2 官方文档
。
注意,RGSS 内的 Ruby 标准库也是不全的
,你在官方文档上看来的标准库有可能不存在于 RGSS 环境,例如 pp
Psych
等。
RGSS 里除了 Ruby 的标准库外,还提供了一套游戏接口,用于控制游戏画面、读取用户输入、播放音频等,你可以在 F1 中看到游戏库的文档。这里我挑重点讲一个雪碧(Sprite)
的用法。
首先这个单词确实不好翻译,其次雪碧也真的是这个单词(笑)。RGSS 为了显示一张图,需要知道
Sprite 就是一个包含着位置、区域、特技
信息的类,可以看到它其实不包含图像数据,所以需要和 Bitmap 一起使用。
s = Sprite.new
s.bitmap = Bitmap.new 20, 20
s.bitmap.fill_rect(s.bitmap.rect, Color.new(255,0,0))
loop { Graphics.update }
# 在游戏中运行,你应该看到屏幕左上角一个小小的红色方块,它就是代码里的s
在 RGSS 中,屏幕坐标系是 → 为 X 轴,↓ 为 Y 轴,也就是说原点在左上角。图片有宽高,Sprite 有 src_rect, ox, oy, x, y,它们是怎么定位的呢?
bitmap sprite
+-----------------+ +------------+
| src_rect | |\ S |
| +------+ B | --> | (ox, oy) --|--> (x, y)
| | S | | | |
| +------+ | +------------+
+-----------------+ (稍微放大了一点)
首先从 bitmap 上裁下 src_rect 指定的一块图案,然后以 src_rect 的左上角为原点确定 (ox, oy) 的位置,最后把 (ox, oy) 这个点对齐到 (x, y) 上,就是该 sprite 的最终位置。这个 (x, y) 如果指定了 viewport,就是相对于 viewport 左上角,否则就是相对于屏幕左上角。
根据以上信息,我们可以简单地把上面的 s 定位到屏幕正中:
s.ox, s.oy = s.width / 2, s.height / 2
s.x, s.y = Graphics.width / 2, Graphics.height / 2
代码是顺序执行的 —— 这意味着如果我们把默认脚本删光,这个游戏窗口会闪一下就消失,因为脚本执行完了
。RGSS 的运行主要依赖于一个循环,它让这个游戏窗口一直显示、一直有内容、以及响应用户的操作
loop do
Input.update # 读取用户输入
# 更新游戏对象
Graphics.update # 刷新屏幕以显示最新的游戏对象,并将 FPS 稳定到 Graphics.frame_rate
end
结合上面的雪碧,试试写一个通过上下左右按键控制雪碧移动吧!参考实现:
def spr(*args)
s = Sprite.new
b = Bitmap.new(*args)
s.bitmap = b
yield s, b if block_given?
s
end
def mainloop
loop do
Input.update
yield
Graphics.update
end
end
def center s
s.ox, s.oy = s.width / 2, s.height / 2
s.x, s.y = Graphics.width / 2, Graphics.height / 2
end
center s = spr(20, 20) { |_, b| b.fill_rect b.rect, Color.new(255,0,0) }
mainloop do
s.x += 1 if Input.press? :RIGHT
s.x -= 1 if Input.press? :LEFT
s.y += 1 if Input.press? :DOWN
s.y -= 1 if Input.press? :UP
end
# 如果感到难以理解这里代码具体的运行顺序,可以搜索Ruby 块
游戏库的文档主要就是 F1,99% 的游戏功能你都需要不停地翻看 F1、默认脚本、别人的脚本来理解和使用。
Ruby 最大的优势之一就是其元编程的能力使得编写插件、造 DSL 等毫不费力,甚至被人说成是魔法
。其实理解元编程有助于我们理解 Ruby 面向对象的本质,而且用起来真的很爽。
# 打开
类
class A
def a
42
end
end
# 能用同一个语法打开类的语言并不多,这意味着 class 这个关键字比起定义
更像打开
class A
alias b a # 复制一份 a 方法并起名成 b
def a # 覆盖
了上面定义的 a
b + 1 # 调用 b,也就是原来的 a,并返回它 +1 的值
end
end
# 获取当前对象
,通过 self 我们可以知道当前代码的作用域
self #=> main
class A
self #=> A
def a
self #=> #<A:0x12345678>
end
end
# super: 调用基类同名方法
class AA < A
def a
super + 1
end
# 怎么知道基类有没有这个同名方法?
def f
super if defined? super
42
end
end
# 那么怎么调用基类不同名方法?
class AA < A
def f
self.class.superclass.instance_method(:a).bind(self).call
# self.class -> AA
# AA.superclass -> A
# A.instance_method(:a) -> #<UnboundMethod:a>
# #<UnboundMethod:a>.bind(self) -> #<Method:a>
end
end
# 动态定义方法
A.define_method(:a) { 42 } # 调用: A.new.a
A.define_singleton_method(:a) { 24 } # 调用: A.a
def A.a() 24 end # 调用: A.a
# 一些钩子(Hook)方法
class A
# 添加方法定义时被调用,参数是一个 Symbol
def self.method_added(sym)
p "added #{sym}"
end
# 添加一个方法 a
def a() end #=> "added a"
# 调用一个未定义的方法时被调用,参数是方法名、调用时的其他参数
def method_missing(sym, *args, &blk)
p "called undefined #{sym}"
end
end
A.new.miaomiao #=> "called undefined miaomiao“
还有很多很多,建议参考Ruby 元编程
一书。
所谓插件
,就是尽量做到直接插入指定位置(一般为 Main 前),不需要手动修改默认脚本即可使用
的脚本。而我们的脚本一般都是对游戏系统有所修改的,大部分情况下我们会使用 alias
等对默认脚本进行修改,这个在上文中也有所介绍。这里我唯一能给出的建议是:别用 old
old_update
这样的名字,很容易撞上。
热插拔
改一下代码重启一下游戏真是太麻烦了,要是有什么简化重新载入插件的手段就好了。—— 接下来我们就来实现这个功能,主要思路是:监控一个文件夹内的所有 .rb 文件,如果有任何改动(增加、删除、修改),就将这些改动同步到正在运行的游戏内。所谓监控
也很简单,每次 Graphics.update
都扫一遍文件的修改时间 File.mtime
即可。
class << Graphics
alias update_without_hotreload update
def update
update_without_hotreload
@plugins ||= []
files = @plugins.keys
Dir.glob('Scripts/*.rb') do |file|
files.delete file
unless @plugins[file] and @plugins[file][0] == File.mtime file
eval @plugins[file][1], TOPLEVEL_BINDING if @plugins[file]
@plugins[file] = [File.mtime(file)]
@plugins[file][1] = prepare_unload_script file
load file
end
end
files.each do |file|
eval @plugins[file][1], TOPLEVEL_BINDING
@plugins.delete file
end
end
def prepare_unload_script file
'' # 消除所有 alias 带来的影响,这里可以通过读注释等方法获取信息
end
end
编写 dll 已经不算入门教程了,可以参考我的另外两篇文章,
鉴于 RGSS 的运行平台是 Windows,这里直接讲 Windows 平台的安装方法。
WITHOUT DEVKIT下的第一个链接下载,编写本文时它是
Ruby 2.6.3-1 (x64)
关联 .rb 文件为可执行程序的选项,建议取消勾选(看不懂英文的无视这句话)
Run 'ridk install' ……,取消勾选,点击 Finish 按钮完成安装
为了验证安装成功,从你的开始菜单中打开命令提示符
(也就是常说的 cmd),在里面输入 ruby -v
回车(注意空格和大小写),你应该看到类似
ruby 2.6.3p62 (2019-04-16 revision 67580) [x64-mingw32]
而不是
'ruby' 不是内部或外部命令,也不是可运行的程序或批处理文件。
用你的文本编辑器(Win10 的记事本、VSCode 等)新建并打开一个文件,输入 ruby 代码后保存为后缀名为 .rb 的文件,然后打开 cmd,输入 ruby
(注意空格),接着把该文件拖放进 cmd,你应该会看到控制台内变成了
C:\>ruby C:\Users\abc\Desktop\a.rb
之类的,然后敲一下回车,运行时的输出(p puts
等)会显示在下面。
你也可以在 cmd 里输入 irb
回车,这时会打开一个 Ruby 的REPL
,也就是你输入一行它返回一行的东西,方便快速验证一些想法。