神無月サスケの波瀾万丈な日常

神無月サスケのツイッター(@ktakaki00)を補完する長文を書きます。

ムンホイXPの地味な技術:その1.軽量化

ムンホイXPは、実は結構軽いです。どのくらい軽いかというと、RPGツクールXPのデフォルトの推奨環境を下回っていても、それなりに動くくらいです。他のツクール作品をプレイしてかくかくした経験がある人でも、本作でかくかく処理落ちがしなかった、という人は結構いるんじゃないでしょうか。これは、独自の軽量化を施しているからなのです。

軽量化を施した経緯

そもそも、じゃあなんで本作は軽量化を施したのでしょうか。それは、開発当初の共同制作者の開発環境にあります。その話をしましょう。

当時の僕の共同制作者は、過半数RPGツクールXPの必須動作環境(PentiumIII 800MHz)以下の環境でした。僕がムンホイのフィールドのテストマップを半分ほど作り、テストしてもらっていると、こういう人がいました。「バージョンアップごとに、どんどん重たくなっていく。うちのPCでは重すぎて動かせない。自分はテストプレイに加われないかもしれない」と。僕のPCは当時は結構ハイスペックな物だったので重たいなんて思いもよらなかったのです。

それを聞いて僕は思いました。「RGSSには軽量化の余地はあるのでは?」と。当時、僕はRGSSを解析していたのですが、どうも最適化されていない部分を発見していました。「僕ならこうするのに」と。でも、こうなっているのだから、これが最善なのだろう、と思っていました。しかし、それを最適化したら、もしかしたら、軽くなるかもしれない、と思いなおしたのです。僕はさっそく、自分が感じた部分の軽量化を施してみました。

そうしたら「重くて動かせない」と言っていた彼はこう言ってきました。「凄い!ほとんどかくかくしなくなりました!」と。そうです、軽量化は功を奏したのです。

2005〜2006年当時、ネット界隈でも「RPGツクールXPは重い」という意見が支配的でした。フリゲをやっている人でそれなら、拙作をプレイする、フリゲを普段やらない層なら、なおさらPCのスペックは低いはず。ならば、まず、可能な部分は軽量化しておかないと「重たい」と言われて飛びついてはもらえないと思ったのです。

こういうノウハウは、一度確立しておけば、他の作品でも流用できるということもあり、僕は軽量化ノウハウの確立を最優先しました。なぜなら、当時のRGSS界隈を見ても、「ウィンドウの改良」とか「自作戦闘」だとか、演出の改良ばかりで、軽量化はほとんど扱われておらず、皆、「俺の作品をやりたいならてめえでいいPC準備しろ」と言わんばかりでした。自分でやるしかないと悟ったのでした。

皮肉なことに、ムンホイXPの公開は2011年となり、2005年当時のPentiumIIIから、CPUのメインは2〜3世代進歩し、僕の施した軽量化はなくても、それなりに動く環境になってしまいました。とはいえ、未だに低スペックのPCでプレイする人は少なくなく、そういう人が軽量化を評価してくれたことは嬉しかったです。

この項では、ムンホイXPが施した軽量化のうち、特に効果があった、しかもRGSS3でも導入されていないようなものを中心に紹介していこうと思います。

まず最初に:RGSSに頼らない軽量化

最初に僕が取り掛かるべきだと思うのは、スクリプトではない部分の軽量化です。具体的にはマップやイベントの置き方の無駄をなくすことで軽量化するということです。実はこれが一番効果的だったりしますので、これをまず最初に紹介します。

必要ない場面ではパノラマを切る

RPGツクールXPでは、パノラマはタイルセット単位でのということもあり、必要のないマップでもパノラマが表示されていることもあるでしょう。実はパノラマが表示されない場合でも、システムはまずパノラマを描画してから、タイルセットを描画しています。このため、パノラマ描画の分だけ、処理が重たくなっているのです。このため、必要のないマップでは、タイルセット指定のパノラマを切ってしまいましょう。

具体的には、マップ読み込み時に自動実行イベントを実行し、そこでパノラマの設定を切るのです。これだけでも相当違ってくるはずです。

見えない部分のタイルは(通行判定以外は)置かない

RPGツクールXPでは、3層のタイルマップ描画が可能です。このため、タイルマップを重ねてマップを描く人が多かったと思います。しかしここに罠があります。重なって画面に見えない部分であっても、律儀に描画しているわけで、その分重たくなります。

もし、画面に見えず、移動判定にも使用しないタイルがあったら、それらは極力置かない方針にしてみましょう。ちりも積もれば山となるで、かなり軽くなります。

ムンホイXPで参考になるのは、フィールド画面(もとまちタウン、もとまちシティ)です。かなり画面が広く、イベントも多くて重たくなっていましたが、余計なタイルを描画しない構造に変えることで、かなり軽くなりました。

必要のないイベントは「一時消去」する

これはどのツクールでも共通ですが、マップを出るまでもう起きるはずのないイベントは、「一時消去」するといいです。多くの人はページを切り替えて透明のページを表示させているでしょうが、「一時消去」すると、数々の処理をパスするため、それだけでかなり軽くなります。さらに後述する今回僕が導入した手法を追加すると、さらに軽くなります。

軽量化手法その1:イベントの位置情報テーブルを作る

まず、RGSS1のプリセットスクリプトの通行判定、Game_Map#passable および Game_Character#passable を見てください。以下のような部分が見られます。


# 全イベントのループ
for event in $game_map.events.values
# イベントの座標が移動先と一致した場合
if event.x == new_x and event.y == new_y

すなわち、マップ上の全てのイベントと位置を照合し、一致するイベントに対して処理を行っているのです。

しかしこのループ、イベントが増えるごとに、処理が増えていくのは明白です。イベントnに対して、O(n)(nのオーダー)で処理が増えて行きます。

しかも、毎フレーム、(1(プレイヤー)+移動するイベントの数)だけ、この処理が行われているのです!マップにN分の1の割合で自律移動するイベントがあった場合、全体的な処理は、O(n^2)(nの2乗のオーダー)で増えることになってしまいます。

しかし、もし、「それぞれの座標にあるイベントのテーブル」が存在するとしたらどうでしょう?例えば、{ イベントの座標 => そこにあるイベントIDの配列 }といったハッシュがあれば、O(n)のループをする必要がなくなり、O(1)で取り出せることになります。全体的な処理も、O(n^2)から、O(n)になりますよね。

以上の処理を実現するために、ムンホイXPでは、EventPositionTable クラスを実装しました。具体的には Hash のサブクラスであり、前述の発想にもとづいています。これのインスタンス $game_map.event_at を導入することで、イベントを見るループを全て、このハッシュを参照する処理に置き換えました。これらのキーワードでスクリプトを検索すると、メカニズムが分かると思います。

この軽量化が効果を施す例

この軽量化は、フィールドマップのように、イベント数が多いマップで如実に効果を発揮します。

あなたの制作でも、イベントが100個を超えたあたりから、如実に移動時にカクカクし始めたのではないでしょうか。僕もそうでした。なにしろイベント数nに対し、n^2のオーダーで処理が増えていたのだから当然です。この軽量化を施すことで、nのオーダーになるため、非常に軽くなります。

結果、ムンホイXPでは、最大で180個のイベントがあるマップがあります(ぜのんの町のフィールドがそれです)が、それほど重たくなっていません。その軽さに一番貢献しているのは、この処理なのです。

Hashで実装する理由

さて。この実装に、僕は Hash を使いましたが、なぜなのでしょう。似たようなメカニズムを見たことがありますが、その人は RGSSのゲームライブラリの Table クラスを使っていました。一見この方が効率がよさそうです。しかし、Tableクラスが値として取れるのは、符号付16ビットの数値(-32768〜32767)だけです。これだと問題が起きることがあります。それは、複数のイベントが、同じ座標に位置した場合です。

イベントには「すり抜け可能」オプションがあります。これがONの場合は、複数のイベントが同じ座標に位置することが可能です。TableクラスでひとつのイベントのIDだけを記録する方法だと、この場合に対処できません。それで Hashを使い、値はIDの配列を取るようにしたわけです。

RGSS2以降で実装する場合

ちなみにムンホイXPはツクールXP、すなわちRGSS1で実装していましたが、同じ考えはRGSS2以降でも可能であり、さらに簡単に実装が可能でしょう。

それは、Game_Map#events_xy メソッドが導入されたことです。このメソッドは指定座標に存在するイベントの配列を取得するものであり、ここを 前述の Hash を用いたメソッドに書き換えることにより(そしてイベント移動時に Hash の情報を書き換えることにより)、よりコンパクトに実装が可能です。ひょっとしたらこの軽量化スクリプトは探せばあるかもしれませんね。

なぜRGSSはこの軽量化を見送ったか

さて、それでは大きな疑問があります。「なぜ、こんなに簡単に実現できる軽量化を、RGSSは見送ったのか」ということです。現にこの軽量化はRGSS3でも実装されていません。

これには理由はあります。ずばり、「この軽量化を行うと、拡張性が著しく低下するから」です。ここを説明しましょう。

イベントの位置座標を、別のテーブル(EventPositionTable)に保存するということはすなわち、イベントのxとyの位置情報が、Game_EventクラスとEventPositionTableという、複数の場所に保存されていることになります。すなわち、スクリプトの拡張を考える人は、これらのデータの一意性を意識しなければならなくなります。もし、Game_Eventのxやyの値を不用意に書き換えた後、EventPositionTableのことを忘れて、書き換えなかったらどうなるでしょう……たちまちデータの整合性が崩れてしまうのです!

このように、スクリプトの拡張性を吟味したRGSSでは、このような「データの一意性」を崩す軽量化を考えるよりも、若干重たくなっても、拡張の容易さ、安全さを選択したのだと言えます。逆に言うと、この軽量化を選択する人は、スクリプトでxやyを直接いじるような拡張を本当にやらないと言えるのか、そういった拡張を行う場合、EventPositionTable 側への反映を意識して行う必要があるといえます。

つまり、軽量化全体にいえることですが、1つ軽量化を施すというのは、ひとつの機能を追加する場合と同様に、拡張性を1つ損ねてしまうといえます。拡張が不可能にはならないのですが、拡張に際して、軽量化を施した部分を意識して組むなど、余計な労力を要求するようになります。だから、軽量化についても、むやみに導入するのではなく、「差し迫って必要のない軽量化は導入しない」「軽量化を施す場合は、それによって拡張性を少なからず犠牲にする」ということを肝に銘じる必要があります。本作では、リメイクということもあり、余計な拡張は一切必要ないと断定できたからこそ、大胆な軽量化が出来たのです。あなたの作品ではどうでしょう。慎重に吟味してください。

軽量化手法その2:不要なスプライトを作成しない

次に効果が大きかった軽量化は、「不要なスプライト(Sprite オブジェクト)を作成しない」ということでした。

まず最初に知っていてもらいたいのは、Sprite およびそのサブクラスのオブジェクトは、生成されたときにシステムに登録され、Graphics.update が呼ばれた時に一気にそれぞれのスプライトの更新が反映される形になっています。つまり、スプライトは、オブジェクトを生成しただけで、特に表示などを行わなくても、システムに多少の負荷をかけているのです。

つまり、必要のないスプライトを最初から生成しないことが出来たら、それだけで軽量化が出来ることになります。

RGSS3で導入された「スプライトを生成しない技術」

RGSS3では一部、この手の軽量化が採用されました。それは、天候(RGSS3:Spriteset_Weather, RGSS1:RPG::Weather)とピクチャ(Sprite_Picture)です。いずれも、RGSS2までは、必要な最大数(天候は40個、ピクチャはRGSS1では50個)を最初に作成しており、それだけの数が存在しなくても常に最大数のスプライトが存在していましたが、RGSS3では、必要なだけしか存在させず、必要になったときに生成、必要なくなったら廃棄(dispose)させることで軽量化を図っています。

なお、ムンホイXPでも同様の軽量化を行っていますが、せっかくRGSS3に導入されている技術なので、細かい説明はそちらをご覧になったほうがいいと思います。

Game_Character に対応する Sprite_Character を作らなくてもいい

さて、ムンホイXPは、もう一歩踏み込んだ「余計なスプライトを作成しない」をしています。それは、「イベントのスプライトも、必要がなければ作らない」というものです。

一般的に、イベントのオブジェクトは Game_Event というクラスが担当しており、その多くの挙動はスーパークラスの Game_Character が担っています。Game_Character に対応するスプライトとして Sprite_Character が作成され、このスプライトは対応する Game_Character を監視し、スプライトの変更を行います。つまり、Game_Character と Sprite_Character は1対1対応しているということです。

しかし、です。イベントの中には画像を表示しないものもあります。たとえば単なる場所移動のイベントなどです。これらは Sprite_Character オブジェクトは透明なスプライトを表示し続けるだけで、実質なにもしていません。ならば、そういうスプライトは作成しなくてもいいのでは?という考えに基づいて、「必要ないスプライトは作成せずに軽量化」を図りました。結果的に、7〜9割のイベントでは、スプライト作成の必要がなかった、という衝撃的な結果が出ました。具体的には、ぜのんの町のマップでは180個のイベントが置かれていますが、スプライト作成の必要性があったのは、30個前後でした。実に6分の1です。

では、「どういう条件でスプライトを非作成にするか」「どういう点に注意をすべきか」を説明して行きます。

スプライト非作成の条件

まず、スプライトを作らない条件についてです。これは吟味の余地がありますが、ムンホイXPでは、以下のいずれかの条件に合致していれば、スプライトを作らないイベントとしています。

  • 全ページ数が1で、イベントグラフィックが設定されていない
  • イベントの起動時にそのイベントが既に消去されている。つまり、「イベントの並列実行」で「イベントの一時消去」が実行された

スプライトを作成しているかどうかを判定するために、インスタンス変数「スプライト非作成フラグ( @non_sprite )」を導入しました。これに該当するイベントは、スプライトを作成しないことにしたわけです。

スプライトが必要なときは、動的生成する

しかし、ここで問題があります。上記を満たすイベントでも、スプライトが必要になる場合があります。それは下記のような場合です。

  • イベントコマンド「移動ルートの設定」で「グラフィックの変更」が行われた場合
  • イベントコマンド「アニメーションの表示」で対象キャラクターとして当該イベントが指定された場合

特に、一時消去されたイベントであっても、その位置にスプライトがないと、アニメーションが正常に表示されないということは盲点です。

このような場合、ムンホイXPでは、「スプライトを動的に生成する」という方法で対処しています。

具体的には、@dynamic_create_queue というインスタンス変数を $game_map に追加し、ここにイベントIDが追加された場合、Spriteset_Map#update にて、動的にスプライトを生成する処理を追加しています。

この、「必要なときに動的に追加する」という処理は、かなりトリッキーに見えますが、驚くほど問題は起きていません。

RGSSがこの軽量化を行わない理由

以上のように、若干ややこしいのですが、RGSSはこのような軽量化は行っていませんし、今後も行わないと思います。

なぜか。それは、「スプライトを作成しているかどうか」を常に意識しながら「動的に生成するかどうか」という処理を追加するか考えていくというのは、非常にややこしいことだからです。

天候やピクチャであれば、それをいちいち改変する人は少ないでしょうが、イベントであれば、改変する人も少なくないでしょう。そういう人たちにいちいち「このイベントのスプライトが作成されていない場合もあるから、拡張する場合は適宜それを意識してコーディングすること」というのはあまりにも面倒です。

このように、この軽量化は、イベントに様々な拡張を施したい場合、結構大変なことになります。十分理解したうえで、素材を導入する場合特に、相性を吟味する必要があります。

軽量化手法その3:Game_Character をあれこれ軽量化

Game_Character は、マップのイベントの数(+プレイヤーなどの数)だけ存在し、それらのオブジェクトが毎フレームごとに update を呼び出します。このため、このあたりを軽量化できたら、イベントの多いマップでは、かなりの軽量化に繋がります。

RGSS3でも導入された、「画面外か」という考え方

まず、軽量化の方法として考えられるのは、「画面外なら、update の処理をあれこれ省略する」というものです。

この考え方は RGSS3 では一部取り入れられており、Game_Event#near_the_screen? メソッドの導入により、「画面に近い」以外のイベントは、一部の処理を省略しています。

ムンホイXPでも、同じような軽量化を導入しています。ムンホイXPでは、Game_Character#on_screen? メソッドを導入し、スプライトのサイズなどから、より厳密に(その分計算量は増えますが)画面内かどうかを判定し、同じ計算を何度も計算せずに済むように一度計算したらインスタンス変数 @on_screen に入れています。@on_screen が false の場合、一部の処理をスキップしているのです。

「置物」すなわちアニメーションも移動もしないイベント

次に考えられるのは、「置物のようにアニメーションも移動もしないイベントは、それらの処理を省略する」というものです。

プリセットスクリプトのGame_Character#update は、たとえアニメーションも移動も行わないイベントであっても、jumping? や moving? の判定を行ったうえ、Game_Character#update_stop を呼び出すなど、かなり手間のかかる処理を行っています。これは、全てのイベントに共通の処理を行うことで処理を簡略化しているのですが、置物系のイベントでは、明らかに冗長です。無駄な処理をスキップできたら、かなり軽くなるのではないでしょうか。

「置物」の条件

それで「置物」の条件を考えましょう。Game_Event#static? というメソッドを作り、そこで定義しています。具体的には、以下の通りです。


def static?
# ※移動ルート『固定』かつ、(キャラクタでないか停止時アニメなし)
return (@move_type == 0) && (@character_name == "" || !@step_anime)
end

これが成り立つことが、置物であることの必要十分条件なのです。

実際にはこの値をインスタンス変数 @static に保存して、それを呼び出しています。これが成り立つイベントは、update の大半をスキップできます。

画面上には、予想以上に「置物」となるイベントが多いです。宝箱のようにアニメーションしないものはあなたの作るマップにも多いのではないでしょうか。

置物イベントに関する注意

さて、置物イベントですが、場合によっては置物ではなくなる場合があります。それは以下の場合です。

  • イベントページが切り替わった場合
  • イベントコマンド「移動ルートの設定」が行われた場合

前者の場合は、改めて static? を呼び出し、@static を更新します。

後者の場合は、イベントコマンドによる移動中は @static を false にし、移動が終了した時点で改めて static? を呼び出し @static を更新します。

導入時には注意が必要:拡張性の問題

以上、Game_Character まわりの軽量化を見てきましたが、いかがだったでしょう。

他の軽量化と同様、この軽量化も、プログラムが複雑化するリスクをはらんでいることがお分かりだと思います。くれぐれも、アクションRPGのように、イベント関連にスクリプトで手を加えるシステムを作る人は、それらの実装がおおむね完了してから、それらの兼ね合いを考えて入れていくことをおすすめします。

軽量化手法その4:Sprite_Character をあれこれ軽量化

Game_Character と同様、マップに多数配置されており、毎フレームごとにそれに比例する update が大量に呼び出されるのが、スプライトです。軽量化その2によって、余分なスプライトを作らない場合でも、まだ一定数のスプライトを作る必要があるわけで、だめを押しておきたいところです。そこで、Sprite_Character#update について、以下のような軽量化が考えられます。

Sprite#updateを呼び出さない禁じ手

Sprite_Character#update は、super によって、スーパークラスの update メソッドを呼び出しています。しかし、ヘルプのSprite#updateには、以下のように書かれています。

フラッシュの必要がない場合は呼び出さなくても構いません。

同様に、Sprite_Character の直接のスーパークラスである RPG::Sprite でも同様です。ダメージの表示などの処理を行っているのですが、これらが必要にあるのは原則戦闘中のみであり、アクションRPGのように「移動中にダメージを表示したりフラッシュさせる」という処理を追加しない限りは、super(RPG::Sprite#update)を呼び出す必要がないのです。

このため、「アニメーションの表示中以外は、superを呼び出さない」という処理を行っています。RPG::Sprite#update はかなり様々な判定を行っているため、この些細な処理がかなりの軽量化になります。

もちろん、このような処理が許されるのは、イベントの拡張を行わない場合のみであり、アクションRPGにありがちな「移動中にダメージを表示」などを追加したい場合、この軽量化は行ってはいけません。

なお、RGSS1の RPG::Sprite は、RGSS2以降は Sprite_Base になっていますが、処理が簡略化されており、この軽量化は意識する必要がありません。

グラフィック転送を毎フレームは行わない

もうひとつ。Sprite_Character#update を見ていると、以下のような行を見つけます。


# グラフィックがキャラクターの場合
if @tile_id == 0
# 転送元の矩形を設定
sx = @character.pattern * @cw
sy = (@character.direction - 2) / 2 * @ch
self.src_rect.set(sx, sy, @cw, @ch)
end

ここで self.src_rect.set というのは、コメントの通り、転送元の矩形を設定しているわけですが、毎フレームごとに設定するのは、若干処理の負担になっているのではないでしょうか。転送元が変更された場合のみでいいんじゃないでしょうか。そこで、以下のように条件を追加し、変更します。



# ★(軽量化)パターンか向きのいずれかが変化した場合
if @tile_id == 0 and (@pattern != @character.pattern or
@direction != @character.direction) then
# 転送元の矩形を設定
self.src_rect.set(
@character.pattern * @cw,
(@character.direction - 2) / 2 * @ch,
@cw, @ch
)
# ★パターンおよび向きを更新
@pattern = @character.pattern
@direction = @character.direction
end

@pattern と @direction というのが新たに追加されたインスタンス変数です。これらは、グラフィックが変更されたときは、nil に初期化します。

「これ、本当に効果あるの?」とお思いでしょうが、これを導入して、実際にベンチマークを行うと、かなり軽くなりました。同様の処理は、RGSS2、RGSS3でも毎フレーム行われているため、軽量化を追及する人は追加してもいいと思います。

その他の軽量化手法あれこれ

他にも様々な軽量化を施しています。特にウィンドウ関係の軽量化は地味にいろいろやっていますが、特筆すべきものをひとつ。

文字をキャッシュに入れる

一度表示した文字をキャッシュに入れて、今後表示するときはキャッシュから描画(Bitmap#bltにて描画)しています。

DrawTextCacheクラスというのを作り、ここに文字のキャッシュを入れています。インスタンスは$text_cache という名前にしており、スクリプト内の Bitmap#draw_text を呼び出しているところを、DrawTextCache#draw に置き換えることにより、実装しています。

これは、当初のPCでは、文字の描画が遅く、文字を描くくらいならBitmap#bltで描画した方が早いため考えたメカニズムですが、最近のPCでは、文字の描画速度はあまり問題にならないようなので、あまり導入の意義は小さいと思います。

軽量化、結論

全体的に言えるのは、最近のPCでは、軽量化の意義は薄れてしまったこと、軽量化を1つ追加するのは、拡張性の幅を1つ狭めてしまうことになるため慎重に行う必要があることがあります。

ただ、そんな中でも、「イベントの位置テーブルを作る」という考え方だけは、今でも意味があると思います。イベントの数の2乗に比例して処理が増えるというのは、とてもCPUパワーの上昇だけでは補えるものではなく、多くのイベントを置いたマップを作る人はぜひ思い出してもらいたいものです。

これまでなら、イベントの多いマップを作るのを断念して、いくつかのマップに分ける人がいたでしょう。しかし、そうせずに済む方法もあるということです。今回のムンホイのリメイクのように、どうしても元の仕様を変えたくないような人は検討する価値があるでしょう。

今日は軽量化についてぼちぼち書いてみました。あともう少し地味な技術について、そのうち書いてみようと思います。