あおたくノート

About

プログラミングの話とかします.
ウェブサイト >> aotak.me

[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#_dumpTable._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モジュール内のクラスで使用されているのはこのほかにToneColorがあるんだけど、この二つもやっぱり_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にしてツクールのプロジェクトフォルダの下に入れるとはかどると思う。

ソース(gist)

おわりに

うちのメンバーはいまのところGitに不慣れなので実のところまだちゃんと運用できてない。とりあえず個人的に使って試してみている。YAMLは遅いのでそこがちょっと気になる。もうちょっと使ってみようと思う。