RPGツクールVXAceのスクリプト言語であるRGSS3(Ruby1.9.2p0 + ツクールライブラリ)に関する個人メモ。あくまでも個人メモなので管理人以外には編集権限はありません。

×


デバッグ向けコード

デバッグ用ログ出力&簡易ベンチ

  • デバッグ用にログ出力メソッドを追加しておくと捗る
LINE = "---------------------------------------------------------------------"
def pf(txt = LINE, *argv) # グローバルスコープで定義する事で全てのクラスから呼び出せる
  return unless $TEST                       # 開発環境以外では出力しない
  p sprintf(txt, *argv) unless argv.empty?  # 引数ありなら整形出力
  p txt.inspect if argv.empty?              # 引数なしならtxtをそのまま出力、txtも省略ならLINEを出力
end
  • 速度比較メソッド(簡易ベンチ)
    • RGSS3ではRuby標準添付ライブラリのBenchmarkモジュールを使用できないためベンチマークメソッドなどを自作する必要がある。
    • Ruby公式から1.9.2をダウンロードしてきて lib\benchmark.rb を丸ごとツクールのスクリプトエディタにコピペして使用するという荒業もある。
    • いずれにしても取れるデータ的にはさほど大きな違いは無いため、下記簡易ベンチでも十分な実用性はある。
def bench(name = "", count = 100000, output = false)
  a = []
  10.times do |j|
    start_time = Time.now
    count.times{|i| yield }
    a[j] = Time.now - start_time
    pf "%2d %.7fs", j, a[j] if output
  end
  pf "bench:#{name} %.7fs", a.average
end

# 使い方
bench("test1"){実行コード}         # 標準の使い方(ループ10万回x10回平均)
bench("test1", 10000){実行コード}  # ループ回数の指定

イベントスクリプトでエラー時にエラーしたイベントを知る方法

  • 通常、イベントスクリプトでエラーするとevalメソッドでのエラー扱いになり、どこがエラーしたのかわからない。
  • evalを書き換えてエラーメッセージに詳細なデバッグ情報を表示するようにできればデバッグが非常に捗る。
  • マップイベント、条件分岐、変数の操作、移動ルート、カスタム移動ルート、バトルイベント、コモンイベント、戦闘テストに対応。
#==============================================================================
# Script_Debug_Information
#==============================================================================
class Game_Interpreter
  alias _interpreter_setup :setup
  def setup(list, event_id = 0)
    _interpreter_setup(list, event_id)
    @common_id = 0
    if $game_temp.common_event_reserved?
      @common_id = $game_temp.common_event_id                                  # コモンイベント情報の保持
    elsif $game_party.in_battle
      @page = $game_troop.troop.pages.index{|p| p.list == @list}               # バトルイベントのページ情報
    else
      @page = $game_map.map.events[@event_id].pages.index{|p| p.list == @list} # マップイベントのページ情報
    end
    @page ||= 0
  end
  #--------------------------------------------------------------------------
  def command_117  # コモンイベント情報を参照可能に
    common_event = $data_common_events[@params[0]]
    if common_event
      child = Game_Interpreter.new(@depth + 1)
      $game_temp.reserve_common_event(common_event.id)
      child.setup(common_event.list, same_map? ? @event_id : 0)
      $game_temp.clear_common_event
      child.run
    end
  end
  #--------------------------------------------------------------------------
  def eval(script)  # スクリプトイベントエラー箇所の特定
    # evalメソッドを再定義しているのでKernelモジュールのメソッドを呼ぶ。変数参照のためにbindingを指定する。
    Kernel.eval(script, eval_binding, @title, 1)
  end
  #--------------------------------------------------------------------------
  def eval_binding  # Bindingオブジェクトでコンテキストをオブジェクト化して返す
    line = @index + 1
    code = @list[@index].code  # 後でスクリプト内で参照できるように変数に代入
    case code
      when 111 then name = :◆条件分岐
      when 122 then name = :◆変数の操作
      when 205 then name = :◆移動ルートの設定
      when 355 then name = :◆スクリプト
      when 655
        name = :◆スクリプト
        i = @index -1
        while @list[i].code == 655
          i -= 1
        end
        line = i + 1
      else name = "CODE:#{code}"
    end

    # スクリプト内で参照できるように短縮変数を設定 必要なければ削除してOK
    v = $game_variables
    s = $game_switches
    pl = $game_player
    pt = $game_party
    i = $data_items
    w = $data_weapons
    a = $data_armors

    # エラーメッセージに表示する文字列の設定
    @common_id ||= 0
    @page ||= 0
    if @common_id > 0            # コモンイベントなら
      c = $data_common_events[@common_id]
      evdata = format("CommonEvent:%03d[%s]", @common_id, c.name)
    elsif $game_party.in_battle  # バトルイベントなら
      t = $game_troop.troop
      evdata = format("troop:%03d[%s]", t.id, t.name)
    else                         # マップイベントなら
      ev = $game_map.events[@event_id]
      evdata = format("event:%03d[%s][%d,%d]", ev.id, ev.name, ev.x, ev.y)
    end
    
    if $BTEST  # 戦闘テストなら
      area = "BattleTest"
    else       # 戦闘テスト以外ならマップ情報取得
      area = format("map:%03d[%s]", @map_id, $data_mapinfos[@map_id].name)
    end
    
    @title = format("%s %s page:%d line:%d %s", area, evdata, @page + 1, line, name)  # エラー表示タイトル整形
    binding
  end
end

class Game_Map        # 詳細デバッグ用 アクセサ
  attr :map
end

class Game_Character  # 詳細デバッグ 移動ルート用
  def eval(code)
    i = $game_map.interpreter
    Kernel.eval(code, i.eval_binding, i.instance_eval{@title}, @move_route_index + 1)
  end
end
  
class Game_Event      # 詳細デバッグ カスタム移動ルート用
  alias _game_character_eval :eval
  def eval(code)
    if @move_route_forcing
      _game_character_eval(code)
    elsif @move_type == 3
      title = format("map:%03d[%s] event:%03d[%s][%d,%d] %s", @map_id,
      $data_mapinfos[@map_id].name, @id, name, x, y, :カスタム移動ルート)
      Kernel.eval(code, binding, title, @move_route_index + 1)
    end
  end
end

# ※というかこれ公式で最初から指定しておくべきじゃない?
# スクリプトの短縮変数利用例
v[20]    # 変数20番を参照
v[6] = 7 # 変数6番を7にセット
s[17]    # スイッチ17番を参照
v[17..19] = [@map_id, pl.x, pl.y] # マップIDとプレイヤー座標を変数17-19にセット 場所移動での利用を想定
v[17..19] = [@map_id, ev.x, ev.y] # マップIDとイベント座標を変数17-19にセット 場所移動での利用を想定
i[15] # ID:15のアイテムを参照
w[14] # ID:14の武器を参照
a[18] # ID:18の防具を参照
if pt.members_equip_include?(w[45]) # パーティ内の誰かが武器45番を装備していればtrue
イベント設定例エラーメッセージ(画像はページ番号実装前のもの)

イベントスクリプトでエラー時にエラーしたコードに飛べるようにする

  • 通常、イベントスクリプトでエラーするとevalメソッドでのエラー扱いになり、どこがエラーしたのかわからない。
  • 前項目の方法だと発生位置は分かるが、スクリプトエディタでエラーした時と異なり一発でエラー箇所に飛ぶことはできない。
  • スクリプトの内容自体は Game_Interpreter クラスに新しいメソッドを追加してそちらに記述し、イベントからはそのメソッドを呼ぶようにするとエラー箇所に飛ぶことが出来るようになる。
    • プログラマにとってはこちらの方がデバッグが楽な場合がある。もちろん前項と併用しても良い。
class Game_Interpreter
  def test # スクリプトから呼ぶメソッドを定義
    #ここに実際のコードを書き、イベントスクリプトには「test」とだけ書く
  end
end

エラーメッセージをデバッグログに出力する

  • 通常エラーが出るとそこでエラーダイアログが出てプレイが終了してしまう。
    • デバッグ中、ゲーム進行には影響がない部分でエラーが出た場合にエラーをログに出力してゲームを続行するには以下の方法を使う。
    • 主にログ出力部分で使うのが良い。または例外出力するコードが正常に動作するかをテストする際にも有効。
  • 注意点としてゲーム本編にかかわる部分のコードでは絶対に使わないこと。
    • エラーを握りつぶしてしまうことになるためバグを見逃しやすくなり、デバッグに支障が出る。
エラーが出る可能性のあるコード rescue puts $! # rescue修飾子でエラーをトラップし、puts $! でエラーメッセージをログ出力する

デバッグ中はRボタンですり抜けモード

  • ゲームパッドでテストプレイする場合、CTRLキーですり抜けるのが面倒。
  • R:ボタンなどにすり抜けキーを割り振っておけばデバッグが快適になる。
class Game_Player
  def debug_through? # デバッグ中のみR:ボタンでもすり抜け可能
    $TEST && (Input.press?(:CTRL) || Input.press?(:R))
  end
  alias _original_update_encounter  :update_encounter
  def update_encounter # すり抜け中はエンカウント無効
    return if debug_through?
    _original_update_encounter
  end
end

NilClassにデバッグ用メソッドを定義

  • nameメソッドやidメソッドなどツクールで頻繁に使う、特にデバッグログ出力に使いやすいメソッドに関してはNilClassに定義しておくとnil判定なしにデバッグコードを書けるので捗る事もある。むしろnilをログに出力できるのでわかりやすくなる効果もある。
    • あくまでもデバッグログでエラーしないもしくはわかりやすければそれでいい。そのため本来と型が違う不正な値を返した方が本来のコードにnilが紛れ込んでも動いてしまう不具合を未然に防げるので、あえてidの戻り値として文字列を返している。デバッグのみで使うならコレで問題ない。
    • 普通のプログラミングではこういうコードは必要ないしnilが入り込まないように注意すべきだが、RGSS3のデータをeachで回す時には役に立ったりする。
      • RGSS3で扱うデータベースの配列はもれなく[0]にnilが入っている。データベースGUI上のIDと対応させるためかほぼ全ての配列が[1]から始まっているため。これがeachで悪さをする。nilなのは[0]だけなのにnilを想定したコード(next if item.nil? 等)が必要になる。
      • データベース配列にeachをかけるような状況はほぼほぼデバッグ用途でしか発生しないので、それならNilClassで対応するのはあり。この場合、自分のコーディングミスでデータベースをnilで書き換えてしまった場合でも分かりやすくなる可能性がある。
  • デバッグ用のコードに毎回nilチェックを入れるのが面倒な場合にだけオススメ。
class NilClass
  def name
    'name:nil'
  end
  def id
    'id:nil'
  end
end






Ruby応用 RGSS3編

アクセスできないインスタンス変数やクラス変数へのアクセス

  • 例えばGame_Partyの @items は itemsメソッドによってラッピングされているためアクセサを指定しても参照できない。
    • こういう場合は Object#instance_variable_get や BasicObject#instance_eval を使うことで @items を直接参照できる。
    • 実行速度としては意外にも instance_variable_get が最も速い。attr_reader は若干遅く、instance_eval と instance_exec は倍の時間がかかる。
    • RGSS3の場合は公開スクリプトとの競合なども考慮するとアクセサを使いづらい環境にあるので、なおのこと instance_variable_get をお勧めする。
  • BattleManager の各種インスタンス変数のようにアクセサのない変数は instance_variable_get や instance_eval(instance_exec)で参照できる。
    • 参照先がクラス変数の場合は class_variable_get(class_eval、class_exec) を使用する。
    • インスタンス変数なら class_eval、class_exec も使用できる。class_eval は instance_eval(instance_exec)よりも速いが、class_exec はそれらよりも遅い。
# 例)Game_Party
p $game_party.items                            # RPG::Itemの配列を返す(ラッピング済み)
p $game_party.instance_variable_get(:@items)   # itemIDをキーとしたアイテムの所持数Hashデータを返す(ラッピング前)
p $game_party.instance_eval{@items}            # itemIDをキーとしたアイテムの所持数Hashデータを返す(ラッピング前)
p $game_party.instance_variable_get(:@weapons) # weaponIDをキーとした武器の所持数Hashデータを返す(ラッピング前)
p $game_party.instance_variable_get(:@armors)  # armorIDをキーとした防具の所持数Hashデータを返す(ラッピング前)

instance_variable_get("@items")      # 変数名でインスタンス変数の取得 変数が存在しない場合は nil を返す
instance_variable_get(:@items)       # シンボルでインスタンス変数の取得 変数が存在しない場合は nil を返す
instance_variable_set(:@items, var)  # インスタンス変数への代入

# 例)BattleManager
p BattleManager.instance_variable_get(:@preemptive) # 先制攻撃フラグの状態を返す
p BattleManager.class_eval{@preemptive}             # 先制攻撃フラグの状態を返す
p BattleManager.instance_eval{@preemptive}          # インスタンス変数なのでinstance_evalでも取得可能
# 先制攻撃や不意打ちのフラグはターン終了処理でオフになるため参照タイミングに注意

# 例)Window_MenuCommand
p Window_MenuCommand.class_variable_get(:@@last_command_symbol) # 最後に選んだコマンドのシンボルを返す
p Window_MenuCommand.class_eval{@@last_command_symbol}          # 最後に選んだコマンドのシンボルを返す

class_variable_get("@@last_command_symbol")       # 変数名でクラス変数の取得 変数が存在しない場合は nil を返す
class_variable_get(:@@last_command_symbol)        # シンボルでクラス変数の取得 変数が存在しない場合は nil を返す
class_variable_set(:@@last_command_symbol, :item) # クラス変数への代入
参照: #RGSS3におけるevalの使い分け

RGSS3におけるevalの使い分け

  • eval には、Kernel#eval BasicObject#instance_eval Module#module_eval(Module#class_eval) の3種類がある。(以下クラス名、モジュール名は省略)
    • 正確には Binding#eval もあるが、これは Kernel#eval の第二引数にBindingオブジェクトを渡すのと同じなので割愛する。
  • eval はRGSS3では一番多く使われている。
    • イベントのスクリプト、移動ルートのスクリプト、スキルのダメージ計算式、変数操作のスクリプト等。
    • evalは文字列を評価できるが、ブロックは評価できない。
    • privateメソッドなのでレシーバをつけて実行できない。
    • 第二引数にBindingオブジェクトを渡した場合、Bindingオブジェクトのコンテキストで実行される。
      • Binding は Kernel#binding を返したメソッドのコンテキストで実行できるため、そのメソッド内で定義したローカル変数なども使用できる。
    • 第三引数はファイル名、第四引数は行番号で、エラーが出た際のメッセージを変更できる。
      • 特にRGSS3ではイベントのスクリプトでエラーが出ると位置を特定できない問題があるので、これらを変更することは非常に有効。
  • instance_eval はそのオブジェクトのコンテキストで実行される。
    • instance_evalは文字列だけでなく、ブロックも評価できる。
      • ブロックに引数は渡せない。引数を渡したい場合は、instance_exec を利用する。
    • アクセサの無いインスタンス変数を操作したい場合などに有用。ただし単一のインスタンス変数を参照するだけなら instance_variable_get(set) の方が良い。
    • メソッドを追加するまでもない処理や、他のスクリプトと競合を回避したいときなどに使える。
  • module_eval(class_eval)はそのクラスオブジェクトのコンテキスト(クラス定義コンテキスト)で実行される。
    • module_eval(class_eval)は文字列だけでなく、ブロックも評価できる。
      • ブロックに引数は渡せない。引数を渡したい場合は、module_exec(class_exec)を利用する。
      • module_exec(class_exec)はprivateメソッドなので注意。
    • アクセサの無いクラス変数を操作したい場合などに有用。ただし単一のクラス変数を参照するだけなら class_variable_get(set) の方が良い。
    • Rubyではメタプログラミング(動的メソッド定義など)に活用されるが、RGSS3ではあまり有用に使える場面は少ないと思われる。
メソッド使い分け
Kernel#eval短いスクリプト文字列の実行や、エラー箇所を書き換えたい場合
主にデータベースで設定したスクリプトの構文解析に使用する
BasicObject#instance_eval特定のオブジェクトのコンテキスト内でスクリプトを実行したい場合
ブロックによる長文スクリプトやProcオブジェクトを実行したい場合
動的に特異メソッドを定義したい場合
主にprivateメソッドやアクセサのないインスタンス変数に連続でアクセスする際に使用する
インスタンス変数へのアクセスだけならinstance_variable_getやinstance_variable_setの方が良い
Module#class_eval
Module#module_eval
クラス定義コンテキスト(Classオブジェクトのコンテキスト)でスクリプトを実行したい場合
つまりクラスをレシーバにしてスクリプトを実行したい場合と言い換えることもできる
class_eval内で定義されたメソッドはそのクラスのインスタンスメソッドとなる
主に動的にインスタンスメソッドを追加するメタプログラミングで使用する RGSS3での出番はほぼ無い
Moduleにinstance_evalクラス定義コンテキスト(Classオブジェクトのコンテキスト)でスクリプトを実行したい場合
基本的にはclass_eval(module_eval)と同じように使えるがメソッドの定義先が異なる
instance_eval内で定義されたメソッドはそのクラスの特異メソッドとなる
主に動的にクラスメソッドを追加するメタプログラミングで使用する RGSS3での出番はほぼ無い
# 例)Game_Party
p $game_party.items                           # RPG::Itemの配列を返す(ラッピング済み)
p $game_party.instance_eval("@items")         # アイテムIDをキーとしたアイテム所持数のHashデータを返す(ラッピング前)
p $game_party.instance_eval{@items}           # instance_evalはブロックを評価できる
p $game_party.instance_eval{self.class}       # Game_Party を返す instance_evalはレシーバのスコープ内で実行される
p $game_party.instance_variable_get("@items") # 単一のインスタンス変数を名前で取得
p $game_party.instance_variable_get(:@items)  # 単一のインスタンス変数をシンボルで取得

a = {1=>1, 2=>1, 3=>1, 4=>1, 5=>1}
p $game_party.instance_eval{@items = a}         # instance_evalのスコープ内では外のローカル変数にもアクセス可能
p $game_party.instance_variable_set(:@items, a) # 単一のインスタンス変数への代入

p =   proc{@items}
l = lambda{@items}
p $game_party.instance_eval(&p)     # Procオブジェクトを渡すことも可能
p $game_party.instance_eval(&l)     # 引数無しlambdaを渡すとwrong number of arguments (1 for 0)エラーが出るので注意 ※後述
p $game_party.instance_exec(&l)     # instance_execは引数なしlambdaも問題なく渡せる
# instance_evalは暗黙でブロックに |obj| パラメータを渡すためlambdaではエラーが出る objにはレシーバのインスタンスが入る
# instance_execには暗黙で渡される引数は無いので見た目通りの引数で処理される
o = lambda{|obj| @items}
p $game_party.instance_eval(&o)     # 明示的に引数を定義したlambdaならエラーせずに通る

pp =   proc{|v, s| @gold = v * s}
ll = lambda{|v, s| @gold = v * s}
p $game_party.instance_exec(5, 6, &pp)  # instance_execなら引数付きのprocオブジェクトも渡せる
p $game_party.instance_exec(5, 7, &ll)  # instance_execはlambdaも問題なく渡せる
# 例)BattleManager
p BattleManager.module_eval{@surprise}     # BattleManagerの不意打ちフラグ(アクセサの無いインスタンス変数)を参照
p BattleManager.module_eval{@preemptive}   # BattleManagerの先制攻撃フラグ(アクセサの無いインスタンス変数)を参照
p BattleManager.class_eval{@preemptive}    # class_evalも同じ
p BattleManager.class_exec{@preemptive}    #=>エラー class_exec(module_exec)はprivateメソッドなのでレシーバ付きでは呼び出せない
p BattleManager.instance_eval{@preemptive} # レシーバがクラスならinstance_evalでもOK
p BattleManager.instance_exec{@preemptive} # instance_execはpublicメソッドなので問題なく呼び出せる
p BattleManager.instance_variable_get(:@surprise) # 単一のインスタンス変数なら専用メソッドで取得可能

p BattleManager.module_eval("@preemptive") # eval同様、文字列での指定も可能
p BattleManager.class_eval("@preemptive")  # class_evalも同じ

f = false
p BattleManager.module_eval{@preemptive = f}  # module_evalのスコープ内では外のローカル変数にもアクセス可能
p BattleManager.class_eval{@preemptive = f}   # class_evalも同じ
p BattleManager.instance_variable_set(:@preemptive , f) # 単一のインスタンス変数への代入
class_eval (module_eval) 内と instance_eval 内のメソッド定義の違い
  • class_eval (module_eval) 内でメソッドを定義すると、そのクラス/モジュールのインスタンスメソッドとして定義される。
  • instance_eval 内でメソッドを定義すると、そのオブジェクトの特異メソッドとして定義される。
    • つまりクラスに対して instance_eval メソッド定義を行うと、クラスメソッド定義の意味になる。
class MyClass
end

MyClass.class_eval do
  def class_eval_method; end
end
MyClass.module_eval do
  def module_eval_method; end
end
MyClass.instance_eval do
  def instance_eval_method; end
end

p MyClass.instance_methods(false)   #=> [:class_eval_method, :module_eval_method]
p MyClass.singleton_methods(false)  #=> [:instance_eval_method]

RGSS3での名前空間活用例

定数をまとめる
  • 例えばGame_BattlerBaseの定数リストのようにプリフィクス付きの定数を大量に定義するなら、プリフィクス名でモジュールを定義してincludeするか、そのままモジュール名::定数名で参照した方がスマートに管理できる。
# 修正前
class Game_BattlerBase
  FEATURE_ELEMENT_RATE  = 11              # 属性有効度
  FEATURE_DEBUFF_RATE   = 12              # 弱体有効度
  FEATURE_STATE_RATE    = 13              # ステート有効度
  FEATURE_STATE_RESIST  = 14              # ステート無効化
  FEATURE_PARAM         = 21              # 通常能力値
  FEATURE_XPARAM        = 22              # 追加能力値
  FEATURE_SPARAM        = 23              # 特殊能力値
  FEATURE_ATK_ELEMENT   = 31              # 攻撃時属性
  FEATURE_ATK_STATE     = 32              # 攻撃時ステート
  FEATURE_ATK_SPEED     = 33              # 攻撃速度補正
  FEATURE_ATK_TIMES     = 34              # 攻撃追加回数
  FEATURE_STYPE_ADD     = 41              # スキルタイプ追加
  FEATURE_STYPE_SEAL    = 42              # スキルタイプ封印
  FEATURE_SKILL_ADD     = 43              # スキル追加
  FEATURE_SKILL_SEAL    = 44              # スキル封印
  FEATURE_EQUIP_WTYPE   = 51              # 武器タイプ装備
  FEATURE_EQUIP_ATYPE   = 52              # 防具タイプ装備
  FEATURE_EQUIP_FIX     = 53              # 装備固定
  FEATURE_EQUIP_SEAL    = 54              # 装備封印
  FEATURE_SLOT_TYPE     = 55              # スロットタイプ
  FEATURE_ACTION_PLUS   = 61              # 行動回数追加
  FEATURE_SPECIAL_FLAG  = 62              # 特殊フラグ
  FEATURE_COLLAPSE_TYPE = 63              # 消滅エフェクト
  FEATURE_PARTY_ABILITY = 64              # パーティ能力
end
# 修正後
module Feature        # モジュールに定数をまとめる
  ELEMENT_RATE  = 11              # 属性有効度
  DEBUFF_RATE   = 12              # 弱体有効度
  STATE_RATE    = 13              # ステート有効度
  STATE_RESIST  = 14              # ステート無効化
  PARAM         = 21              # 通常能力値
  XPARAM        = 22              # 追加能力値
  SPARAM        = 23              # 特殊能力値
  ATK_ELEMENT   = 31              # 攻撃時属性
  ATK_STATE     = 32              # 攻撃時ステート
  ATK_SPEED     = 33              # 攻撃速度補正
  ATK_TIMES     = 34              # 攻撃追加回数
  STYPE_ADD     = 41              # スキルタイプ追加
  STYPE_SEAL    = 42              # スキルタイプ封印
  SKILL_ADD     = 43              # スキル追加
  SKILL_SEAL    = 44              # スキル封印
  EQUIP_WTYPE   = 51              # 武器タイプ装備
  EQUIP_ATYPE   = 52              # 防具タイプ装備
  EQUIP_FIX     = 53              # 装備固定
  EQUIP_SEAL    = 54              # 装備封印
  SLOT_TYPE     = 55              # スロットタイプ
  ACTION_PLUS   = 61              # 行動回数追加
  SPECIAL_FLAG  = 62              # 特殊フラグ
  COLLAPSE_TYPE = 63              # 消滅エフェクト
  PARTY_ABILITY = 64              # パーティ能力
end

class Game_BattlerBase
  def element_rate(element_id)
    features_pi(Feature::ELEMENT_RATE, element_id)  # モジュールの定数を参照
  end
end
既存のクラス名を使いたい
  • ツクールの例で言えば職業を表すクラス名としてClassが使われているが、これはRubyでもクラスのクラスとして定義されている。
    • VXAceではRPGモジュールの名前空間にこれらのクラスを定義する事で解決している。
    • こういった名前の重複を解決するのが名前空間の基本的な役割となる。
class RPG::Class < RPG::BaseItem  # RPGモジュールの名前空間でClassを定義
end

RGSS3でセーブデータに保存できないオブジェクト

  • VXAceではゲームのセーブデータにMarshalを利用しているため保存できないオブジェクトが存在する。
    • これらが保存される領域に含まれるとセーブが無言で失敗するだけでなく、保存しようとしたindexのセーブデータを即座に強制削除するという恐ろしい仕様が存在する。
    • いずれも通常ならば混じらないものではあるが、常に留意しておきたい。
  • Marshalでは無名クラスは保存できない
    • 通常の場合なら無名クラスを作る意味も無いが、誤った情報を元にしたコードによって無名クラスが紛れ込んでしまう事はありうる。例えば構造体の誤った定義方法や、class定義文を使わないクラス定義方法などで作られたクラスは無名クラスとなる。
    • 無名クラスはRGSS3プログラミングにおいては基本的に使う必要性が無いものなので、うっかり使わないように注意したい。
    • 暗黙の無名クラス(後述)なども存在するので注意。
  • Marshalは特異クラスを保存できない。特異クラスは暗黙の無名クラスである。
    • 特異メソッドを定義した場合、自動的に特異クラスにメソッドが定義されるので注意。
    • クラスへの特異メソッド定義(クラスメソッド定義)は問題ない。あくまでもオブジェクトに対して特異クラスを定義した場合に問題になる。
    • RGSS3ではオブジェクトへの特異メソッド定義は行わない方が無難。
      • ただしextendによる特異メソッド(オーバーコールメソッド)定義であればエラーにならずに正常に保存できる。この場合ロード時にメソッドが復元されないので注意が必要。
      • extendで保存されたクラスはextend定義を削除してしまうとロードできなくなる。取り返しが付かなくなる可能性があるので極力使うべきではない。
  • MarshalではMethod、Proc、MatchDataなどのインスタンスも保存できない
    • これらはインスタンス変数やクラス変数、グローバル変数、定数などに代入してしまうとゲームセーブ時に自動で保存対象となるため注意が必要。
    • デフォルト値をブロックで定義したHashのような内部にこっそりとProcオブジェクトを持つオブジェクトも注意が必要。こういったブロックとして渡したコードをあとから利用するようなクラスの場合は、大抵が内部的にProcオブジェクトを使用しているので、可能なら利用しないのが無難。
  • 詳細は公式リファレンスのMarshal#dumpを参照の事。
参照: Ruby1.9.2公式リファレンス Marshal#dump
参照: #セーブデータに保存される内容一覧表

ゲーム更新時の注意点

イベントの変更
ゲームの更新時にイベントの内容を更新した場合、ロード直後のマップのイベントはマップが切り替わるまで更新されない。
これはツクールの仕様で、セーブデータに現在のマップのイベント内容と並列処理のコモンイベントの内容が全て記録されているため。
これらの情報にはイベントの設定で設定した全ての情報が含まれるため、例えばショップの品揃えなども該当する。
インスタンス変数の追加
セーブデータに記録されるクラスにインスタンス変数を追加した場合、適切に初期化処理を行わないとセーブデータを開けなくなる。
この問題はインスタンス変数がセーブデータに存在しなかった場合を想定していないと顕在化する。
この場合、値参照時に @value ||= default_value の形で動的に初期化処理を行う事で解決できる。
セーブデータに保存できない情報
前述の #セーブデータに保存できないオブジェクト 参照。
セーブデータに記録したくない情報
セーブデータに記録したくない情報があった場合、Game_Tempクラスに情報を追加すれば保存されない。
利用時に値が存在しなくてもエラーにならないように注意。
セーブする変数を制限する
クラスのインスタンス変数の中でセーブしたい変数を制限したい場合は、marshal_dump と marshal_loadメソッドを定義する。
使用例はGame_Interpreterを参考にすると良い。






RGSS3実践コード

グローバルに参照可能な変数/メソッド

# デバッグフラグ
$TEST   # 実行テスト中ならtrue 戦闘テスト中もtrue game.exeから起動した場合はfalse
$BTEST  # 戦闘テスト中ならtrue

# スクリプトオブジェクト
$RGSS_SCRIPTS       # 全スクリプト配列      Data/Scripts.rvdata2
$RGSS_SCRIPTS[0]    # スクリプトリストの1行目のデータ [id, name, dump, script]
$RGSS_SCRIPTS[0][1] # スクリプトリストの1行目の名前 デフォルトだと▼ モジュール
$RGSS_SCRIPTS[0][3] # スクリプトリストの1行目のスクリプト文字列

# データベースオブジェクト
$data_actors        # アクター        Data/Actors.rvdata2        RPG::Actor の配列 
$data_classes       # 職業            Data/Classes.rvdata2       RPG::Class の配列 
$data_skills        # スキル          Data/Skills.rvdata2        RPG::Skill の配列 
$data_items         # アイテム        Data/Items.rvdata2         RPG::Item の配列 
$data_weapons       # 武器            Data/Weapons.rvdata2       RPG::Weapon の配列 
$data_armors        # 防具            Data/Armors.rvdata2        RPG::Armor の配列 
$data_enemies       # 敵キャラ        Data/Enemies.rvdata2       RPG::Enemy の配列 
$data_troops        # 敵グループ      Data/Troops.rvdata2        RPG::Troop の配列 
$data_states        # ステート        Data/States.rvdata2        RPG::State の配列 
$data_animations    # アニメーション  Data/Animations.rvdata2    RPG::Animation の配列 
$data_tilesets      # タイルセット    Data/Tilesets.rvdata2      RPG::Tileset の配列 
$data_common_events # コモンイベント  Data/CommonEvents.rvdata2  RPG::CommonEvent の配列 
$data_system        # システム        Data/System.rvdata2        RPG::System
$data_mapinfos      # マップ情報      Data/MapInfos.rvdata2      RPG::MapInfo のハッシュ 

# ゲームオブジェクト
$game_temp.common_event_reserved? # コモンイベントの実行が予約されているかどうか
$game_temp.reserved_common_event  # 予約されているコモンイベントデータの取得
$game_system.save_disabled      # セーブ禁止かどうか 禁止ならtrue
$game_system.menu_disabled      # メニュー禁止かどうか 禁止ならtrue
$game_system.encounter_disabled # エンカウント禁止かどうか 禁止ならtrue
$game_system.formation_disabled # 並び替え禁止かどうか 禁止ならtrue
$game_system.battle_count       # 戦闘回数
$game_system.save_count         # セーブ回数
$game_timer.sec # タイマーの残り秒数
$game_message.texts      # 現在表示中のテキスト
$game_message.choices    # 現在表示中の選択肢
$game_message.face_name  # 現在表示中の顔ファイル名
$game_message.face_index # 現在表示中の顔番号
$game_message.visible    # メッセージ表示中かどうか
$game_switches[n]         # 番号nのスイッチフラグ
$game_variables[n]        # 番号nの変数データ
$game_self_switches[key]  # keyのセルフスイッチフラグ keyは [map_id, event_id, 'A'..'D']
$game_actors[actor_id] # アクターリストのラッパー $data_actorsからGame_Actorクラスのインスタンスを生成する
$game_party.gold              # 所持金
$game_party.steps             # 歩数
$game_party.in_battle         # 戦闘中かどうか 戦闘中ならtrue
$game_party.exists            # メンバーが存在すればtrue
$game_party.members           # 戦闘中は戦闘参加メンバーリスト、移動中は全メンバーリスト
$game_party.all_members       # 全メンバーリスト
$game_party.battle_members    # 戦闘参加メンバーリスト
$game_party.leader            # 先頭メンバー
$game_party.items             # パーティが所持するアイテムリスト 大事なものも含む
$game_party.weapons           # パーティが所持する武器リスト
$game_party.armors            # パーティが所持する防具リスト
$game_party.equip_items       # 武器+防具リスト
$game_party.all_items         # アイテム+武器+防具リスト
$game_party.name              # パーティ名の取得(≒リーダー名)
$game_party.highest_level     # パーティ内で最も高いLv
$game_party.menu_actor        # メニュー画面で最後にカーソルを合わせたアクター
$game_party.target_actor      # メニュー画面でスキルやアイテムのターゲットになったアクター
$game_party.encounter_half?   # エンカウント半減ならtrue
$game_party.encounter_none?   # エンカウント無効ならtrue
$game_party.cancel_surprise?  # 奇襲無効ならtrue
$game_party.raise_preemptive? # 先制攻撃アップならtrue
$game_party.gold_double?      # ゴールド二倍ならtrue
$game_party.drop_item_double? # ドロップ二倍ならtrue
$game_troop.turn_count # 現在のターン数
$game_troop.members    # 敵メンバーリスト
$game_troop.troop      # グループ情報の取得
$game_troop.exp_total  # 倒した敵の経験値合計 実際に倒した敵のみ
$game_troop.gold_total # 倒した敵のお金合計 実際に倒した敵のみ
$game_map.boat             # 小型船の情報取得
$game_map.ship             # 大型船の情報取得
$game_map.airship          # 飛行船の情報取得
$game_map.map_id           # マップID
$game_map.display_name     # マップの表示名
$game_map.width            # マップの横マス数
$game_map.height           # マップの縦マス数
$game_map.loop_horizontal? # マップが横ループするかどうか
$game_map.loop_vertical?   # マップが縦ループするかどうか
$game_map.disable_dash?    # マップがダッシュ禁止かどうか
$game_map.encounter_list   # エンカウントする敵グループのリスト
$game_map.encounter_step   # エンカウント歩数
$game_map.overworld?       # フィールドタイプかどうか
$game_player.vehicle            # 乗り物の取得
$game_player.in_boat?           # 小型船に乗っていればtrue
$game_player.in_ship?           # 大型船に乗っていればtrue
$game_player.in_airship?        # 飛行船に乗っていればtrue
$game_player.normal_walk?       # 乗り物に乗っておらず強制移動中でもなければtrue
$game_player.dash?              # ダッシュ中ならtrue
$game_player.debug_through?     # デバッグすり抜け中ならtrue
$game_player.check_action_event # 決定ボタンでイベントを起動する
$game_player.get_on_off_vehicle # 乗り物を乗り降りする
$game_player.get_on_vehicle     # 乗り物に乗る
$game_player.get_off_vehicle    # 乗り物から降りる
$game_player.on_damage_floor?   # 現在位置がダメージ床かどうか

文章の表示イベントに式展開を導入

  • イベント「文章の表示」で #{式} の形式で式展開できるようになる。
    • 例えば文章中に所持金を表示する場合、一度変数に所持金を代入してから変数を呼び出す必要があった。式展開を使えば直接文章中に #{$game_party.gold} で所持金を表示できる。
class Window_Message
  def convert_escape_characters(text)
    result = super(text)
    result.gsub!(/\#\{(.*)\}/i){ $game_map.interpreter.instance_eval($1) }  # 式展開
    result
  end
end

イベント内で使える各種スクリプトの追加

class Game_Interpreter
  #--------------------------------------------------------------------------
  # ● 現在のイベントのインスタンス取得
  #--------------------------------------------------------------------------
  def self_event
    $game_map.events[@event_id] if same_map? && @event_id > 0
  end
end

メモ欄からメタタグを参照する(Mix-inモジュールのサンプル)

  • 仕様
    • タグは<>で囲む。タグ名には半角英数字だけでなく日本語表記にも対応。大文字小文字は無視する。
      • 例)<encount_off> <Encount_OFF> <エンカウントなし> いずれの書き方も対応可能
      • 囲い文字(<>)と区切り文字(:=半角スペース)は制御文字として使用しているのでタグ名には使用できない。
    • メタデータを取得したい場合は<force_critical:100>のように記述する。
      • 表記は<Force_Critical:100> <FORCE_CRITICAL=100> <force_critical = 100> などどれでも対応可能。
      • 区切り文字は順不同でいくつでも書けるので(意味はないが)<force_critical::== 100> のような書き方でも通る。
      • データは文字列、整数、実数での取得に対応。タグ名と異なり半角英数字、_アンダーバー、.ピリオドしか使えない。アンダーバーを使えるのでRubyの整数表記がそのまま使える。つまり <Price 5000_0000> のような記述も可能。
    • タグ名のみの存在チェックは include?("タグ名") exclude?("タグ名") の形で<>を含めずに指定する。
    • メタデータを取得する場合には get_meta("タグ名") get_meta_int("タグ名") get_meta_float("タグ名") の形で<>を含めずに指定する。
    • タグ名には文字列を直接書かずにシンボルの使用を推奨。文字列定数でも良いがシンボルの方がスマートかつ軽量&低負荷。
      • 例えば include?("encount_off") と include?(:encount_off) は同義。
      • 個人的には文字列定数の代わりに使うシンボルは全て大文字表記にしたい。include?(:ENCOUNT_OFF) のように。こうする事でシンボルの使用目的が明確になりコードの可読性が向上する。
    • 配列タグはRubyの配列同様 [] で囲い、, で区切る。例)[:skill_info, #{name}, #{id}, 50.5, -45, 0xff]
      • get_array(タグ名) で指定する。タグ名を第一要素とする配列を取得する。配列内の文字列は自動型変換&式展開される。メモ例なら get_array(:skill_info) で取得する。
      • 自動変換対象は、整数(10進、16進、8進、2進)、浮動小数点数、シンボル、式展開。それ以外は文字列として扱われる。
    • 式展開はMetaDataモジュールをincludeしたクラスのインスタンスのスコープで展開される。例えば前述のメモ例がスキルに書かれていた場合、#{name}はスキル名、#{id}はスキルIDになる。
      • 例えば使用後、別のスキル(一つ下のスキル)に変化するスキルとしてメモ欄に [:skill_change, #{id+1}] とすれば、get_array(:skill_change)[1] で変化後のIDとして一つ下のスキルのIDを得られる。
# メタデータ取得モジュールを定義
module MetaData
  # メモ欄取得用API include先で再定義して利用する
  def note_txt
    @note or ""   # デフォルトではインスタンス変数noteを参照する
  end
  # メモにtxtが含まれればtrue 大文字小文字は無視する
  def include?(txt)
    return false if note_txt.empty?
    /<#{txt}>/i === note_txt
  end
  # メモにtxtが含まれなければtrue 大文字小文字は無視する
  def exclude?(txt)
    !include?(txt)
  end
  # メモに含まれるメタデータ txt の値(String)を取得する
  def get_meta(txt, default = nil)
    return default if note_txt.empty?
    return $1 if /<#{txt}[=:\s]+([\w\.]+)>/i =~ note_txt
    return default  # メタデータが無ければdefaultを返す。
  end
  # メモに含まれるメタデータ txt の値(Integer)を取得する
  def get_meta_int(txt, default = 0)
    return default if note_txt.empty?
    value = get_meta(txt)
    value ? value.to_i : default  # メタデータが無ければdefaultを返す。
  end
  # メモに含まれるメタデータ txt の値(Float)を取得する
  def get_meta_float(txt, default = 0.0)
    return default if note_txt.empty?
    value = get_meta(txt)
    value ? value.to_f : default  # メタデータが無ければdefaultを返す。
  end
  # メモに含まれる配列データ(自動型変換&式展開対応)を取得する
  def get_array(txt, default = nil)
    return default if note_txt.empty?
    return to_a($1) if /\[([:]?#{txt}(,\s*.+?))\]/i =~ note_txt
    return default
  end
  # , 区切りの文字列を配列(自動型変換&式展開対応)に変換する
  RegexpFloat = /^[+-]?\d+(_\d+)*(\.\d+(_\d+)*)?([eE][+-]?\d+)?$/
  RegexpZero  = /^[+-]?0+[_0+]*$/ 
  RegexpHex   = /^[+-]?0x\h+(_\h+)*$/ 
  RegexpOct   = /^[+-]?(0o|0)[0-7]+(_[0-7]+)*$/
  RegexpBin   = /^[+-]?0b[01]+(_[01]+)*$/ 
  RegexpInt   = /^[+-]?(0d\d+|[1-9]\d*)(_\d+)*$/
  def to_a(txt)
    txt.tr("\s\[\]","").split(/,/).map do |v|
      case v
        when RegexpZero       then 0
        when RegexpHex        then v.to_i(16)
        when RegexpOct        then v.to_i(8)
        when RegexpBin        then v.to_i(2)
        when RegexpInt        then v.to_i(10)
        when RegexpFloat      then v.to_f
        when /\:([^\s\d]\S+)/ then $1.to_sym
        when /\#\{(.*)\}/     then instance_eval($1)
        else v
      end
    end
  end
end

# RPG::BaseItemにメタデータ取得モジュールをインクルード
# アクター、職業、スキル、アイテム、武器、防具、敵キャラ、ステートに対応
class RPG::BaseItem
  include MetaData
  # RPG::BaseItemは@noteでメモ欄を参照できるのでnote_txtメソッドの再定義は不要
end

# Game_Mapにメタデータ取得モジュールをインクルード
# マップの設定に対応 メモがあるのはRPG::Mapだが参照が面倒なのでGame_Mapに実装した方がよい
class Game_Map
  include MetaData
  # Game_Mapは@noteでメモ欄を参照できないので再定義
  def note_txt
    return "" if @map.nil? # 戦闘テストの場合nilが返ってくるので対策
    @map.note
  end
end

# RPG::Tilesetにメタデータ取得モジュールをインクルード
# タイルセットに対応 タイルセット情報はGame_Map.tilesetで取得可能
class RPG::Tileset
  include MetaData
  # RPG::Tilesetは@noteでメモ欄を参照できるのでnote_txtメソッドの再定義は不要
end

# RPG::Class::Learningにメタデータ取得モジュールをインクルード
# 職業の習得スキルに対応 習得スキル情報はActor.class.learningsで参照可能
class RPG::Class::Learning
  include MetaData
  # RPG::Class::Learningは@noteでメモ欄を参照できるのでnote_txtメソッドの再定義は不要
end
  • メモ欄の仕様
    • メモ欄は横45文字、縦12行の入力欄があるが、内部的には長さ無制限のStringとして扱われる。
    • 改行は \r\n になる。例えばメモ欄が1行目 123、2行目456だった場合、@noteで参照できる文字列は "123\r\n456" になる。
  • 使用例
    • 防具のメモ欄に <encount_off> を記入(大文字で <ENCOUNT_OFF> でも可)
    • Game_Player.update_encounterで$game_party.all_membersのfeature_objectsを調べ、item.include?(:ENCOUNT_OFF) がtrueならエンカウントせずにreturn
    • あくまでも一例。実際のコードは各自実装のこと。
  • 使用例2
    • スキルのメモ欄に <FORCE_CRITICAL:100> を記入
    • Game_Battler.item_applyのクリティカル判定の後に強制クリティカル判定を追加
    • item.get_meta_int(:FORCE_CRITICAL) で確率取得して成功なら強制的にクリティカルフラグをセット
    • あくまでも一例。実際のコードは各自実装のこと。
  • 使用例3
    • スキル20(流し斬り)のメモ欄に [:skill_info, #{name}, #{id}, 50.5, -45, 0xff]
    • $data_skills[20].get_array(:skill_info) で [:skill_info, "流し斬り", 20, 50.5, -45, 255] が得られる。

セーブデータへの保存方法(簡易版)

セーブデータの仕様
  • VXAceのセーブデータはRGSS3で記述されたクラスのMarshalデータで記録されている。
    • Marshalデータはクラス情報を保存、復元するためのRubyのエンコード/デコード機能。
    • クラスを復元するための情報としてインスタンス変数の内容は全て記録される。
    • RGSS3のMarshalフォーマットバージョンは4.8。
    • Marshal#dumpで扱えないオブジェクトは保存されない(公式リファレンス参照)。
      • 特に注意が必要なのは特異メソッド。下記ゲームオブジェクトには特異メソッドを定義してはならない。
  • 保存されるクラスは以下の表を参照。
  • 注意点として、ゲーム配布後にこれらのオブジェクトにインスタンス変数を追加してしまうと既存のセーブデータを読み込めなくなる可能性がある。
    • 必ず読み込めなくなるわけではないが注意が必要。詳細な条件は判明次第掲載予定。
    • 確実に読み込めなくなるのは、追加したインスタンス変数が原因でエラーが発生する場合。RGSS3ではセーブデータのロード処理中にエラーが発生するとそのセーブデータを「破損データ」として扱い、読み込みを拒否する仕様になっている。RGSS3におけるセーブデータのロードとは、セーブファイルに記録されたMarshalデータを元に保存時のオブジェクト状態を再構築する処理を指す。この処理の中で実行されるコード(例えば初期化されていないインスタンス変数の参照)でエラーが出ると再構築が失敗するため読み込めなくなる。特にスクリプトのデバッグ中は注意したい。
参照: Ruby1.9.2公式リファレンス Marshal#dump
セーブデータに保存される内容一覧表
グローバル変数クラス保存の有無、保存される情報
$game_tempGame_Temp保存されない
$game_systemGame_System全ての情報が保存される イベント「システム情報」関連の状態
$game_timerGame_Timer全ての情報が保存される イベント「タイマーの操作」のタイマーの状態
$game_messageGame_Message全ての情報が保存される 最後に表示されたメッセージなど
$game_switchesGame_Switches全ての情報が保存される 全てのスイッチの状態
$game_variablesGame_Variables全ての情報が保存される 全ての変数の状態
$game_self_switchesGame_SelfSwitches全ての情報が保存される 全てのセルフスイッチの状態
$game_actorsGame_Actors全ての情報が保存される 全てのアクターのリスト
Game_Actor各アクターの能力やスキル、装備などの状態も保存される
Game_Action各アクターの行動情報も保存される
$game_partyGame_Party全ての情報が保存される パーティ情報 所持金やメンバーなど
$game_troopGame_Troop全ての情報が保存される 現在(最後)のエネミーグループ情報
バトルイベントが実行中であればイベントデータ(Game_Interpreter)も保存される
通常、戦闘中に保存されることは無い(戦闘中はセーブ画面を開けない)
$game_mapGame_Map全ての情報が保存される 現在のマップの状態
イベントが実行中であればイベントデータ(Game_Interpreter)も保存される
Game_Interpreter実行中のイベントの内容が保存される
保存されるのは @depth, @map_id, @event_id, @list, @index, @branch
スクリプトで定義した独自のインスタンス変数は保存されない
Game_CommonEvent並列処理のコモンイベントは各内容と共に未実行のものも含めて全て保存される
つまり並列コモンイベント内容はバージョンアップ時に自動で更新されない
実行中ならイベントデータ(Game_Interpreter)も保存される
Game_Eventマップ内のイベントは全てのページ内容と共に保存される
つまり現在のマップのイベント内容はバージョンアップ時に自動で更新されない
並列処理のイベントが実行中ならイベントデータ(Game_Interpreter)も保存される
Game_Vehicleマップ上に乗り物があればその状態も保存される
Game_Screen現在の画面の状態も保存される
$game_playerGame_Player全ての情報が保存される プレイヤーが操作しているプレイヤーイベントの状態
Game_Followers隊列歩行ONならそのメンバーも保存される
Game_Follower隊列歩行ONなら隊列歩行時の各メンバーの状態も保存される
追加情報の保存をセーブデータに任せる
  • セーブファイルに保存したいデータが小さい場合、上記保存対象のクラスに新しくインスタンス変数を定義して代入しておけば勝手に保存してくれる。
    • 独自クラスを作らないような小規模なデータの場合に有効。
    • 統計や実績のようなデータはGame_Systemなどに新しいインスタンス変数を作って保存しておくと用途が分かりやすい。
    • 例えばアクターに新しい能力値を追加したい場合は、Game_Actorにインスタンス変数を定義すればいい。
  • 独自クラスを定義するような大規模なデータは後述の方法を使った方が良い。

セーブデータへの保存方法(独自クラス版)

  • 独自クラスを作ってそのデータをセーブデータに保存したい場合には以下の方法を使う。
# 独自クラスの定義
  # 今回は例としてオプション設定用クラスとする
  # プリフィックスは他のスクリプト製作者とクラス名がかぶらないように必ずつけること
  # 今回は例として Sample としておく
  # 基本的にスクリプト記述者名の短縮形が良い
class Sample_Game_Option
  # 独自クラスの定義コード
  def on_load  # ロード時に実行されるコード(サンプル)
  end
end

# DataManagerの再定義
  # aliasのプリフィックス部分は スクリプト記述者名_主要クラス名 としておくと問題が少ない
  # 今回は例として sample_option としておく
  # エイリアス名を _ で始め、スペースを2つ開けて、元の名前をシンボル表記するのは執筆者のマイルール
class << DataManager
  #--------------------------------------------------------------------------
  # ● 各種ゲームオブジェクトの作成
  #--------------------------------------------------------------------------
  alias _sample_option_create_game_objects  :create_game_objects
  def create_game_objects
    _sample_option_create_game_objects
    # 独自のグローバル変数に独自クラスのゲームオブジェクトを作成 重複注意
    $option = Sample_Game_Option.new
  end
  #--------------------------------------------------------------------------
  # ● セーブ内容の作成
  #--------------------------------------------------------------------------
  alias _sample_option_make_save_contents  :make_save_contents
  def make_save_contents
    contents = _sample_option_make_save_contents
    # 独自クラスの内容をセーブデータに追加 :option は保存用シンボル名なので重複注意
    contents[:option] = $option
    contents  # 戻り値がファイルに保存されるので最後に必ずcontentsを返す事 
  end
  #--------------------------------------------------------------------------
  # ● セーブ内容の展開
  #--------------------------------------------------------------------------
  alias _sample_option_extract_save_contents  :extract_save_contents
  def extract_save_contents(contents)
    _sample_option_extract_save_contents(contents)
    # 独自クラスの内容をセーブデータから読み込む データが無い場合には新規に作成
    # 新規作成コードを書いておかないと既存のセーブデータを読み込めなくなるので注意
    $option = contents[:option] || Sample_Game_Option.new
    $option.on_load  # 必要なら独自クラスのロード処理を呼び出す
  end
end

# 独自クラスで保存するデータのカスタム
  # インスタンス変数の中にセーブデータに保存したくないデータがある場合にはmarshalメソッドを再定義する
  # Marshalで保存できないクラスはエラーするため、インスタンス変数に保持している場合には必要になる
  # 実際のコード例はGame_Interpreterを参照すると良い
class Sample_Game_Option
  # 保存するデータのカスタム
  def marshal_dump
    # 保存したいインスタンス変数の配列を返す
    [@value1, @value2, @value3]
  end

  # 保存したカスタムデータの復元
  def marshal_load(obj)
    # 保存した配列からインスタンス変数に復元する
    @value1, @value2, @value3 = obj
  end
end

セーブデータのヘッダ情報の変更方法

  • セーブ選択画面で表示される情報を変更したい場合、セーブデータのヘッダを変更する必要がある。
  • 以下に例として、セーブしたマップのマップ名を表示する方法を掲載する。
# DataManagerの再定義
class << DataManager
  #--------------------------------------------------------------------------
  # ● セーブヘッダの作成
  #--------------------------------------------------------------------------
  alias _sample_make_save_header  :make_save_header
  def make_save_header
    header = _sample_make_save_header
    header[:map_name] = $game_map.display_name  # ヘッダーの追加
    header
  end
end

# Window_SaveFileの再定義
class Window_SaveFile
  #--------------------------------------------------------------------------
  # ● リフレッシュ
  #--------------------------------------------------------------------------
  alias _sample_refresh  :refresh
  def refresh
    _sample_refresh
    draw_map_name(0, 0, contents.width - 4)  # マップ名の描画
  end

  # マップ名の描画メソッド(右上に右寄せで描画)
  def draw_map_name(x, y, width)
    header = DataManager.load_header(@file_index)  # ヘッダーを取得
    return unless header && header[:map_name]
    draw_text(x, y, width, line_height, header[:map_name], 2)
  end
end

グローバルデータの保存方法

  • 全てのセーブデータ間で共有するグローバルデータを保存したい場合、セーブデータとは別のファイルに保存する必要がある。
  • 今回の仕様では通常のセーブロード時にグローバルデータもセーブロードする。
    • $global_data.saveを呼び出せば任意のタイミングで保存可能。変更後即時保存したい場合に使える。
# グローバルデータを保持するためのクラスを定義
class Sample_Global_Data
  attr_accessor :test1, :test2, :test3  # サンプル用の変数
  # 初期化処理
  def initialize
    @test1, @test2, @test3 = 0, 0, 0
  end

  # 任意セーブ用メソッド
  def save
    DataManager.save_global_data
  end
end

# DataManagerの再定義
class << DataManager
  GLOBAL_FILENAME = "Global.rvdata2"  # グローバルデータのファイル名
  #--------------------------------------------------------------------------
  # ● 各種ゲームオブジェクトの作成
  #--------------------------------------------------------------------------
  alias _sample_option_create_game_objects  :create_game_objects
  def create_game_objects
    _sample_option_create_game_objects
    # 独自のグローバル変数に独自クラスのゲームオブジェクトを作成 重複注意
    $global_data = Sample_Global_Data.new
    load_global_data  # ニューゲーム時にもデータを反映するように作成時に読み込んでおく
  end

  #--------------------------------------------------------------------------
  # ● セーブの実行
  #--------------------------------------------------------------------------
  alias _sample_save_game  :save_game
  def save_game(index)
    return false unless _sample_save_game(index)  # 通常のセーブが失敗したら終了
    save_global_data
  end
  
  #--------------------------------------------------------------------------
  # ● ロードの実行
  #--------------------------------------------------------------------------
  alias _sample_load_game  :load_game
  def load_game(index)
    return false unless _sample_load_game(index)
    load_global_data
  end

  # グローバルデータをファイルにセーブ
  def save_global_data
    # グローバルデータの保存に失敗したらデータ削除(既存の処理を踏襲)
    begin
      save_global_without_rescue
    rescue
      delete_global_file
      false
    end
  end

  # グローバルデータをファイルからロード
  def load_global_data
    # グローバルデータのファイルが存在しない場合は成功扱いで戻る
    return true unless FileTest.exist?(GLOBAL_FILENAME)
    load_global_without_rescue rescue false
  end

  # グローバルデータをファイルにセーブ(例外処理なし)
  def save_global_without_rescue
    File.open(GLOBAL_FILENAME, "wb") do |file|
      Marshal.dump($global_data, file)
    end
    return true
  end

  # グローバルデータをファイルからロード(例外処理なし)
  def load_global_without_rescue
    File.open(GLOBAL_FILENAME, "rb") do |file|
      $global_data = Marshal.load(file)
    end
    return true
  end
  
  # グローバルデータを削除
  def delete_global_file
    File.delete(GLOBAL_FILENAME) rescue nil
  end
end

乱数の保持

  • セーブデータをロードしても乱数が変化しない方法。ガチャ要素などでリセマラさせたくない場合などに使える。
    • 説明上、上記乱数を「保持乱数」と呼ぶ。
    • 乱用するとプレイヤーにストレスを与えやすいので注意。個人的には使用非推奨。
class Game_System
  def rand(v)
    @rand ||= Random.new  # Game_Systemクラスにインスタンス変数を定義して保存をシステムに任せる
    @rand.rand(v)         # Randomオブジェクトのインスタンスメソッドなので引数に範囲式も渡せる
  end
  # Randomオブジェクトのインスタンス変数を定義する事でseedなどがmarshalで記録される仕様を利用している。

  # 目的別に個別の乱数を使いたい場合はメソッドごとインスタンス変数を複数定義するだけでいい。
  # 例えば lvup_rand、gacha_rand のような感じで目的に応じた名前で定義すれば分かりやすい。
  def lvup_rand(v)
    @lvup_rand ||= Random.new
    @lvup_rand.rand(v)
  end
  def gacha_rand(v)
    @gacha_rand ||= Random.new
    @gacha_rand.rand(v)
  end
end
  • 使用例
    • 保持乱数を使いたい部分で、$game_system.rand を使用する。
    • Kernel.rand と異なり、Random#rand に値を渡しているので引数に範囲式を使える。
    • 例えばイベントで1〜100の保持乱数を使いたい場合、イベントの変数操作でシステムの乱数の代わりにスクリプト欄で $game_system.rand(1..100) と記述する。
  • ちなみに単純に乱数に範囲式を使いたい場合は、Game_SystemではなくGame_Tempに同じように定義すればセーブデータに保存されない。
    • この場合は $game_temp.rand を参照すればいい。
class Game_Temp
  def rand(v)
    @rand ||= Random.new
    @rand.rand(v)
  end
end

スイッチや変数を名前で参照/設定するコード

  • イベントの設定からスイッチや変数を参照する場合、イベント内にスイッチや変数の名前が表示されるため非常に分かりやすいが、スクリプト内からスイッチや変数を利用する場合にはスイッチ番号や変数番号でしか指定できないため可読性が低い。これを名前指定可能にするコード。
    • 変数はコードで利用する際の利便性のため、設定メソッドの他に加算メソッドも追加している。
    • 名前が存在しない場合はそれぞれ false もしくは 0 を返す。
  • 通常の利用範囲なら問題ないが、利用頻度が高く実行コストが問題になる場合にはインデックスの保持やハッシュ式にするなどの対応が必要。
  • 事前準備としてスイッチ名、変数名は同じ名前を使用しないルールを厳密にする必要がある。
    • Array.find_indexを利用しているため、同じ名前がある場合は最初に見つかった番号が返される。
class Game_Switches
  #--------------------------------------------------------------------------
  # ● スイッチの取得(名前で検索)
  #     name : 文字列、またはシンボルで指定する
  #--------------------------------------------------------------------------
  def find(name)
    id = $data_system.switches.find_index(name.to_s)
    id ? @data[id] : false
  end
  #--------------------------------------------------------------------------
  # ● スイッチの設定(名前で検索)
  #     value : ON (true) / OFF (false)
  #--------------------------------------------------------------------------
  def set(name, value = true)
    id = $data_system.switches.find_index(name.to_s)
    @data[id] = value if id
    on_change
  end
end

class Game_Variables
  #--------------------------------------------------------------------------
  # ● 変数の取得(名前で検索)
  #     name : 文字列、またはシンボルで指定する
  #--------------------------------------------------------------------------
  def find(name)
    id = $data_system.variables.find_index(name.to_s)
    id ? @data[id] : 0
  end
  #--------------------------------------------------------------------------
  # ● 変数の設定(名前で検索)
  #--------------------------------------------------------------------------
  def set(name, value)
    id = $data_system.variables.find_index(name.to_s)
    @data[id] = value if id
    on_change
  end
  #--------------------------------------------------------------------------
  # ● 変数への加算(名前で検索)
  #--------------------------------------------------------------------------
  def add(name, value = 1)
    id = $data_system.variables.find_index(name.to_s)
    @data[id] += value if id
    on_change
  end
end
# 使用例
if $game_switches.find(:森ボス撃破成功)
  $game_variables.add(:評判, +5)
else
  $game_variables.add(:評判, -1)
end
変数で管理している独自パラメータをスクリプトから操作する場合の小技
  • 独自パラメータを変数に持つ場合は、専用の参照メソッドを定義する事でスクリプト上の操作性や可読性を向上できる。
  • イベントからしか管理しないのであれば必要ない。あくまでもスクリプトから管理する場合の小技。
  • ただまあ・・・これを書けるプログラマであればもはやactorごとに独自のパラメータ追加して管理した方がはるかに扱いやすいとは思うんだけど。
    • 一応変数を使うメリットとしては、デバッグ中にF9で変数の中身をいじれるところぐらいか。それにしたってスクリプト書けるプログラマにはさほど(ry
class Game_Variables
  PARAM_SHIFT = 21  # 独自パラメータの開始変数id
  PARAM_WIDTH = 5   # 独自パラメータのアクター1人分の要素数
  def param(a, p = nil)
    @data[param_id(a, p)]
  end
  def param_set(a, p, v)
    @data[param_id(a, p)] = v
  end
  def param_add(a, p, v = 1)
    @data[param_id(a, p)] += v
  end
  def param_id(a, p = nil)
    p ? a * PARAM_WIDTH + p + PARAM_SHIFT : (a * PARAM_WIDTH + PARAM_SHIFT)..(a * PARAM_WIDTH + PARAM_SHIFT + PARAM_WIDTH - 1)
  end
end

#使用例
alias $var $game_variables
$var.param(0)              #=> [5, 10, 15, 20, 30] アクターID 0 の独自パラメータを返す
$var.param(1)              #=> [45, 0, 20, 10, 10] アクターID 1 の独自パラメータを返す
$var.param(0, 4)           #=> 30 アクターID 0 の独自パラメータ4 を返す
$var.param_set(0, 4, 100)  # アクターID 0 の独自パラメータ4 に100をセットする
$var.param_add(0, 1, 50)   # アクターID 0 の独自パラメータ1 に50を加算する
$var.param(0)              #=> [5, 60, 15, 20, 100]
$var[21..30]               #=> [5, 60, 15, 20, 100, 45, 0, 20, 10, 10] 変数21〜30に独自パラメータのデータが記録されているのがわかる

コモンイベント呼び出しを利用したアイテム作成

  • メニュー画面で使用されたアイテム効果「コモンイベント」のコモンイベント内で使用対象を取得するには下記の手順でイベントを組む。
    • 事前準備として変数のひとつ(ここでは仮に20番とする)に「コモンアイテムの対象」の名前を付けておく。
  • 例)使用した対象のレベルを1上げるアイテム
    • 変数の操作 [20]コモンアイテムの対象 代入 スクリプト $game_party.target_actor.id
    • レベルの増減 変数 [20]コモンアイテムの対象 増やす 定数 1
    • 欠点および改良点 アイテム効果「コモンイベント」はあくまでもアイテムの追加効果として扱われるためコモンイベントが呼び出された時点ですでにアイテムは消費されている。そのためレベルマックス時にはアイテムを無駄に消費してしまうので、対象のレベルが最大かどうかを判定して最大であればブザー音を鳴らして消費したアイテムを補填するなどの処置を行った方が親切。
      • Game_Actor.item_test を書き換えてレベルマックス時にはアイテムを使用できないように改造するのが最もスマートではある。
    • 注意点 対象が指定されていないアイテムの場合 $game_party.target_actor の情報は更新されないので注意。直前のターゲット情報のままになる。
  • コモンイベント呼び出しを行うアイテムは全て一つのコモンイベントを呼び出し、内部で条件分岐した方が管理は楽。
    • 呼び出したアイテムを判別するには $game_party.last_item.object をチェックする。名前判別にしておくと読みやすい。IDチェックでも可。
    • 条件分岐 スクリプト $game_party.last_item.object.name == "アイテム名"
  • 戦闘中に使用されたアイテムの場合、使用者や対象者を保持しておく必要があるため、スクリプト改造が必須。
    • $game_temp.common_user コモンイベント呼び出しアイテムを使用したBattler
    • $game_temp.common_target コモンイベント呼び出しアイテムの対象となったBattler
    • $game_temp.common_item コモンイベント呼び出しアイテム
    • 注意点 ダメージタイプなし、追加効果コモンイベントのアイテムを使用すると、コモンイベントが呼び出される前に攻撃失敗のメッセージが出る。これは対象に対して有効な効果がなにも設定されていないため。これを防ぐにはスクリプトを改造するか、何かしら別の有効な効果を併用する必要がある。
  • 上記のようにメニュー画面と戦闘中では扱いが異なるので、一括管理コモンイベントも二つに分けておくのを推奨。
    • もしくは戦闘中用のスクリプトはメニュー画面でも使用可能なので、共有化してしまうか。
#==============================================================================
# コモンイベント呼び出し時にBattler保存
#==============================================================================
class Game_Battler
  #--------------------------------------------------------------------------
  # ● スキル/アイテムの使用
  #    行動側に対して呼び出され、使用対象以外に対する効果を適用する。
  #    item_global_effect_applyにitemインスタンスを渡すように変更
  #--------------------------------------------------------------------------
  def use_item(item)
    pay_skill_cost(item) if item.is_a?(RPG::Skill)
    consume_item(item)   if item.is_a?(RPG::Item)
    item.effects.each {|effect| item_global_effect_apply(effect, item) }
  end
  #--------------------------------------------------------------------------
  # ● 使用効果[コモンイベント]
  #    アイテムの効果が使用者に対して発揮された場合のみ呼び出される(回復など)
  #    使用効果なしでコモンイベント実行のみのような場合には呼ばれない
  #    タイミングはコモンイベント実行前
  #--------------------------------------------------------------------------
  def item_effect_common_event(user, item, effect)
      $game_temp.set_common_user(user)
      $game_temp.set_common_target(self)
      $game_temp.set_common_item(item)
  end
  #--------------------------------------------------------------------------
  # ● 使用効果[コモンイベント]
  #    効果対象がいない場合のみ呼び出される
  #    タイミングは使用者行動後〜コモンイベント実行前
  #--------------------------------------------------------------------------
  def item_global_effect_apply(effect, item = nil)
    if effect.code == EFFECT_COMMON_EVENT
      $game_temp.set_common_user(self)
      $game_temp.set_common_target(nil)
      $game_temp.set_common_item(item)
      $game_temp.reserve_common_event(effect.data_id)
    end
  end
end

class Game_Temp
  def set_common_user(user)
    @common_user = user
  end
  def common_user
    @common_user || nil
  end
  def set_common_target(target)
    @common_target = target
  end
  def common_target
    @common_target || nil
  end
  def set_common_item(item)
    @common_item = item
  end
  def common_item
    @common_item || nil
  end
end

class Game_Battler
  #--------------------------------------------------------------------------
  # ● 使用効果[コモンイベント]
  #--------------------------------------------------------------------------
  def item_effect_common_event(user, item, effect)
    @result.success = true # コモンイベント呼び出し時はアイテム/スキル使用結果を成功にする。条件付で成否判定したい場合はそのコードを追記する
  end
end






RGSS3便利な小技

表記の桁を揃える

  • 似たようなコードを羅列する場合、桁を揃えておくと可読性とメンテナンス性が向上する。
    • 主に二重配列やハッシュのデフォルト値を設定する際などに有効。
    • 矩形コピペする際に特に威力を発揮する。VXAceのスクリプトエディタでも矩形コピペは使えるので活用したい。矩形選択はAlt+ドラッグ。
      • 余談だが矩形は「くけい」と読む。「たんけい」ではないので注意。矩形は全ての角が直角の四角形(正方形、長方形)を指す。
# 桁をそろえない場合
HASH_DATA = [ 
  :key_name => "内容",
  :long_key_name => "とてもとても長い文字列",
]

# 桁をそろえた場合
HASH_DATA = [
  :key_name      => "内容"                  ,
  :long_key_name => "とてもとても長い文字列",
]

モジュール定義の小技

  • モジュールの特異メソッドを定義する場合、selfではなくモジュール名で定義しておくと、そのままコピペできて便利。
    • モジュール関数化してMix-inすればモジュール名を付けなくてもコードを記述できるようになるが、メソッドの依存関係が不透明になるという欠点がある。メソッドの依存関係を明示するためにモジュール名を書くのであればモジュール関数化する必要もMix-inする必要も無い。実際RGSS3組み込みの関数型モジュールはMix-inでは一切使われていない。
      • Vocabモジュールのような定数管理用モジュールはMix-inして使う方が一般的だと思うがMix-inせずにVocab::〜と毎回書くことでVocabモジュールの定数を使用している箇所を検索しやすくなるというメリットもある。
    • 直接関係ない話だが、Game_BattlerBaseなどにある定数群は、拡張性を考えればモジュール化して一元管理し他のクラスにMix-inできるように分離した方が便利だと思う。
module MyModule
  def self.module_method1      # self で指定するとモジュール名を毎回記述するか関数化してMix-inする必要がある
  end
  def MyModule.module_method2  # モジュール名で指定すればそのまま丸ごとコピペできる
  end
  module_function
  def module_method3           # モジュール関数化
  end
end

class MyClass
  include MyModule           # モジュールをインクルードする
  def class_method
    module_method3           # includeすればモジュールのメソッドを自身のメソッドのように扱える
    MyModule.module_method3  # モジュール名を付ければメソッドの依存関係が分かりやすくなり検索性も上がる
  end
end

別名能力値にエイリアス

  • 能力値名を変更している場合、それに合わせて能力値にエイリアスを設定するとスクリプトで使いやすい。
    • 新しい名前でメソッドを定義するのも良い。
# 例)魔法防御を精神、幸運を器用さに変更した場合
# エイリアスを設定する例
class Game_BattlerBase
  alias mnd  :mdf  # mdf を mnd で参照可能にする
  alias dex  :luk  # luk を dex で参照可能にする
end

# 新しくメソッドを定義する例
class Game_BattlerBase
  def mnd;  param(5);   end    # 魔法防御 → 精神 
  def dex;  param(7);   end    # 幸運 → 器用 
end






WIN32APIを必要とするコード

INIファイルにアクセスするモジュール

  • 標準INIへの追加設定書き込み、読み込み用モジュール
    • 最小構成なので不正なデータには対応できずにエラー落ちする。運用側で注意が必要。
    • コメントで各APIのシグネチャを載せている。詳しくは欄外のリンクを参照。
    • 整数にのみ対応。実用レベルではコレで十分。
  • ちょっとしたグローバルデータの保存が必要になった場合に便利。
    • INIファイルは単なるテキストファイルなのでユーザーが簡単に内容を編集できる。
    • ゲーム進行に関わる重要なデータは記録するべきではない。
    • 本格的にグローバルデータを保存したい場合は前述の#グローバルデータの保存方法を使う方が良い。
  • なおWin32APIはRuby3.0で廃止された古いライブラリのためか公式ドキュメントでは404になっている。参考リンクとしてRuby1.8.7のドキュメントを提示しておく。
#==============================================================================
# Game.iniアクセス用モジュール
#==============================================================================
module Ini
  # ☆ INIファイル名の設定
  FILENAME = './Game.ini'  # VXAce標準のINIファイルを指定

  # ☆ INIファイルからIntegerの読み込み
  def Ini.read_int(section, key, default = nil)
    # UINT GetPrivateProfileInt(LPCTSTR lpAppName, LPCTSTR lpKeyName, INT nDefault, LPCTSTR lpFileName);
    gppi = Win32API.new('kernel32', 'GetPrivateProfileInt', 'ppip', 'i')
    gppi.call(section, key, default, Ini::FILENAME)
  end

  # ☆ INIファイルへStringの書き込み、成功したらtrue
  def Ini.write(section, key, data)
    # BOOL WritePrivateProfileString(LPCSTR lpAppName, LPCSTR lpKeyName, LPCSTR lpString, LPCSTR lpFileName);
    wpps = Win32API.new('kernel32', 'WritePrivateProfileString', 'pppp', 'i')
    wpps.call(section, key, data.to_s, Ini::FILENAME) != 0
  end
end
参照: Ruby 1.8.7 リファレンスマニュアル > class Win32API
参照: Learn Windows GetPrivateProfileInt 関数
参照: Learn Windows WritePrivateProfileString 関数

ウィンドウ位置保存/復元用モジュール

  • ゲーム画面のウィンドウの位置を復元するためのモジュール。
  • RGSS3ではアプリケーション終了時のイベントをトラップできないので、用例としてゲーム保存時に合わせてウィンドウ位置をINIファイルに保存する方法を載せている。
#==============================================================================
# ウィンドウ位置保存/復元用モジュール
#==============================================================================
module WindowResumer
  include Ini
  # uFlags(一部抜粋)
  SWP_NOSIZE   = 0x0001 # 現在のウィンドウサイズを維持(width,heightパラメータを無視)
  SWP_NOZORDER = 0x0004 # 現在のZオーダーを維持(hWndInsertAfterパラメータを無視)
  CLASS_NAME = 'RGSS Player'
  UTF8 = 'UTF-8'
  SJIS = 'SHIFT_JIS'
  
  # ☆ INIファイルからString(SHIFT_JIS)の読み込み ゲームタイトル取得用
  def self.read_ini(section, key, default = "")
    buffer = "\0" * 256
    # DWORD GetPrivateProfileString(LPCTSTR lpAppName, LPCTSTR lpKeyName, LPCTSTR lpDefault, LPTSTR lpReturnedString, DWORD nSize, LPCTSTR lpFileName);
    gpps = Win32API.new('kernel32', 'GetPrivateProfileString', 'pppplp', 'i')
    gpps.call(section, key, default, buffer, buffer.size, Ini::FILENAME)
    buffer.encode!(SJIS, SJIS).strip
  end
  
  # Win32API の FindWindow関数をフロー制御メソッドにラッピング
  def self.find_window
    sjis_title = read_ini('Game', 'Title')
    # HWND FindWindow(LPCSTR lpClassName, LPCSTR lpWindowName);
    fw = Win32API.new('user32', 'FindWindow', 'pp', 'l')
    @hwnd = fw.call(CLASS_NAME, sjis_title).nonzero? unless sjis_title.empty?
    p "window found #{sjis_title.encode(UTF8, SJIS)}" if @hwnd
    @hwnd ||= fw.call(CLASS_NAME, nil).nonzero?
    p "window class found #{CLASS_NAME}" if @hwnd
    @hwnd
  end
  
  # Win32API の GetForegroundWindow関数をフロー制御メソッドにラッピング
  def self.get_foreground_window
    # HWND GetForegroundWindow(VOID);
    gfw = Win32API.new('user32', 'GetForegroundWindow', nil, 'l')
    @hwnd = gfw.call.nonzero?
    p "get_foreground_window" if @hwnd
    @hwnd
  end

  # 外部から呼ばれる想定のメソッドはモジュール名をつけて区別する
  # INIファイルからウィンドウ位置を復元
  def WindowResumer.resume
    # フロー制御メソッドとフロー制御演算子でフローを単純化
    find_window or
    get_foreground_window
    return false unless @hwnd  # ホワイトリスト 有効なハンドルなら通す

    left = Ini.read_int('WindowPosition', 'left', -1)
    top  = Ini.read_int('WindowPosition', 'top' , -1)
    # BOOL SetWindowPos(HWND hWnd, HWND hWndInsertAfter, int left, int top, int width, int height, UINT uFlags);
    swp = Win32API.new('user32', 'SetWindowPos', 'lliiiii', 'i')
    # フロー制御演算子の評価値を戻り値とする
    left >= 0 && top >= 0 and
      swp.call(@hwnd, 0, left, top, 0, 0, SWP_NOSIZE | SWP_NOZORDER) != 0
  end

  # 現在のウィンドウ位置をINIファイルに保存
  def WindowResumer.save
    return false unless @hwnd  # 条件を満たしたら処理を進める
    
    rect_buffer = "\0" * 16
    # BOOL GetWindowRect(HWND hWnd, LPRECT lpRect);
    gwr = Win32API.new('user32', 'GetWindowRect', 'lp', 'i')
    if gwr.call(@hwnd, rect_buffer) != 0
      left, top = rect_buffer.unpack('l2')
      left2 = Ini.read_int('WindowPosition', 'left', -1)
      top2  = Ini.read_int('WindowPosition', 'top' , -1)
      # ウィンドウ位置が変わったときだけ保存する
      if left != left2 || top != top2
        Ini.write('WindowPosition', 'left', left)
        Ini.write('WindowPosition', 'top', top)
      end
    end
  end
end
参照: Learn Windows FindWindow 関数
参照: Learn Windows GetForegroundWindow 関数
参照: Learn Windows GetPrivateProfileString 関数
参照: Learn Windows SetWindowPos 関数
参照: Learn Windows GetWindowRect 関数
使用例
WindowResumer.resume  # rgss_mainの前にウィンドウ位置を復元する
rgss_main { SceneManager.run }
class << DataManager
  #--------------------------------------------------------------------------
  # ● セーブの実行
  #--------------------------------------------------------------------------
  alias __window_pos_resume_save_game  :save_game
  def save_game(index)
    return false unless __window_pos_resume_save_game(index)  # 通常のセーブが失敗したら終了
    WindowResumer.save  # ゲームの保存時に合わせてウィンドウ位置を保存
    true  # WindowResumer.saveの成否にかかわらず、RGSS3の保存結果を返す
  end
end




フリーエリア

管理人/副管理人のみ編集できます

広告募集中