RGSS³ 入门教程

目录

本教程尝试用一页讲完 RGSS 入门,但是讲归讲,多写、多试错才能真正学会一样东西。准备好了,那就开始吧 ——

Ruby — 字面量、五种变量、面向对象

字面量

所谓字面,就是要认得代码,解决XXX 是什么的萌新问题。在阅读本文的过程中,虽然我们希望能够循序渐进地把知识点展开,但是知识点之间存在相互的关联,有时你会在前文看到一些无法理解的词句,不要担心,继续往下读就可以了。

首先我们做一个读法上的约定,==(两个 = 符号连着写)读作等于,而 = 读作赋值为(还记得吗,无法理解的东西可以先不纠结)。

空、真、假
niltruefalse
Ruby 中没有所谓的布尔值(bool),但是表达式有真假:只有 nil false 是假(指出现分支条件时判定为否),别的都是真(如 0)。假设 T 为真,F 为假,x 为任意,那么有如下运算:
与:T && x == x; F && x == F
或:T || x == T; F || x == x
非:!T == F; !F == T
数(Numeric)
3.14159265359421e6(就是106)……
支持一些常见的运算:3 + 5 == 8 5 - 3 == 2 3 * 5 == 15 5 / 3 == 1(注意,整数运算会得到整数)5.0 / 3 == 1.6666666666666667(至少有一个是小数,结果就会是小数)5 % 3 == 2 -5 % 3 == 1 。还有如与、或、亦或、移位等,请自行了解。
字符串(String)
'hello\n world'"hello\n world"……
双引号里的字符串可以转义,常见的转义符有 \n(换行)\t(制表符)等,
还有一种转义叫内嵌表达式"3 + 5 = #{3 + 5}" 会被转义成 "3 + 5 = 8",注意当你写下这个字符串时转义就已经完成了。
符号(Symbol)因为符号在中文里有太多意思,一般倾向于说 Symbol
:this_is_a_symbol:'me too':"I'm #{3 + 5}!"……
数组(Array)
[][nil, 1, '2', :'3']……
我们可以用下标来访问数组内部的东西,如 [1, 2, 3][0] == 1 [1, 2, 3][1] == 2 ,也就是说下标从 0 开始,当你使用正数越界访问时,会得到 nil
哈希表(Hash)严格来说,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]

答案(反白可见):

五种变量

先讲变量,它的基本用法就是 x = 3; x + 5 #=> 8 ,读作先把 x 赋值为 3;然后计算 x + 5 # 返回 8(井号直到行尾是注释,不会执行,对执行代码的程序也没有意义),也没什么好说的。我这里不提数学领域的变量,因为压根不是一回事,也请各位不要混淆。总结一下,变量只有两件事可以干:赋值、使用。

但即使是这样,也不得不举个例子:

a = 1; b = a; a = 2; b == ?

答案(反白可见): 提示:名与实。

猜出来的别忙着高兴,还有一题:

a = [1]; b = a; a[0] = 2; b == ?

答案(反白可见):

理解上面两个例子,就可以继续了。我先在桌上排出 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

面向对象

有些人已经注意到我在上面使用了 defclass ,那么它们是什么呢?

在 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 — 雪碧、主循环

RGSS 里除了 Ruby 的标准库外,还提供了一套游戏接口,用于控制游戏画面、读取用户输入、播放音频等,你可以在 F1 中看到游戏库的文档。这里我挑重点讲一个雪碧(Sprite)的用法。

雪碧(精灵)

首先这个单词确实不好翻译,其次雪碧也真的是这个单词(笑)。RGSS 为了显示一张图,需要知道

  1. 这图每个像素是啥颜色(Bitmap)
  2. 显示在哪,显示哪一部分(Sprite/Plane+Viewport)
  3. 显示的时候要不要加特技(如变色、透明、旋转、缩放)(Sprite)
  4. 谁负责把它画到屏幕上(Graphics)

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 — 元编程

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 元编程一书。

RGSS — 插件

所谓插件,就是尽量做到直接插入指定位置(一般为 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 扩展

编写 dll 已经不算入门教程了,可以参考我的另外两篇文章,

附录:安装和运行独立的 Ruby

安装

鉴于 RGSS 的运行平台是 Windows,这里直接讲 Windows 平台的安装方法。

  1. 打开 RubyInstaller 下载页
  2. 点击WITHOUT DEVKIT下的第一个链接下载,编写本文时它是Ruby 2.6.3-1 (x64)
    安装包大约 10MB 左右,但 GitHub 线路在国内访问通常较慢,请耐心等待或者科学上网
  3. 双击运行 rubyinstaller-*.exe
    安装过程中可能会有关联 .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,也就是你输入一行它返回一行的东西,方便快速验证一些想法。


帮助编辑本页面