DXRubyでRPGを作る(2)
2013/12/18
DXRuby Advent Calendar18日目です。前回はRPGに何が必要なのかを考えました。今回も引き続き、DXRubyでRPGを作る方法を模索します。
では引き続き、どんな構造に?
Event(モジュール)
Eventは任意の名前での登録はないですが、情報を階層的に保持する点でDataと酷似します。
Eventは、まず発生源ごとにそれに適したフォームを作成することにしましょう。例えば、Mapの特定のマスに行ったときに起こすものなら、2つ情報を与えるようなフォームにします。このフォームのクラス名はEPageにします。
そして管理しやすいように、フォームは全て何らかのグループに属すようにし、そのグループはEventモジュール直下か他のグループに登録させましょう。このグループの名前はEBookにします。
その手順は、
- EventモジュールにEBookクラスを、EBookクラスにEPageクラスを作る
- EPageクラスのinitializeで、発生源の特定用の情報の数と、発生時に渡す情報の数を引数としてもらい、add_eventメソッドとholdメソッドを定義する
- EventモジュールとEBookクラスにbookメソッドを定義し、Symbolを渡すことでその名前のメソッドでEBookクラスを返すようにし、EBookクラスにはpageメソッドも定義し、SymbolとEPage.new用の引数をもらい、EPageクラスを返すようにする
- has_book?メソッドとhas_page?メソッドを定義する
こうなります。では早速「1.」をやりましょう。
module Event
class EBook
class EPage
end
end
end
こうですね。次に「2.」をやります。
module Event
class EBook
class EPage
attr_reader :args
#どんなEPageかを表します
#具体的には、add_eventやhold時の引数の数です
def initialize(arg_num,hold_args)
#arg_numは発生源を特定するための情報の数
#hold_argsはhold時に送信する情報量(arg_numを除く)の数値の配列
# ([最低限, 最大数])
@event = {} #イベント保存用ハッシュ
hold_arg_num = hold_args.map{|i| (i < 0 ? 0 : i.div(1))}
#hold_argsの中身に不正なものが無いかの修正
@args = [arg_num, hold_arg_num.min, hold_arg_num.max]
#これを見ればどんなEPageか特定できます
tmp = class << self;self;end #特異クラスを生成
tmp.class_eval do
#まずholdを定義します
#その際、hold_arg_numの値に応じてエラーメッセージを変えます
#holdメソッド内で毎回条件分岐しないようにしています
if hold_arg_num.min != hold_arg_num.max
define_method(:hold){|*args|
if args.size < arg_num + hold_arg_num.min || args.size > arg_num + hold_arg_num.max
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num + hold_arg_num.min}..#{arg_num + hold_arg_num.max})",
caller(1))
end
if args.size - arg_num == 0
@event[args].call if @event.has_key?(args)
else
if @event.has_key?(args[0...arg_num])
@event[args[0...arg_num]].call(*args[arg_num...args.size])
end
end
}
else
if hold_arg_num.min == 0
define_method(:hold){|*args|
if args.size != arg_num
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num})",
caller(1))
end
@event[args].call if @event.has_key?(args)
}
else
define_method(:hold){|*args|
if args.size != arg_num + hold_arg_num.min
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num + hold_arg_num.min})",
caller(1))
end
if @event.has_key?(args[0...arg_num])
@event[args[0...arg_num]].call(*args[arg_num...args.size])
end
}
end
end
#次にadd_eventメソッドを定義する
define_method(:add_event){|*args, &block|
if args.size != arg_num
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num})",
caller(1))
end
if block
@event[args] = block
else
@event.delete(args) if @event.has_key?(args)
end
}
end
end
end
end
end
私はこうなりました。一気に難しく見えるようになりました。が、「特異クラス」とdefine_method(name){..}
さえ理解すればいけると思います。
特異クラスを生成したtmp = class << self;self;end
という一行ですが、;
で複数行を1行にまとめただけです。元々は、
class << self
self
end
という特異クラスをtmp
に代入したのです。簡単に言うと、self
のコピーです。そしてdefine_method(name){..}
は、渡したブロックをそのままnameの名前でメソッドとして定義するものです。
まとめると、selfのコピーに対してclass_eval{..}
をし、そのブロック内でdefine_method(name){..}
を実行することで、selfの特異メソッドとして登録しています。
これができたらもう簡単です。同じ要領で「3.」をやります。
module Event
def self.book(name)
newR = EBook.new
define_method(name){newR}
module_function name
newR
end
class EBook
def page(name,arg_num,hold_arg_num = [0])
newF = EPage.new(arg_num,hold_arg_num)
tmp = class << self;self;end
tmp.class_eval do
define_method(name){newF}
end
newF
end
def book(name)
newR = EBook.new
tmp = class << self;self;end
tmp.class_eval do
define_method(name){newR}
end
newR
end
class EPage
attr_reader :args
def initialize(arg_num,hold_args)
@event = {}
hold_arg_num = hold_args.map{|i| (i < 0 ? 0 : i.div(1))}
@args = [arg_num, hold_arg_num.min, hold_arg_num.max]
tmp = class << self;self;end
tmp.class_eval do
if hold_arg_num.min != hold_arg_num.max
define_method(:hold){|*args|
if args.size < arg_num + hold_arg_num.min || args.size > arg_num + hold_arg_num.max
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num + hold_arg_num.min}..#{arg_num + hold_arg_num.max})",
caller(1))
end
if args.size - arg_num == 0
@event[args].call if @event.has_key?(args)
else
if @event.has_key?(args[0...arg_num])
@event[args[0...arg_num]].call(*args[arg_num...args.size])
end
end
}
else
if hold_arg_num.min == 0
define_method(:hold){|*args|
if args.size != arg_num
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num})",
caller(1))
end
@event[args].call if @event.has_key?(args)
}
else
define_method(:hold){|*args|
if args.size != arg_num + hold_arg_num.min
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num + hold_arg_num.min})",
caller(1))
end
if @event.has_key?(args[0...arg_num])
@event[args[0...arg_num]].call(*args[arg_num...args.size])
end
}
end
end
define_method(:add_event){|*args, &block|
if args.size != arg_num
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num})",
caller(1))
end
if block
@event[args] = block
else
@event.delete(args) if @event.has_key?(args)
end
}
end
end
end
end
end
さっきより簡単です。補足すると、module_function(name)
はnameの名前を持つメソッドをモジュールメソッドにしています。
さいごに、「4.」を付け足します。
module Event
def self.book(name)
newR = EBook.new
define_method(name){newR}
module_function name
newR
end
def self.has_book?(name)
return false unless self.singleton_methods.include?(name)
return true if self.__send__(name).class == EBook
return false
end
class EBook
def page(name,arg_num,hold_arg_num = [0])
newF = EPage.new(arg_num,hold_arg_num)
tmp = class << self;self;end
tmp.class_eval do
define_method(name){newF}
end
newF
end
def book(name)
newR = EBook.new
tmp = class << self;self;end
tmp.class_eval do
define_method(name){newR}
end
newR
end
def has_book?(name)
return false unless self.singleton_methods.include?(name)
return true if self.__send__(name).class == EBook
return false
end
def has_page?(name)
return false unless self.singleton_methods.include?(name)
return true if self.__send__(name).class == EPage
return false
end
class EPage
attr_reader :args
def initialize(arg_num,hold_args)
@event = {}
hold_arg_num = hold_args.map{|i| (i < 0 ? 0 : i.div(1))}
@args = [arg_num, hold_arg_num.min, hold_arg_num.max]
tmp = class << self;self;end
tmp.class_eval do
if hold_arg_num.min != hold_arg_num.max
define_method(:hold){|*args|
if args.size < arg_num + hold_arg_num.min || args.size > arg_num + hold_arg_num.max
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num + hold_arg_num.min}..#{arg_num + hold_arg_num.max})",
caller(1))
end
if args.size - arg_num == 0
@event[args].call if @event.has_key?(args)
else
if @event.has_key?(args[0...arg_num])
@event[args[0...arg_num]].call(*args[arg_num...args.size])
end
end
}
else
if hold_arg_num.min == 0
define_method(:hold){|*args|
if args.size != arg_num
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num})",
caller(1))
end
@event[args].call if @event.has_key?(args)
}
else
define_method(:hold){|*args|
if args.size != arg_num + hold_arg_num.min
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num + hold_arg_num.min})",
caller(1))
end
if @event.has_key?(args[0...arg_num])
@event[args[0...arg_num]].call(*args[arg_num...args.size])
end
}
end
end
define_method(:add_event){|*args, &block|
if args.size != arg_num
raise(ArgumentError,
"wrong number of argument (#{args.size} for #{arg_num})",
caller(1))
end
if block
@event[args] = block
else
@event.delete(args) if @event.has_key?(args)
end
}
end
end
end
end
end
singleton_methods
はレシーバの特異メソッドの名前(Symbol)の配列です。そのなかに含まれているかを見ればいいのです。
これで完成です。リファレンスもどきと使い方の例を載せておきます。
<Eventモジュールの構造>
Event
|
+―EBook ‥‥EPageの入れ物
| |
| +―EBook ‥‥EBook内にEBookを入れられる
| | |
| | ・
| | ・
| | ・
| |
| +―EPage ‥‥イベントフォーム。
|
+―EBook
|
+―EBook
・
・
・
<リファレンス>
Event
.book(name)
nameの名前でEventモジュールにメソッドを作成し、
新しく生成したEBookオブジェクトを返す。
[PARAM] name:
定義するメソッド名をStringかSymbolで指定する。
.has_book?(name)
nameの名前でEventモジュールにEBookが登録されているかを返す。
[PARAM] name:
調べる名前をSymbolで指定する。
EBook
#book(name)
nameの名前でselfにメソッドを作成し、新しく生成したEBookオブジェクトを返す。
Eventモジュールの同名メソッドと同じ動作。
[PARAM] name:
定義するメソッド名をStringかSymbolで指定する。
#page(name, arg_num, hold_arg_num)
nameの名前でselfにメソッドを作成し、
arg_num,hold_arg_numを渡して新しく生成したEPageオブジェクトを返す。
[PARAM] name:
定義するメソッド名をStringかSymbolで指定する。
[PARAM] arg_num:
イベントフォームで、
イベント発生源を指定するために必要な情報量を数値で指定する。
EPage#add_event, #holdの引数の数に影響する。
[PARAM] hold_arg_num:
イベントフォームで、
イベント発生時に送信する情報量(arg_numを除く)を数値の配列で指定する。
EPage#add_event, #holdの引数の数に影響する。
[SEE_ALSO] EPage#hold, EPage#add_event
#has_book?(name)
nameの名前で自身にEBookが登録されているかを返す。
[PARAM] name:
調べる名前をSymbolで指定する。
#has_page?(name)
nameの名前で自身にEPageが登録されているかを返す。
[PARAM] name:
調べる名前をSymbolで指定する。
EPage
#add_event(arg1[, arg2[, arg3 ...]]){|[hold_arg1[, hold_arg2 ...]]| ..}
情報とイベント(ブロック)を関連付ける。
ブロックの引数には、hold時に送信された情報がそのまま渡される。
このブロックの引数の個数に対してエラーチェックをしないが、
適切な個数でないとhold時にエラーが発生することに注意。[[trap:Event]]
[PARAM] arg1[, arg2[, arg3 ...]]:
EBook#page時に指定したarg_num個の、
イベント発生源を指定するために必要な情報を指定する。
[SEE_ALSO] EPage#hold, EBook#add_event
#hold(arg1[, arg2[, arg3 ...]][, hold_arg1[, hold_arg2 ...]])
情報に関連付けられたイベントを発生させる。
[PARAM] arg1[, arg2[, arg3 ...]]:
EBook#page時に指定したarg_num個の、
イベント発生源を指定するために必要な情報を指定する。
[PARAM] [arg1[, arg2 ...]]:
EBook#page時に指定したhold_arg_numの中の
(最小の数)個以上(最大の数)個以下の、イベントに送る情報を指定する。
[SEE_ALSO] EPage#add_event, EBook#page
<使い方>
Event.book(:group).page(:test, 1, [0,1])
Event.group.test.add_event(7){|str = "何も送られません"|
print "#{str}でした。\n"
}
Event.group.test.hold(6) #=> nothing will happen
Event.group.test.hold(7, "私は愚か") #=> 私は愚かでした。
#Event.group.test.hold() #=> ArgumentError
Scene(モジュール)
Sceneの条件分岐はMap/Battle/Animationそれぞれのモジュール/クラスの方で.update/.drawを定義しておけば簡単です。ついでに.init/.quitなども呼びましょう。今回はシンプルにコードだけ貼っておきます。
module Scene
@@scene = nil
def self.scene
@@scene
end
def self.scene=(v)
@@scene = v
end
def self.update
if (@@scene.methods + @@scene.private_methods).include?(:count_frame)
@@scene.__send__(:count_frame)
end
old_scene = @@scene
if (@@scene.methods + @@scene.private_methods).include?(:update)
@@scene.__send__(:update)
end
if @@scene != old_scene
if (old_scene.methods + old_scene.private_methods).include?(:quit)
old_scene.__send__(:quit)
end
if (@@scene.methods + @@scene.private_methods).include?(:init)
@@scene.__senf__(:init)
end
end
old_scene = @@scene
if (@@scene.methods + @@scene.private_methods).include?(:draw)
@@scene.__send__(:draw)
end
if @@scene != old_scene
if (old_scene.methods + old_scene.private_methods).include?(:quit)
old_scene.__send__(:quit)
end
if (@@scene.methods + @@scene.private_methods).include?(:init)
@@scene.__send__(:init)
end
end
end
end
EventScene(クラス)
この部分はフレーム毎に呼び出すだけで、自由な演出ができるようにします。こちらから何かすると単なる制限になってしまう気がしてなりません。こちらもコードのみです。
class EScene
attr_reader :update_block, :draw_block, :frame_counter
def initialize(&block)
init
@update_block = nil
@draw_block = block
end
def set_update(&block)
@update_block = block
end
def set_draw(&block)
@draw_block = block
end
def update
self.instance_exec(self.frame_counter, &@update_block) if @update_block
end
def draw
self.instance_exec(self.frame_counter, &@draw_block) if @draw_block
end
def init
@frame_counter = 0
end
def quit
end
private
def step
@frame_counter += 1
end
end
Battle(モジュール)
まだ考えられていません。RPGと言っても戦闘だけは何種類もあるので、その中で最低限の共通部分を模索しています。
Map(モジュール)
最後に持ってきたところからもうお分かりでしょうが、今回の山場です。
Mapは圧倒的に情報量が多いのです。それでも最終的にはゲームループに持ち込むのですが、
- Menuが出ているときは他の部分は停止させる
- Key入力に応じて主人公が移動する
- Main Characterと被らないようにNPCを動かす
- Main Characterが移動中ならスムーズにScrollする
- MenuのMenuに情報を伝える
など考慮しなければならない難問が多くあります。それに、マップチップ情報やNPCの数はもはや不特定多数です。
しかし!そんな不特定多数のオブジェクトの制御をするのに参考になるものを、DXRubyの作者mirichi氏が作っています。DXRubyWSです。具体的には、オブジェクトツリーで上位にあるオブジェクトから順々に情報を伝えています。
DXRubyのマニュアルの3.9 Spriteクラス
をご覧ください。次のような説明があります。
Spriteクラスはゲームのキャラを扱う場合の基本となるクラスです。描画位置、描画パラメータ、画像データを保持し、自分自身を描画する機能を持ちます。
実はこれ、よくよく注意して読むと面白いことが分かります。Sprite.update(ary)
とあるのにSprite#update
はないのです。これは、Spriteが「継承」され、#update
が定義されることを前提に作られているからです。
そしてDXRubyWSもそれを利用しています。WS(=Window System)を構成するあらゆるコントロールはこれを継承したWSControlまたはそのサブクラスなのです。そしてそれらを制御する際には、大きなものから小さなものへと情報が伝えられていきます。
これを応用するとどうなるでしょうか。ずばり、例えばMenuはMenuクラスで、NPCはNPCクラスでそれに特化したオブジェクトを生成し、Map.update
時に順に#update
が呼び出されるようにするのです。
この時、Menuが出ていたら、他のオブジェクトの#updateをしなければいいのです。
Key入力の情報を主人公の#update時に処理すればいいのです。
NPCの#updateの時に主人公の位置を調べればいいのです。
Map.update時に主人公の移動中かを調べればいいのです。
Menuオブジェクトは状況に応じてその配下のMenuオブジェクトの#update
を呼び出せば良いのです。
このように、それぞれの#update時に少し条件分岐をするだけで、全体では複雑な制御が可能になります。
コードを載せられなくてすみません。現在、この辺りを製作中です。詳しいことはまたこまごまとした記事になるでしょう。
終わりに
私は、私と同じようにRPGを作りたくなった人たちが再利用できるような、汎用なライブラリを作ろうとしています。先に話した制御構造を実現し、データを設定すれば基本的には使えるようにしたいのです。
それができれば、元々がプログラミング言語なので、RPGツクールのRGSSのようなものを作らなくても拡張性は最大です。すばらしいRPGが作られることでしょう。
現在、Data管理モジュール・Event管理モジュールが出来てはいます。Mapモジュールも少しずつ作っています。詳しくはダウンロードをご覧ください。長い時間がかかると思いますが、学生が終わるまでには作り上げたいと思います。
最後まで読んで頂きありがとうございました。