はじめに
皆さん、RPGツクールをご存知ですか?
RPG制作ソフトであり、現在に至るまで複数のバージョンが発売されています。
ツクールの中でもRPGツクールXP
はRubyを拡張したスクリプト言語RGSS
を使用してゲームを制作可能なり、製作者が行える表現の幅が大幅に広がりました。
ツクールXPは2004年に発売されたかなり古いソフトですが今なお使用している人も少なくなく、筆者もRPGツクールXPで作られたとあるゲームのプレイやMODの制作を行なっています。
筆者がそのゲームのMODの制作を始めた理由はそのゲームが好きというのもありますが、以下が一番の理由でしょう。
ゲームの動作が重すぎる
MODを導入していないバニラ版はそれほど重くないのですが、MOD導入後は明らかに重いと感じました。
定期的に短いフリーズが入り、プレイに支障が出るレベルの重さでした。
ストレス無くヌルヌルなプレイを実現するため、ツクールの開発環境を構築してゲームがバニラ版と比べて重い原因を探りました。
結果的にゲームを重くしている要素がいくつも見つかり、ゲームの軽量化に成功したのでゲームが重かった原因と筆者がしたことを記事にまとめます。
アクターのステータスへの参照を減らす
味方キャラクターはHPやSPなどのステータスを持っています。
ただ、これらの参照は意外と重いらしく、参照時の重さが原因で以下の問題が発生していました。
- 全回復クリスタルに触れた時、例外なく数秒フリーズする
- キャラを移動させた時、たまに画面がカクつく
1. 全回復クリスタルに触れた時、例外なく数秒フリーズする
フィールド上にはクリスタルがあり、それに触れることで全回復とセーブができます。
このクリスタルにはかなりの頻度で触れるのですが修正前は以下の仕様であり、キャラのHPを大量に参照していて非常に重かったです。
- 全ての状態異常に対して、状態異常の回復処理を行う。状態異常の中には体力の最大値を減らすものもあるので、体力の最大値を回復させた上で体力も最大まで回復させる。
- 状態異常にかかっていなくても、この処理は実行されます。
- 非常にマニアックなゲームであり、130以上の状態異常があります。
- パーティの仲間全員に対して1の処理を行う。
- 1~4人です。
修正前のソースコードのイメージ
def remove_state(state_id)
if state?(state_id)
# 現在の体力が0のときの処理
if @hp == 0
# 色々と処理
end
# 状態異常を回復
@states.delete(state_id)
end
# HP および SP の最大値チェック
@hp = [@hp, self.maxhp].min
@sp = [@sp, self.maxsp].min
end
def recover_all
@hp = maxhp
@sp = maxsp
for i in 0..137
remove_state(i)
end
end
バニラ版では状態異常の数がそこまで多くなかったので回復時のフリーズは発生しませんでしたが、MODで状態異常が激増したので回復の度に100回以上体力を参照し、その結果一瞬フリーズするようになっていました。
この仕様を以下のように修正しました。
- 全ての状態異常に対して、状態異常の回復処理を行う。
- 全ての状態異常の回復後、体力の最大値を回復させた上で体力も最大まで回復させる。
- パーティの仲間全員に対して1の処理を行う。
修正後のソースコードのイメージ
def remove_state(state_id, is_recover_all = false)
if state?(state_id)
# 現在の体力が0のときの処理
if is_recover_all == false && @hp == 0
# 色々と処理
end
# 状態異常を回復
@states.delete(state_id)
end
# HP および SP の最大値チェック
if is_recover_all == false
@hp = [@hp, self.maxhp].min
@sp = [@sp, self.maxsp].min
end
end
def recover_all
@hp = maxhp
@sp = maxsp
for i in 0..137
remove_state(i, true)
end
end
この結果、体力と体力の最大値の参照回数が1%未満に激減しました。
実際のゲームの動作でも回復時の硬直が無くなり、自分でも驚いた記憶があります。
2. キャラを移動させた時、たまに画面がカクつく
10歩進むごとにHPとSPが自動回復する仕様がありますが、処理が無駄に分割されていてステータスへの参照が不必要に多くなっていました。
その所為で10歩ごとにたまに画面がカクつきました。
修正前のソースコードのイメージ
# stepは歩数
# dead関数では体力を参照しています
# HP回復
if step % 10 == 0
all_heal = $game_party.all_hp_autoheal
for actor in $game_party.party_actors
actor.hp += actor.hp_autoheal unless actor.dead?
actor.hp += all_heal unless actor.dead?
end
end
# SP回復
if step % 10 == 0
all_heal = $game_party.all_sp_autoheal
for actor in $game_party.party_actors
actor.sp += actor.sp_autoheal unless actor.dead?
actor.sp += all_heal unless actor.dead?
end
end
以下のようにステータスの参照をなるべくまとめ、キャラクター1人あたり8回から3回に減らしました。
その結果、10歩ごとの画面のカクつきが以前より無くなりました。
修正後のソースコードのイメージ
if step % 10 == 0
all_heal_hp = $game_party.all_hp_autoheal
all_heal_sp = $game_party.all_sp_autoheal
$game_party.party_actors.each do |actor|
unless actor.dead?
actor.hp += actor.hp_autoheal + all_heal_hp
actor.sp += actor.sp_autoheal + all_heal_sp
end
end
end
画面の描画処理をなるべく減らす
ゲームでメニュー画面を開くとパーティメンバー全員分の体力ゲージ、キャラの立ち絵、キャラの名前などのステータス情報の枠が表示されます。
枠はbitmapを複数個使用して表示されるのですがツクールでのbitmap(特にテキスト)の描画はかなり重く(参考)、メニューを開くのに1秒ほど時間がかかります。
なので、書き出し処理を使い回せる所は使い回すようにしました。
描画される内容自体は変わりませんがbitmapの読み込み処理が幾分か軽減されたので、メニューの表示完了まで0.1秒ほど早くなりました。
また、ゲームではパーティメンバーの交代機能があり、メニューを開けばいつでも交代を行えます。
その際にステータスの枠の表示も更新しますが、修正前は交代に無関係のキャラの枠も再描画していました。
描画内容は変わりませんが描画処理自体は走っており、交代完了まで画面が1秒ほど硬直していました。
これを交代するメンバーの枠のみ再描画するように修正し、硬直時間を半分ほどに減らしました。
配列の自動拡張を行わせない
大体のプログラミング言語は配列の長さ以上の値のindexを参照した時、エラーを出すかnullを返すと思います。
しかしRubyは以下のように、配列が自動的に拡張されます。
array = [0, 1, 2]
array[5] = 5
print array # [0, 1, 2, nil, nil, 5]
ゲームでは起動時に全キャラ(500人以上)のセリフ(一人一人かなりの量がある)を配列に代入します。
バニラ版では特に問題は無かったのですが、MODではキャラが増えた影響で最初に用意された空の配列の外の要素にセリフを代入しようとし、その度に配列が自動拡張されていました。
配列の長さも長く、1つ1つの値のサイズも大きい影響で自動拡張処理は非常に重い物になっており、MOD版はゲームの起動速度(数十秒ほど)が明らかに遅かったです。
そこで、セリフの空の配列の長さをキャラの人数に合わせて自動設定する機能を作成しました。
その結果、配列の自動拡張は発生しなくなり、起動速度もバニラ版と対して変わらないレベル(数秒ほど)まで改善しました。
おわりに
この記事でやったことから、以下の3つを学びました。
- hpとspの参照は重め
- これは筆者が触っているゲームだけかもしれません
- bitmapの描画はかなり重い
- 特にテキスト
- 配列の自動拡張は配列の内容によっては重くなる
パフォーマンスを最優先にしたり、元々のコードが密結合のために修正後のコードがSOLID原則をあまり順守していない物になっていますが、ヌルヌルなゲームプレイを実現できたので個人的には満足です。
皆さんももし、ツクールXPを触って重いなと思ったら上記の3つを確認すると良いかもしれません。