[Ruby][RGSS] GitでRPGツクールをバージョン管理する
RPGツクールをグループ開発に使うときに、Gitでバージョン管理したいと思ったので、ちょっと試してみた。
はじめに
- この記事はRPGツクールVX Aceを対象にしています。といってもRPGツクールXPやRPGツクールVXでもやることはそんなに変わらないと思うんだけど、RGSSのバージョンの差異でうまくいかないかもしれません。そのあたり試してないのであしらかじめご了承ください。
- この記事はGitが使えることを前提にしています。SourceTreeでのGitの使い方については記事があるので、Git使ったことないけど興味あるという方はまずそちらを読んでみてください。
RPGツクールのデータ構造
RPGツクールのデータファイルはバイナリで保存されている。アクターやエネミー、アイテムなどのデータベースはもちろん、スクリプトもバイナリ。
バイナリだと、差分が出せない。画像なら見比べればいいし、音声なら聞き比べればいいんだけど、データファイルだとそういうわけにいかない。
これは困るよね。
じゃあどうするか。データファイルをテキストデータにシリアライズすればいい。
さあバイナリの解析……する必要はない。RPGツクール側でデータファイルを読み込む処理があるので、それと同じことをすればRubyからデータが読めそう。やってることはごく単純で、データファイルをMarshal.loadしてるだけだ。
MarshalというのはRubyの組込ライブラリで、Rubyに最初から入っている。オブジェクトをバイナリに出力したり、出力したバイナリを読み込んでオブジェクトに復元したりできる。PerlのStorableのような、データの永続化のための機能だ。
というわけでためしにActors.rvdata2をMarshal.loadしてみよう。
data = File.open('Actors.rvdata2', 'rb') do |file|
Marshal.load(file.read)
end
これだけだと、エラーになる。Marshal.dumpされたデータは元のオブジェクトがどのクラスのインスタンスだったか覚えているので、復元するためにはそのクラス情報が必要になる。Actors.rvdata2の場合は、RPG::Actor
などがそう。そのクラス情報はどこにあるかっていうと、RGSS3の内側に定義されてるので、Actors.rvdata2を読み出すには、RGSS3が必要ってこと。
これだとツクールの外からじゃ読み出せないんじゃ?と思うかもしれないんだけど、悲観することはない。RPGツクールのヘルプを見ると、RGSS3内部のクラス定義についてちゃんと載っている。これを書き写せばいい(書き写したものは記事の最後に添付しておいた)。
さて、RPG
モジュールの内側については基本的にこれでよさそう。
ところがRPG::Map
とかRPG::Tileset
なんかを見るとTable
なるクラスを使っている。Table
はRGSSの内部で定義されてるんだけど、データ構造が分からないと読み出しようがない。
先人の知恵にあやかろう。mirichiさんが以前RGSS2の頃にRGSS2を知る(29)という記事を書いているのでこれを参考にする。
一行目はヘッダで、ここを見るとTable
をMarshal.dumpしたものは、どうやら_dump
したデータらしい。というわけなのでTable
クラス作ってTable#_dump
とTable._load
メソッドを定義する。
class Table
def initialize(data)
@num_of_dimensions,
@xsize, @ysize, @zsize,
@num_of_elements,
*@elements = *data
if @num_of_dimensions > 1
if @xsize > 1
@elements = @elements.each_slice(@xsize).to_a
else
@elements = @elements.map{|element|[element]}
end
end
if @num_of_dimensions > 2
if @ysize > 1
@elements = @elements.each_slice(@ysize).to_a
else
@elements = @elements.map{|element|[element]}
end
end
end
def _dump(limit)
[@num_of_dimensions,
@xsize, @ysize, @zsize,
@num_of_elements,
*@elements.flatten].pack("VVVVVv*")
end
def self._load(obj)
Table.new(obj.unpack("VVVVVv*"))
end
end
後でテキストにシリアライズするので、配列の配列に格納することにした。別に一次元の配列に入れてもいいんだけどね。
RPGモジュール内のクラスで使用されているのはこのほかにTone
とColor
があるんだけど、この二つもやっぱり_dump
したものなので、同じようにクラスを作ってやる。
class Color
attr_accessor :red, :green, :blue, :alpha
def initialize(data)
@red, @green, @blue, @alpha = *data
end
def _dump(limit)
[@red, @green, @blue, @alpha].pack("EEEE")
end
def self._load(obj)
Color.new(obj.unpack("EEEE"))
end
end
class Tone
attr_accessor :red, :green, :blue, :gray
def initialize(data)
@red, @green, @blue, @gray = *data
end
def _dump(limit)
[@red, @green, @blue, @gray].pack("EEEE")
end
def self._load(obj)
Tone.new(obj.unpack("EEEE"))
end
end
これでデータを読めるようになった。
シリアライズ
シリアライズにはYAMLを使った。
require 'yaml'
...
File.open('Actors.yml', 'w') do |file|
file.write(YAML.dump(data))
end
とかやればYAMLで出力できる。さすがにマップファイルあたりはYAMLにするのにすごく時間がかかるので、ほかの方式も視野に入れたほうがよさそう。JSONはそこそこ速いらしいのでいいかもしれない。
Scripts.rvdata2
実はスクリプトだけはちょっと事情が違っている。スクリプトをMarshal.load
すると、ID、スクリプトエディタ上のセクション名、そして文字列をセットにしたものの配列が返ってくる。この文字列はzlibで圧縮されているのでZlib::Inflateを使って展開する必要がある。展開するとスクリプトのテキストデータを得られるので、そのままrbファイルに吐き出せばいいだろう。
出力するときの名前はちょっと気を使う必要があって、たとえばセクション名をそのままファイル名にすると、同じセクション名のときに上書きしてしまう。
IDをファイル名にすると今度はどのファイルがどのセクションなのか分かりにくいのでそれも困る。
結局同じセクション名をファイルにして、同じセクション名なら同じ名前のファイルに追記されるようにした。追記するときにはスクリプトのIDと区切り文字を一緒に書き出しておいて、読み出すときはその区切り文字で区切って、IDごとに別々に読み出せばいい。
あとはgitでよろしくやるだけ
テキストデータにシリアライズできればあとはそのテキストデータをgitにコミットするだけ。
ブランチをチェックアウトしたりして、またrvdataに戻したいときは逆にYAMLを読み込んでMarshal.dumpすればいい。
ソース一式はgistにアップしたので、ご自由にお使いください。Ocraでexeにしてツクールのプロジェクトフォルダの下に入れるとはかどると思う。
おわりに
うちのメンバーはいまのところGitに不慣れなので実のところまだちゃんと運用できてない。とりあえず個人的に使って試してみている。YAMLは遅いのでそこがちょっと気になる。もうちょっと使ってみようと思う。