神無月サスケの波瀾万丈な日常・はてなブログ編

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

Moon Whistle の曲に作詞しました・第2弾

前回、拙作ムンホイのフィールド曲に歌詞を付けたところ、大変好評でしたので、ふたたびいくつか歌詞を作成しました。

今回は4曲です。前回のオーソドックスなのと比べて、若干変化球を選びました。
好評ならまたやります。

曲のダウンロード

下記の歌詞の曲は、ここからダウンロード出来ます。
http://fhouse.s17.xrea.com/moonwhistle_remake/midimh.html

いろんな人たち (XMIDI009.mid)

1.

(安いよ安いよ お買い得だよ)
八百屋のご主人
(うなぎの蒲焼 香り振りまく)
魚屋のご主人
みな、立ち止まり、商品ながめる
お客さんを 魅了していく

2.

(タイムサービスの 鐘鳴らすよ)
ブティック店員さん
(試食をしてって そっと勧める)
パン屋のお兄さん
みな つられて 不可抗力で
引き寄せられる いろんなお客さん

3.

(ダイエットには この薬!)
薬局の 看板
(眠ってる和服 ありません?)
問いかける 着物屋
商売努力
みんな 必死に 頑張るお店
応援 したくなる

4.

(うちはスポーツマージャンです)
老舗の雀荘
(お酒とカラオケをどうぞ)
カラオケ喫茶
どんな お店なの 僕には分からない
でも なんだか 悪い気はしない

エレベーターダンジョン(XMIDI013.mid)

(1…3…6…9…10…12…15…19)
(とびとびとびとび……からくりだ
とびとびとびとび……こまる)
ビルの中 ずらりと並んでる
エレベーターの 奇妙な表示板
(とびとびとびとび……からくりだ
とびとびとびとび……こまる)
どこの階に どうすれば 行けるの?
どの箱に 乗れば 辿り付けるんだろう?
(とびとびとびとび……からくりだ
とびとびとびとび……こまる)
あーわからない もうお手上げ
だけど 行かなくちゃ
ひとつひとつ 数字を覚え
ひとつずつ ひとつずつ やらなきゃ

僕は行かなきゃ
待ってる人のため
(そう……だ……やる……んだ……)
絶対あきらめない!

おうたをうたおう(XMIDI020.mid)

1.

さあみなさん きょうは みんなに
あたらしい おうたを おしえましょう
まずはわたしが うたうから
みんなそれに つづけて うたってね
さいしょは みんな なれないでしょうけど
くりかえす うちに きっと おぼえる
あたらしい おうたを おぼえた あとには
おうちの ひとにも うたってあげて

2.

さあみなさん きょうは みんなに
かみしばいを みせて あげますよ
わるいおにも こわいおばけも
でてくるけれど こわがらないで
さいごには みんな せいぎのみかたが
わるいやつ せいばい してくれる
だからみんな あくびせずに しんけんに きいてね
わたしの はなす この はなしを

嵐の予感(xmidi064.mid)

夕間暮れ 忍び寄る
暗闇の中 ひっそりと
にじり寄る この空気
きっと何かが 僕を 狙ってる
空には三日月 星の群れ
穏やかに 僕を照らす
でもその空と この風は
じきに 嵐を 呼び起こすだろう……

Moon Whistle の曲に作詞しました

Moon Whistle は 1999年公開。リメイクも2011年に出ており、その間、歌詞を作ってくれる人がたくさんいました。
フリゲの中には作者公式の歌詞があるゲーム音楽もあるようですね。ふと気が向いたので、主要なフィールド曲に歌詞をつけてみました。
普段はあまり作詞などしない僕なのでたどたどしいのはご容赦を。

曲のダウンロード

下記の歌詞の曲は、ここからダウンロード出来ます。
http://fhouse.s17.xrea.com/moonwhistle_remake/midimh.html

いざ行かん (XMIDI005.mid)

春の土手を歩く風の匂い
草は萌えてちょうちょは舞いを楽しむ
川は流れメダカが泳ぐ
柔らかい日差しが川を照らして 輝く
ふと浮かぶよ あの日の思い出
みんなわいわい 陽気の中 駆け回った
季節巡り 雪が解けて また来た春 また思い出になる

ひざしの中を (XMIDI024.mid)

青空を覆ううろこ雲
ビルの谷間から覗く太陽
心地よい風が吹いてくる
空き地から聴こえるよ虫の声

セイタカアワダチソウも 赤く色づくもみじも
どうして 僕の心を 揺るがすの?

友達と一緒に出かけよう
きっとみんなおんなじ気持ちだろう
国道沿いの枯れすすき
遠くまで道はずっと続いてる

夕闇にとける街 (XMIDI001.mid)

お日様が 低い空で 町を赤く染める
街灯が 僕の影 長く延ばしてるよ

公園の 賑やかな声 今はもう静まった
誰かさんの サッカーボール ぽつんとたたずむ

夕焼けに 光る 一番星
晩御飯が 待ってる おうちに 帰ろう
あしたまた 遊ぼう

さわがしい大通り (XMIDI006.mid)

1.

忙しそうに歩く人達 腕時計 見ている
窓越しに喫茶店 見てみたら さぼってる サラリーマン

ビルが 立ち並んで 見知らぬ人が 通り過ぎる
窓の ガラス光る 昼の太陽 反射する

黒い革靴 書類手に持ち 繁華街 
無心に 駆け回る 大人たちの午後

2.

犬の鳴き声 鳥のさえずり 自動車のクラクション
しゃれた生垣 干された布団 青空の飛行機雲

路地の 隅の方で 井戸端会議 おばさん達
デパートの 駐車場で こっそり遊んでる 子ども達

晴れた昼下がり けだるい午後 元気出し
さあ 歩き出す 探検だ 知らないとこ 行くぞ

手をつないで歩こう (XMIDI054.mid)

住んでる 町は違っても 心は通じ合うよ
歳の差 気にならないよ 仲良く行こう

僕が困れば 助けてくれて
君が困れば 僕が行く
何も話をしなくても 笑顔が一番
言葉はいらない 分かり合える

手と手を とりあおう 友達になろう
嫌な子 いるけど きっと仲良くなれる
一緒に 歩こう 街は魅力で一杯
僕らみんな一緒に 凄い冒険するぞ

(追記 2017.04.23 若干歌詞に不自然な部分があったので修正を行いました)

ムーンホイッスル小説版を電子出版します

ご無沙汰しておりました、久々のブログ記事です。

今回、拙作ムーンホイッスル(Moon Whistle)の小説版を、Amazonで電子出版することになったので、お知らせです。既にアマゾンで登録を済ませており、数日内に公開される流れです。

ここまでの大まかな流れ

このセルフノベライズは、僕が2013年の春から夏にかけて仕上げたものです。おりしもフリーゲームのノベライズが盛んになっていた時期、ということで執筆を思いつきました。

2013年の9月に、某社の編集者さんに読んでいただける機会があり、内容としては、一定の理解を示していただけたのですが、事情があって出版には至りませんでした。

それは、ライトノベル専門の編集部だったこともあり、「あまりライトノベルらしくない」という理由が大きかったようです。確かに、キャラクターを立てるというより、ちょっと児童文学っぽい描写が多いのは僕も承知しています。

もう一つは、「長年愛されておりファンのすそ野が広いのは事実だが、ここ数年の盛り上がりには欠ける」という点も挙げられました。

そういうわけでその編集部では出していただけませんでしたが、丁寧な対応をしてくれたと感謝しています。

そして、同じ会社の別の書籍部に渡してくださいました。これが2014年1月のことです。

しかしそちらの編集者さんからは、あまり良い対応をしていただけませんでした。いくつかのフリーゲームのノベライズを担当している方なのですが、僕の小説は、いつまでたっても読んでもらえないばかりか、こちらがメールで相談してもなしのつぶてということが続きました。興味がなくても、最低限、読んでは欲しかった、それは無理でもきちんとその旨ご連絡いただけたら、と思っています。このように、編集者さんにも、いろんなタイプがあるのだと痛感しました。

2013年の末ごろに、「ムーンホイッスルが商業展開するかも」ということを僕はツイッターで表明したのですが、それはこの件が進んでいることでした。かなりいろいろと考慮してもらったのですが、前述のとおり、出版には至りませんでした。

電子出版で出してみることに

そういうわけで、一度はお蔵入りにした小説ですが、その後「紙の書籍がだめでも、電子出版ならいけるんじゃないか」と思うに至りました。

なにしろ、昨年あたりから、僕のよく知っている人達が次々に電子出版に参加しており、あちこちで話を聞いていたからです。

これまで「紙の書籍でないと、読みたい人の手に届かない」そう思っていたのですが、これだけ電子書籍が普及してきたなら、その懸念は小さくなる、と感じました。

それで早速、その準備を始めました。2014年9月ごろのことです。

その後、表紙絵やイラストを描いてくれる方(飛鳥好一さん)とも出会い、こうやって電子出版することが出来ました。感謝。

読んでくれる皆様に感謝

今回、こちらの小説については、いろんな方に読んでもらいました。そして、いろんな意見をいただけました。

特に、今回、電子書籍での販売に至ったのも、知人からのアドバイスでした。

小説を公開するだけなら「小説家になろう」など、無償で公開可能なサイトはたくさんあります。しかし、今回は、あえて有償にした方がいい、という意見が圧倒的でした。フリーで公開しても、数件感想書いてもらうだけで風化しかねない、といった意見が聞かれました。

この「フリーより有償の方がいい」という意見については、僕の中では肯定する気持ちと否定する気持ちが半々でしたが、やはり「本当に読みたい人にだけ手に取ってもらいたい」という気持ちで実験的な要素も含めています。

金額はひとまず5ドル(500〜600円くらい)にしました。こちらも、僕は300円くらいにするかと思っていましたが、それじゃ安い、というご意見を伺ってのことです。確かに、本当に読みたい人にだけ届けるのなら、妥当かもしれません。価格は変更可能らしいので、少しこの金額で余裕を見ます。

今しばらくお待ちを

現時点(17:04 2015/04/25)ではまだ公開されていません。

公開されたら、恐らく、プレビューとして序盤が読めると思いますので、興味がある方は見ていただけたら幸いです。

追記(21:05 2015/04/25):公開されました!

小説ムーンホイッスル
http://www.amazon.co.jp/gp/product/B00WO3O3O0

今後地味にプロモーションしていきたいと思います。

ムンホイXPの地味な技術:その3.フィールドマップのあれこれ

普通のRPGだと、ウィンドウのレイアウトとか戦闘のシステムやレイアウトなどに凝るものらしいですが、本作品ではそういうのは極力据え置きにして、マップ画面の仕様とかそういうものばかりいじっていました。

このため、町を移動するのが結構意味も無く楽しかったりするでしょう。それは、システム的に地味なことをたくさんしているからかもしれません。

プライオリティの導入

RGSSゲームライブラリ、Tilemapクラスにはプライオリティという概念が導入されています(RPGツクールXPのみ。VX以降は廃止)。タイルごとにプライオリティを、なし(0)から5までの間で設定することで、そのタイルの位置する「高さ」を設定しているのです。

ムンホイXPのシステムでは、この考え方を Game_Character オブジェクト(イベントやプレイヤーなど)にも導入し、タイルとの親和性を高めました。

Game_Character#priority の仕様

具体的には、Game_Character に @priority というインスタンス変数を追加しています。priority の取ることの出来る値は、タイルと同様0〜5で、これにより、同じプライオリティにあるタイルと同じ高さに表示されることになります。

これによって、様々な仕様を変更しています。例えば、Character#screen_z は、priority 導入により、それを用いた計算式に拡張しています。

プライオリティを使った多層処理

通常はプレイヤーやイベントのプライオリティは1ですが、このプライオリティを操作することで、多層処理を行っています。具体的には、以下の例が挙げられるでしょう。金網の下をくぐりぬけたり、上を通ったりすることです。


ここでプレイヤーが金網の下を通っている時のプライオリティは1、金網の上に乗っている時のプライオリティは3です。プライオリティ1の時を「下層」、プライオリティ3の時を「上層」と呼んでいます。

そして、タイルのプライオリティは次の方針に基づいて決められています。

  • 0=プレイヤーの下にあるもの(床や地面など)
  • 1=プレイヤーと同じ高さにあるもの(民家の壁など)
  • 2=プレイヤーの上にあるが上層になると下になるもの(屋根など)
  • 3=プレイヤーが上層に出たとき同じ高さにあるもの(屋根の上にある障害物)
  • 4以上=プレイヤーが上層に出ても上にあるもの(アドバルーンなど)
マップエディタでの設定方法

タイルにあったプライオリティをイベントに拡張するにあたって、イベント名に「@2」や「@4」などの「@+数字」が付いていた場合、その数字を、そのイベントのプライオリティとして設定することにしています。イベントを上層に置くためには、名前に「@3」という文字を入れると良いわけです。下層に置く場合は、デフォルトのプライオリティが1のため、わざわざ「@1」と書く必要はありません。

なお、下層と上層では、タイルの通行判定が違います。このため、上層の通行判定だけ、別のタイルセットのものを使っています。ムンホイXPでは、タイルセットはひとつにまとめており、ID=1番のものを使用していますが、上層の場合はID=2番の通行判定を使っています。

プレイヤーの上層と下層の切り替え

イベントから「スクリプトの呼び出し」で以下のようにすると変更できるようにしています。
下層に変更する場合:

$game_player.set_priority(1)

上層に変更する場合:

$game_player.set_priority(3)

これは、コモンイベントの1番(上層→通常)と2番(通常→上層)から呼び出されいます。イベントから、これらのコモンイベントを呼び出して切り替えます。ただし、実際にはもう少し複雑な処理を追加しているため、興味がある人は実際にコモンイベントおよび、そこから呼び出されているスクリプトを確認してみてください。

結論:多層処理だけなら、他のより簡単な実装方法がある

以上、ムンホイXPでの多層処理のメカニズムでした。実際には多層処理を行いたいだけなら、わざわざこんなことをしなくても、より簡単な方法はいくつかあります。

例えば、RPGツクール2000の方法として、同じタイルセット(2000ではチップセットという)グラフィックを使って、通行判定だけを変更したものを準備します。通常と上層を切り替えるタイミングで、イベントコマンド「チップセットの切り替え」を行えば、通行判定やプライオリティを変更させることが出来ます。XPには「タイルマップの切り替え」はイベントコマンドにはありませんが、同様のことはスクリプトで出来ると思います。

また、RPGツクール95では、オリジナルのムンホイでは、屋上など、上層の地形は「海」になっています。上層に出る時は、RPGツクール95特有の機能「主人公タイプの変更」でタイプを「通常」から「船」に変更することによって、海は通れるが普通の地形は通れない、つまり屋上のタイルは動けるが、別のところには行けない、というようにして実現しています。

以上を参考にすれば、より簡単な方法はあるのですが、ここではデータの一貫性があった方が拡張がしやすいと思い、今回のような方法を採用しました。

補足:この方式の採用を検討する人への注意

もうひとつ重要な注意があります。ムンホイではプレイヤーなど、主要なキャラクターの大きさが32までになっています。これはVXやVX Aceのキャラチップに近い仕様です。このため、これらのプライオリティの設定で問題ないのですが、より大きなキャラチップを使用している人が多いでしょう(32x64など)。この場合、この設定だけでは不十分な場合があります(キャラクタがプライオリティ2の屋根を突き破る、といったことがある)。このため、この考えを導入したい人は、タイルのプライオリティの値を吟味する必要があるでしょう。

斜め移動

はじめに:なぜ斜め移動はRGSSに標準搭載されないのか

斜め移動は、結構スクリプト素材も出回っており、単に斜めに移動するというだけなら、大して難しくはありません。そもそも、RGSSレベルでも、イベントコマンドでは斜め移動が標準搭載されており、これなら、プレイヤーの移動にも斜め移動が採用されてもおかしくないように思えます。(更に言えば、イベントコマンドでの斜め移動はRPGツクール2000の時代からありましたよね)

しかし、実際はRPGツクールVX Aceで、多用な機能が標準搭載されたRGSS3でさえ、斜め移動を採用しませんでした。これはなぜでしょうか。

僕に考えられる大きな理由の一つは、「ユーザー側が考慮しないと、斜め移動の見た目が不自然に見える場面が多くなるから」だと考えられます。

斜め移動が不自然に見える3つの例

まず最初に。斜め移動の通行判定は、RGSSではどのように組まれているのか、見てみましょう。左下に移動する場合(Game_Character#move_down_left)を引用します:


# 下→左、左→下 のどちらかのコースが通行可能な場合
if (passable?(@x, @y, 2) and passable?(@x, @y + 1, 4)) or
(passable?(@x, @y, 4) and passable?(@x - 1, @y, 2))

コメントにあるように、左下に行く場合「下→左、左→下 のどちらかのコースが通行可能な場合」は通行可能で、通行の処理を行うのです。

確かにこれなら、通行できないはずの地形に迷い込むようなことはありません。しかし、見た目としては、非常に不自然になることがあります。僕がテストプレイしていて見つけた(そして対処した)大きな問題点を3つにまとめ、それぞれ次の名前で呼ぶことにしました。

  • 梯子
  • 吹き抜け
  • プレート(壁)

これらを、ひとつずつ説明して行きます。

梯子

次のような移動を考えます。すなわち、階段や梯子のような地形に、斜め移動で入る(あるいは出る)場合です。

この時、「壁の中を斜めに横切って、階段(梯子)の途中の部分に飛び移っている」というように見えてしまいます。

吹き抜け

次のような移動を考えます。

この時、「何もない吹き抜けを斜めに横切って、空中移動している」ように見えてしまいます。

プレート(壁)

次のような移動を考えます。移動先は「下への移動のみが不可」という、平らな壁(プレート)のような地形です。プレートに斜めに入る(または出る)場合です。


この時、「身体半分を壁にめり込ませながら移動している」ように見えてしまいます。

斜め移動禁止地形の導入

そこで、ムンホイXPでは、タイルに「斜め移動禁止」属性を追加しました。上記の3つのうちどれであるかは、自動判定されます。すなわち……

  • 「斜め移動禁止」で、通行不可であれば、吹き抜けタイル
  • 「斜め移動禁止」で、通行可能で、プライオリティがプレイヤーより低ければ梯子タイル
  • 「斜め移動禁止」で、通行可能で、プライオリティがプレイヤーより高ければプレートタイル

ということです。

なお、この「斜め移動禁止」属性は、地形タグで実装するのが一般的でしょうが、本作では茂み(0x40)を使用していないため、茂み属性を「斜め移動禁止」として使用しています。

スクリプトでの斜め移動禁止の実装

Game_Map#ladder? で梯子判定を、 Game_Map#wellhole? で吹き抜け判定をしています。プレート判定は、梯子判定と処理が共通する部分が多いため、Game_Map#ladder? の引数を変えることで、まとめて判定します。

これらのメソッドを、斜め移動の際に呼び出し、斜め移動禁止でないか判定します。Game_Character#move_lower_left に追加された処理を引用します:


# ★斜め移動禁止地形の場合
if $game_map.ladder?(@x, @y, @priority, true) or
$game_map.ladder?(@x - 1, @y + 1, @priority, false) or
$game_map.wellhole?(@x, @y + 1, @priority) or
$game_map.wellhole?(@x - 1, @y, @priority) then
return false unless @through
end

こうすることによって、斜め移動禁止の地形で、斜め移動を行わせないことが可能です。

斜め移動が行えない場合通常移動を行うことが大事

さて。斜め移動を制限したわけですが、ここで移動の快適さのために重要なことがあります。それは、例えば「右上に動こうとして動けなかった場合、右か上に動ける場合、その方向に移動を行う」ということです。

すなわち、斜め移動が行えなくても、通常移動が可能なら、それをするということです。この考え方は、斜め移動禁止を導入しなくても重要ですが、斜め移動禁止を入れて制限を多くするなら、なおさら必要です。

なお、右上に動こうとして動けなかった場合で、右と上の両方に動ける場合はどちらに動けばいいのでしょう。どちらでもいいのでしょうが、僕は経験則から、上下の動きの方を優先しています。このあたりは、議論の余地があると思いますので皆さんも調整しながら考察するとよろしいでしょう。

これらの実装は、Game_Player#update の中に書かれていますので、興味のある方は解析してみてください。

結論:斜め移動で一番厄介なのは見た目の問題

斜め移動は常に見た目との戦いなのです。つまり、斜め移動を無くすか制限を強くすれば解決するのでしょうが、それではプレー時の爽快感が失われてしまいます。よって若干面倒でも、こだわっていきたい、そんな僕のこだわりをどうか皆さんも理解して欲しいのです。

なお、今回、斜め移動の見た目問題については、一番端的な部分を押さえたつもりですが、まだまだ、他にもあるように思えます。こういうのは、理屈ではなく見た目の問題なので、探すだけ見つかるでしょう。ぜひ、皆さんも、斜め移動を導入するときは、このような見た目の問題を意識してみてください。

移動速度(ダッシュの速度)

もう一つ小さなことですが、指摘しておきたいのは、移動速度の問題です。

RPGツクールXPの移動速度は、1〜6まで指定でき、数が大きいほど速いのです。プレイヤーのデフォルトの移動速度は4なのですが、これは少し遅く感じます。一方で5にすると、少し速く感じてしまいます。何とか、この中間が取れないものでしょうか。

本作では、ここをいじって、通常の移動速度を4と5の中間、低速ダッシュを5と6の中間、高速ダッシュを6の移動速度にしています。

実装

プレイヤーやイベントの移動速度については、Game_Character#update_move で、以下のように定義されています。


# 移動速度からマップ座標系での移動距離に変換
distance = 2 ** @move_speed

ここで @move_speed が1〜6の値を取るのです。1タイルは32ピクセルですが、これが論理座標で128の大きさになります。よって、例えば @move_speed が 6 なら、distance は2の6乗、すなわち64になり、2フレームで1タイルを移動する計算になります。@move_speed が5なら4フレーム、4なら8フレームです。

そこで、中間の速度を取るためには、3フレームや、6フレームで1タイル移動することを考えてやればいいのです。そこで、先程のコードの後に、以下を追加しています。


# ★若干移動速度アップ
if @move_speed == 4
distance = 22
elsif @move_speed == 5
distance = 43
end

これで、@move_speed が4の時、6フレームで、@move_speed が5の時、3フレームで1マス進むようになります。実際に計算すると進む座標の数は128より大きくなるように見えますが、多くなった部分は後の処理で丸められるので、問題ありません。

フレームレートをいじるよりも簡単な解決方法

ここで紹介した方法は、驚くほどシンプルに移動速度を変える方法です。

ツクールXPが出た当時は、内部フレームレートを通常の40fps から 60fps に変更することによって、移動速度を1.5倍にするという人が結構いました。CPUの性能が上がった今なら、それでも問題ないかもしれませんが、フレームレートを増やすということは、負荷を増やすということです。カクカクと処理落ちする人も多くなったのではないでしょうか。

移動速度のために処理落ちを増やすなんてナンセンスです。それよりは、こういうシンプルな方法がありますよ、ということです。

なお、この手法は、RGSS2(RPGツクールVX)以降でも比較的簡単に実装できます。

結論

以上、マップ関連のあれこれを見てきました。自分で読み返していて、つくづく地味なことやっているなあ、と痛感したのでありました。でも、僕は確信しています。本当に重要な技術は、レイアウトとか演出とかを派手にすることではなく、こういう縁の下の力持ち的な地味なものなんだ、ということを。

今回の紹介が皆さんの参考になれば幸いです。

ムンホイXPの地味な技術:その2.ビューポート分割

導入動機:天候を導入しようと思って出てきた障壁

ムンホイのXP版にあって、オリジナルになかった要素のひとつに、天候があります。雨が降ったり、雪が降ったりする演出です。これを導入した最初の動機は単純で、せっかくRPGツクールXPに、そのような機能が準備されているのだし、導入すればささやかな演出の華になるだろう、と思ったことでした。

しかし、そこにひとつの障壁がありました。それは本作のマップの構成です。

本作のマップ構成:屋内でも屋外部分が描かれている

ムンホイの屋内マップ構成の一例を次に挙げます。

ご覧になっての通り、周囲に外の景色が描かれています。これは、RPGツクール95時代には比較的一般的なやり方でした。一方、RPGツクール2000以降は屋内では周囲が黒い壁に囲まれており、屋内マップでは外の景色は描かれないのが一般的になりました。

これには理由があります。ツクール2000で初めて天候が導入されたのですが、もしこれを導入する場合、屋内に外の景色が描かれていると、その外の部分にも雨を降らせないと不自然になるからです。ツクール2000には(それ以降のツクールでも)「屋内部分では雨が降らず、外の部分にだけ雨を降らせる」といった機能はないため、屋内では単に雨をやませる、その際のマップで外の景色は描かない、というやり方が定着したのでしょう。

要求:「屋内から見える外の部分だけ雨が降っている」を実現できないか

さて。オリジナルのムンホイは天候のシステムが無かったRPGツクール95作品です。このため、天候のことを意識しないマップ構成になっています。このため、天候などは入れずに行くべきか? と思いました。

あるいは、マップの方を書き換えて外の部分を塗りつぶすという手もあるでしょう。しかしそれはしたくありませんでした。あくまでオリジナルの雰囲気を変えたくなかったし、そこに手を加えるくらいなら、天候など入れない方がいい、と。

そのように考えたのですが、最終的には「屋内から見える外の部分だけ、天候のアニメーションを表示する」という処理が実現できました。これは試行錯誤して出来たというよりは、あるとき急にひらめいて、その考えに基づいて入れてみたらすんなりうまく行った、という性質のものです。そのメカニズムを紹介していきたいと思います。

実例

カニズムの説明の前に、先にこの実装によって、具体的にどういう風になったかを、スクリーンショットを使って紹介します。百聞は一見にしかず。

この「屋内マップで描かれている屋外の部分にだけ画面処理を加える」というシステムの実装により、天気のほかに、夕焼けの描画もオリジナル版から変更することが出来ました。オリジナルの95版では、屋内でも全体が夕焼けのパレットになっていましたが、リメイク版では、屋外部分のみが夕焼けになっています。次のような感じです:

パレットが変わっているのが、屋外部分というわけです。雨のアニメーションなども、この部分にだけ描画されるのです。

具体的にどうしたのか、RGSSでの実装やマップエディタでの設定方法を紹介します。

実装:屋内と屋外でビューポートを分ける

屋外用のビューポート(名づけてビューポート0)を作成

基本的な考え方は、屋外のあるマップでは、屋内用と屋外用に、スプライトを表示するビューポートをそれぞれ用意する、というものです。

既存のRGSSでは、ビューポートは、viewport1〜viewport3 が用意されていて、viewport3 が一番手前にあり、viewport1 が奥です。そして、通常のマップ(Tilemap)は、viewport1 に表示されています。(奥にある=z座標が小さい、ということです)

今回は、そこに拡張を行います。屋内マップで屋外も表示する必要がある場合、屋内部分と屋外部分を分けて表示するために、屋内部分を既存のviewport1に、屋外部分をそれより奥にある新設したビューポートに表示することにします。この新設したビューポートは、viewport1 より奥にあるため、viewport0 と呼んでいます。

マップの屋内用と屋外用の Tilemap オブジェクトを作成し、それぞれのビューポートに置きます(どうやって屋内と屋外を分けるかは後述)。そして、天候の処理、雨や夕焼けなど、外だけに処理が必要になった時は、屋外用(ビューポート0)に対してだけその処理を行うことによって、「外側だけ、天候やパレットが変更される」という処理が実現できます。

タイルマップの要素を屋内と屋外に分ける

このように「屋内と屋外の部分でビューポートを分けて表示する」ことにしました。では、マップの要素で、どこが屋内でどこが屋外かを設定するにはどうすればいいでしょう。

マップエディタを見ると、以下のように設定されています:

マップの外側を示す部分に×、窓の部分に\という記号が置いてあるのが見えると思います。結論から言うと、これらの記号は3つあるレイヤのうちの最も手前(レイヤ2)に置かれています。×の記号は、残りの2つは外側に、\の記号はレイヤ1を内側に、一番奥(レイヤ0)を外側に置く、という意味です。(特に記号が無ければ、全て内側です)

内側と外側に分かれたタイルマップを作成する処理は、Game_AnotherMapData クラスによって行われています。Game_AnotherMapData#setup メソッドを見てもらうと分かりやすいと思います。マップから読み込んだ マップデータオブジェクト(Table)をもとに、inner および outer と名づけられたマップデータ(Table)を作成しています。これらが、内部用、外部用の Tilemap で使用されるのです。

なお、このように「屋内と屋外で表示を分ける処理を行う必要があるマップかどうか」の設定は、初期化イベントというもので設定しています。原則、マップの座標(0,0)に置かれているイベントで、イベントの名前に「!init」が含まれているものが初期化イベントと見なされます。初期化イベントのグラフィックにタイルを設定すると、そのタイルIDが、特別な処理をするものになります。ここで、×マークの描かれたタイルが設定されています。(\は、×マークの次のIDのタイルです)

このようにして、屋内用と屋外用の2枚のTilemapオブジェクトを作成し、それぞれ別のビューポートに所属させることによって、屋内と屋外を表現しているのです。

おさらい:エディタで中を見ながら確認

さて、ここまで読んで理解出来ているでしょうか。RPGツクールXPで実際にムンホイXPを読み込みスクリプトエディタを起動して、該当箇所と付き合わせながら読んでいくと理解しやすいと思います。

ここまでの理解を元に、Spriteset_Map クラスを読んでみましょう。簡略化のため、タイルマップ、パノラマ、天候の処理は、Spriteset_MapElements クラスを作成し、そちらに処理を移しているため、それらを合わせて読んでいってください。

Spriteset_MapElements#initialize メソッドを見てください。屋内と屋外に分けたデータ(AnotherMapData クラスのオブジェクト $game_map.am_data)を元に、ビューポート0とビューポート1に分けて作成している様子が分かりやすいと思います。

屋外部分にあるイベントの設定

タイルセットのビューポートを分けることが出来ました。次は、イベントの設定です。

イベントの中には、屋外部分に配置されている物もあります。例えば、上記のスクリーンショットの中では、外にカプセル(宝箱)があるのが見えると思います。ゲームをプレイした方ならお分かりでしょうが、建物の北の駐車場から回り込めば取ることが出来るのですよね!

このように、外側部分にあるイベントはビューポート0に置く必要があります。そのようなイベントは、名前に「!v0」という文字を含ませることによって設定するようにしています。具体的には、Game_Event#viewport0? というメソッドで判定しています。

そして、このメソッドでビューポート0用と判定されたイベントのスプライトはSpriteset_MapElements#add_sprite_to_viewport0 メソッドでビューポート0に配置されています。

このように、ビューポート0に配置するイベントは手動で設定する必要があるため、若干面倒ですが、これは仕方が無いですね。自動判別する仕組みが作れたら便利だったのですが。

プレイヤーが外側の部分に出るときの処理

次に考慮するべきなのは、プレイヤーが外側、つまりビューポート0の部分に出る場合です。

この場合、ビューポート0に配置するイベントと同様、プレイヤーをビューポート0に配置しなければ、パレットの変更などに対処できません。

このため、外に出るマップの場合、通常(ビューポート1)用と、外に出た時(ビューポート0)用の2種類のスプライトを作成し、現在位置のビューポートに合致する方だけを表示するようにしています。つまり、屋内にいる時は、ビューポート1用のスプライトだけを表示し、ビューポート0用は消去、逆に屋外に出たらビューポート0用を表示し、ビューポート1用は消去するということです。

この処理を実装したのが、Sprite_Character2 です。通常の Sprite_Character にビューポートの監視を行う処理を追加し、異なるときは表示を行わないようにしています。

このビューポートの監視に使う変数が、Game_Player に追加された @viewport_id というインスタンス変数です。プレイヤーが内側にいる時(あるいはビューポートを分けない場合)は 1 を、外側の時は 0 を取るようにしています。

ビューポートの切り替え(内側と外側を切り替える)は、イベントで行っています。コモンイベントの12番と13番に「ビューポート0」と「ビューポート1」というのがありますが、これをイベントから呼び出すことで、切り替えているのです。

結論:限りなく地味な技術。僕自身、他の作品で採用するかどうか不明

解説は以上ですが、いかがだったでしょうか。この「外だけ雨が降っている」という処理、スーファミなどのRPGでは、普通に見られた処理なので、本作で見かけても目新しいとは思えなかったと思いますが、ツクールの他の作品で見かけたことがないという人も少なくないのではないでしょうか。

無理もありません。外と内を分ける為には、マップ設定から若干ややこしい処理を行う必要があるので、なかなかやる人はいないでしょう。現にこの僕も、リメイクということで元のマップを変更したくないために行った処理です。1から新作を作る場合は、こんなややこしい処理を入れる代わりに、外側を黒く塗りつぶしていたと思うのです。

このため、この技術は本作のみのものになるかもしれません。しかしこの「Tilemapを2枚作り別々のビューポートを作る」という発想は、何かあなたの制作のヒントになるかもしれないと思い、紹介させていただきました。RGSSでこのあたりを大胆にいじっている人は少ないのですが、このあたりの改変こそが、RGSSの醍醐味だと僕は思っているのです。

ムンホイ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パワーの上昇だけでは補えるものではなく、多くのイベントを置いたマップを作る人はぜひ思い出してもらいたいものです。

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

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

ムンホイXPの地味な技術:はじめに

さて、今年の初めてのエントリは何にするかと悩んでいましたが、昨年大晦日、つまり昨日結果が公表された、フリゲ2011のコメントを見て思ったことがあります。

それは、ムンホイXPが好評をいただいたのは嬉しいですが、その中には、結構、それを地味に支えている技術があってのことだということです。

これまではこういうことは黙ってきたのですが、せっかく本作はRGSSのソースを公開しているのだし、もっと皆に参照してもらうためにも、また、皆さんの創作で僕の技術を参考にしてもらうためにも、こういう技術というのは独り占めせずに、公表して共有していくべきかな、と思い始めたので、紹介して行きます。

最初に:本稿で扱わないムンホイXPの分かりやすい技術

まず最初に。ムンホイXPで導入した技術の中でも、結構分かりやすい技術や、「よそがやっているだろう」的な技術は、本稿では紹介しません。

例えば、以下のものです。該当するクラス名などを紹介しておきますので、興味がある人は、RPGツクールXPスクリプトエディタで該当するクラスなりモジュールなりを見てください。

隊列歩行

RGSS3(RPGツクールVX Ace)では標準搭載された隊列歩行ですが、本作でも搭載されていました。RGSS3では Game_Follower というクラス名でしたが、本作では Game_SubPlayer という名前になっていました。正直、メカニズムは一緒なので、興味のある人は、RGSS3を見るほうがいいと思います。

敵が出ますアイコン

エンカウントする場所に行くと、画面の右上にモンスターのアイコン「敵が出ますアイコン」が出ますが、これは Game_AttentionIcon クラスを作成して実現しています。具体的には Game_Picture クラスをオーバーライドし、イベントコマンド的にも、ピクチャの1番として操作できるようにしています。実装は驚くほどシンプルなので、そのクラスを見て、納得していただけれと思います。

主人公が物陰に隠れたとき、ダッシュ中は半透明で表示

本作は現代物ということで、ビルなどが多く、必然的にキャラのスプライトが物陰に隠れることが多くなります。そんなとき、操作が不便にならないように、とダッシュボタンを押している間は、半透明で主人公(先頭のキャラ)のいる場所を表示するようにしています。

この「半透明で主人公(先頭のキャラ)が表示される」という仕組みですが、実は仕組みは至って簡単です。「ダッシュキーが押している間だけ、先頭のキャラ(Game_Player)をZ座標999(=最前面)に、透明度128(=半透明)で表示」という風にしているのです。

つまり、常に半透明キャラは表示されているわけですが、物陰に隠れていないときは通常のキャラが見えており、物陰に隠れて先頭のキャラの表示が消えたときだけ、半透明に見える、というわけです。

この半透明キャラをゴーストと呼んでおり、実装は Sprite_GhostCharacter クラスを作成し、実装しています。これは Sprite_Character のサブクラスなのですが、内部的に別の処理をするため、Sprite_Character2 というクラスを挟んでいます(ビューポート区分という概念を導入したため)。いずれにせよ、実装は非常にシンプルですので、実物を見て確認してみてください。

ダッシュキーを押している間の戦闘高速化

戦闘中、ダッシュキー(ボタンA、すなわちShiftやZ、パッドのボタン1など)を押している間、戦闘が高速に進みます。これは結構愛用した人が多いのではないでしょうか。

これは、RPGツクール95、すなわちオリジナルのムンホイの戦闘でも、Shiftを押すと高速化出来たこともあり、スーファミ版の「メタルマックス2」のあるモードの高速戦闘に想を得て、作ったものです。

これは、Scene_Battle クラスの中を書き換えることによって実装しています。ムンホイXPでは、戦闘画面などを切り替えていないため、このクラスに大きく手を加えていないのですが、この処理実現のために、インスタンス変数 @wait_count の亜種 @rough_wait_count を追加しています。

@wait_count は、1フレームごとに1ずつ値を減らし、0になったら次のフェーズ(処理)に移るという、強制的にウェイトを取るものでした。今回追加した @rough_wait_count は、「特にキーが押されていない場合は @wait_count と同様だ(1フレームごとに1ずつ値を減らす)が、ダッシュボタンが押されていたら即値を 0 にして、次のフェーズに移る、というものです。

実際の処理は、もう少し込み入ったことをしているため複雑ですが、基本的な考え方として以上を覚えていてくれれば差し支えありません。詳しくは Scene_Battle#update の中に記述されています。

あとは、地道に @rough_wait_count と @wait_count のウェイト数調整を行い、自然なフレーム数にしたのですが、この調整が結構大変でしたが、まあその見えない努力の結果、ダッシュしている場合もそうでない場合も、自然なウェイト数になっていると思います。

まとめ:本当の技術は、技術を施したことを悟られないものである

以上、目で見て分かるムンホイXPの(地味な)技術を紹介しました。ムンホイXPは画面のレイアウトも変更していないし、その他の演出も追加しておらず、全体的に地味です。しかし、さらに地味な技術が本作を支えています。

僕は敢えて技術を見せ付けることはしませんでした。むしろ、「技術というのは、プレイしている人が、それが施されていることを気づかないようにしてこそ本物」という考えから、むしろ黙ってきました。演出をいちいち追加するような「どうだ、俺すごいだろう」的技術は、正直悪趣味だと思っています。このため、全体的に地味で地味で仕方がないのですが、それでもプレイした人は「なんだかわからないが、取りあえず悪いとは思えない」という不思議な感覚に陥ったと思います。それを支えているのは、「気づかない、地味な技術」に他なりません。

僕はこのノウハウを内緒にしておこう、あるいはソースを解析した人だけの特権にしておこうと思いましたが、RGSS3の時代になっても、僕が蓄積した技術は有効であるばかりか、より一層重要性を増している気がしたので、僕一人のものにするのではなく、皆で共有してもらいたいと思い、公開することにします。