⚔️

『RPGツクールMZ』のウィンドウ文字表示を調べてみた

2021/05/01に公開

『RPGツクールMZ』関連記事 目次

『RPGツクールMZ』のウィンドウ枠を調べてみた
『RPGツクールMZ』のウィンドウ内容を調べてみた
に引き続き『RPGツクールMZ』 Window 周りの処理を調べていこうと思います。

例によって『RPGツクールMZ』非公式JavaScriptリファレンスのページに適宜リンクしてきます。

文字表示に関するコードを読もう

今回は文字とか画像とかの表示の際のレイアウトに関わる機能を追っていこうと思います。
具体的はcontents ( _contentsSprite.bitmap ) の中にどんな風に文字が書かれているか、です。

ただ、文字を表示するのはこれだけじゃなくて TextManager とか Game_Message($gameMessageに代入されてる)とかが関わってきます。
今回調べるのはレイアウト関連だけなので関係ないですが、ColorManager なんかも文字を書く場合には関わってきますね。

Windowクラスはウィンドウ枠の表示までです。
contents に関するものは Window_Baseクラスに、さらにメッセージ表示に関しては Window_Messageクラスが関わってきますので、今回はこのWindow_BaseWindow_Message のふたつのクラスを追いかけようと思います。

あと前回にちょっとだけ出た Scene_Messageクラスもウィンドウの生成・制御を行なっているのでチェックの必要があります。
関連クラスがたくさんあって、面倒くさいですねっ!!

Window_Base の initialize(初期化)を読む

『RPGツクールMZ』はコンストラクタから initialize() を呼んで初期化処理をするのを基本としてます。
Window_Baseクラスは生成時に指定された Rectangleデータに合わせて、move()メソッドで大きさを調整する処理をしてますね。

initialize()から呼ばれるcreateContents()メソッドでは contentsBitmapクラスを設定して、幅をcontentsWidth()(つまりinnreWidth)、高さをcontentsHeight()(つまりinnreHeight) に揃えてます。
またここで resetFontSettings()メソッドを呼んで文字のフォント・サイズ・色を初期化してます。

Window_Base の update(更新)を読む

『RPGツクールMZ』はコンテナオブジェクトから、そこに含まれる子オブジェクトへと順次 update()メソッドを呼んでいくことで、全体を更新する仕組みになっています。
しょーじき「今時イベントドリブンじゃないとか古くせぇな」と思います。
ごく一部 addEventListener() 使ってるところもありますが。

それはともかく update()メソッドです。
Window_Baseupdate() を見ると、文字表示関連の更新やってないんですよね…。
えー、どこで表示してんのー?!

Window_Message の update(更新)を読む

落ち着いて考えれば、Window_Base はその名の通り基礎なので、実際の表示はやらないんですね。
文字を表示しているのは Window_Message。こいつを見ていきます。

Window_Messageは[名前][所持金][選択肢の表示][数値入力の処理][アイテム選択の処理]と、いくつも子ウィンドウの参照を持ってて、ちょっと肥大化してるクラスです。
メッセージを表示するだけなら、子ウィンドウはひとつも必要ありません。
…これらのウィンドウを実際に children として持ってる Scene_Message の方で管理して欲しかったなぁ。
ということなので、コードを読んでてもメッセージに関係ない処理が多くて見通しが悪く、「このクラス丸ごと作り直してぇな」なんて感想を持っちゃいますが、もう存在している以上はしょうがない。頑張って読んでいきましょう。

initialize() は文字に関しては、これといったことをやってません。
_textStateプロパティをnullにしているぐらい

ではあらためて Window_Messageupdate()メソッドです。
ここで色々とアップデートが行われてますが、メッセージの表示に関わりがあるのはズバリ updateMessage()startMessage() ですね。名前を見ればわかります(笑)

startMessage() では $gameMessage.allText() メソッドで保存されてるテキストから文字列を取り出して textStateに設定し、表示の下準備をしてます。
メッセージの扱いはかなり面倒くさく、できれば避けたいのですが…どうも避けては通れない雰囲気です。

textState のコードを読む

まず textState ってなんだって話ですよ。
Window_Baseクラスの createTextState()メソッドの中身を見ると、だーーっとプロパティが設定されているのが分かります。
『RPGツクールMZ』の[文章の表示]は基本的に1文字ずつ表示されるので、途中どこまで表示したかとか、次に表示する座標はどこかなどの状態を保持しておく必要があるわけで textState がそれ。
詳細を知りたい人は MV.TextState を読んでコードも読んでね。

Window_Message の newLineX を読む

ではまた update() の続きです。
newLineX()メソッドで顔画像のあるなしを見て、startX(文章の開始位置)を決めてます。

Window_Message.prototype.newLineX = function( textState ) {
    const faceExists = $gameMessage.faceName() !== "";
    const faceWidth = ImageManager.faceWidth;
    const spacing = 20;
    const margin = faceExists ? faceWidth + spacing : 4;
    return textState.rtl ? this.innerWidth - margin : margin;
};

ここで marginローカル変数が定義されてるんですが、これ Windowクラスの margin とは一切関係ない値なんですよね…なんで同じ名前つけるのか。
いやまぁ意味的にはどっちも余白(margin)かもしんないけどさぁ…プロパティと同じ名前かつ役割の違うローカル変数は良くない。
ふつー同じ名前つけたら最終的に this.margin = margin とやって同期取るんで、そういう予想でコード読んだらそうならなくて大混乱です!
このタイプの名前被りを、他で見かけないのは幸いです(知らないだけかも…)

閑話休題、顔画像がある場合 startX は、ImageManager.faceWidth で取れる固定値144 + 20 = 164 になります。
20は突然現れたリテラル、つまり固定値なんですよね…。
顔画像がない場合、これまたここで突然現れた固定値4になる。
いやこれ、Window.png の画像変えた時に絶対調整したい数字じゃないですか、なんで固定値なの。
一応、顔画像のサイズを ImageManager.faceWidth から取っているところを見ると、顔画像サイズは [データベース]-[システム2]-[高度の設定]の項目スッカスカな所で設定できるようにしたい、と考えてた雰囲気はあります。
しかし、顔画像の表示位置ぐらいになると「あっ、これリリースに間に合わんわ」みたいな感じですっ飛ばして、リテラル直書き実装された空気。
がんばって、もーちょっとだけ頑張ってプリーズ!!

アラビア語圏の方には悪いんですけど rtl(右から左に読む言語)プロパティは無視。

Window_Message の newPage を読む

newPage()メソッドで resetFontSettings() など行って、諸々のメッセージ表示の初期化が行われています。

    textState.x = textState.startX;
    textState.y = 0;

とやっているので文字表示の開始位置も確定してますが、この x, y は具体的にはウィンドウのどこに当たるんでしょうか。
おそらくcontents 内部の座標だと思われるので、実際そうなのかさらにコードを追います。

次に設定しているのは height です。

    textState.height = this.calcTextHeight( textState );

さらに新規の行の準備をする Window_BaseprocessNewLine()で次のような処理を行って、textState.height を重ねて行のy位置を決めていることが分かります。

Window_Base.prototype.processNewLine = function( textState ) {
    textState.x = textState.startX;
    textState.y += textState.height;
    textState.height = this.calcTextHeight( textState );
};

Window_Base の calcTextHeight を読む

さて、ここで登場している calcTextHeight()Window_Base のメソッドで、名前の通りに行高さ textState.height を計算してます。
行高さを求めるのが以下の式。

textState.height = lineHeight() + 行の最大フォントサイズ - システム指定フォントサイズ

……まーーーったく意味がわからない。
というのも、フォントサイズを文章中に制御文字の \{ とかで変更していないなら、次の式が成り立ちます。

行の最大フォントサイズ - システム指定フォントサイズ = 0

つまり

textState.height = lineHeight()

そして lineHeight()メソッドなんですが……、36に固定なんです!
フォントサイズは[データベース]-[システム2]-[高度な設定]-[フォントサイズ]があって大きさ変えられるのに、ホワーイ?!

さらに、よく考えてください「行の最大フォントサイズ」ということは、
行の文字全てのフォントサイズがシステム指定フォントサイズより小さかった場合にどうなるのか。

textState.height = lineHeight()+ 行の最大フォントサイズ - システム指定フォントサイズ

なので、たとえば行の最大フォントサイズが10、システム指定フォントサイズが40だと。

36 + 10 - 40 = 6

システム指定フォントサイズが大きいとむしろ行高さが狭くなる、というわけのわからん事態が発生します。
ここではたったの6ピクセルしかありません、行間が0どころか文字が重なっちゃいます。
もっとシステム指定フォントを大きくすると、行高さがマイナスになることも容易に分かります。

HI・DO・I!

Window_Message の updateMessage を読む

さて気を取り直して、気を確かに持って…くじけそうな心をふるい起こし、ともすれば崩折れそうな震える膝に手を当て、深呼吸をして前に進みましょう。

文字を書く準備はできましたから、またWindow_Message に戻って今度は実際文字を書いていく updateMessage() の方を見て行きます。
このへん制御文字(\>)やボタン入力で一気に表示するとか、逆に待つとかの処理もしてますが、最初に文字表示に直接関係あるのは processCharacter()です。
processCharacter()Window_Baseクラスのメソッドで、改行とエスケープ(制御文字)の処理もしてますが、基本は textState.buffer に文字を溜め込んでいます。

溜め込んだ文字は、これも Window_Baseクラスの flushTextState()contents に描画されています。
ちょっとこのメソッドややこしいんですけど、重要なところは Bitmapクラスの drawText()メソッドで文字の描画を行なっている部分です。
(ちなみに Window_BasedrawText() とは別ものなのでご注意ください)
前に代入処理など行ってますが、結局 width 以外は textState の値が使われて 、実質以下のようなコードです。

this.contents.drawText( textState.buffer, textState.x, textState.y, width, textState.height );

やっとここで textState に保存した x, y が使われるわけです。
結局は予想通りcontents 内部の座標そのままでした。

ここで気をつけるのは height です。
drawText() では height で指定した範囲の中央に文字が描かれます。
文字が中央に配置されるから、行の上下に空白が入ります。
下に揃えてると思ってたので、レイアウトが合わないわけです。

例えば規定値の26ピクセルのフォントだと、文字の開始位置は行高さ36の中央に表示されます。
ウィンドウの上部から考えると、padding の12に加えて (36-26)/2 = 5 の隙間ができるわけですから、合計してウィンドウ上部から17ピクセルに文字が表示されることになります。

ちなみに、width に関しては Window_BasetextWidth() で計算されます。つまり文字に必要な幅ぴったりが用意されるので、文字寄せの左・中央・右の差はありません。
また flushTextState() は細切れに呼ばれるので、文章全体の文字寄せとは関係ありません。
あとついでに px の p の下の方が26のラインより下に出てるのが気になるかもしれませんが、英文の文字の中央と文字サイズは日本語と違ってて、大文字 M の高さが文字のサイズであり、g とか q とか下に出る部分は文字サイズからは除外されてます。
英文の文字レイアウトを真面目にやろうとすると、めっちゃ大変なので、ツクールは雑な計算でそこそこの見栄え程度にしたみたいです。

Scene_Message のコードを読む

さて前回 Scene_MessageWindow_Message を生成しているところを読もうとしていたところを覚えているでしょうか。
やっと準備が整ったので、そこやっていきます!!

その名もズバリ createMessageWindow() というメソッドがあって、その中で大きさを決めてるのが messageWindowRect()メソッドです。
さらにその中で高さを決めてるのが Scene_Baseクラスの calcWindowHeight() です。

    const wh = this.calcWindowHeight( 4, false ) + 8;

見ての通り、行数をリテラルの 4 で渡してるんですよ…。
またここで雑に +8 とかリテラル書いて調整しているのも嫌な感じです。
おそらくウィンドウ下の入力待ちサインを表示する領域を確保するためのものだと思うんですが、文章レイアウト的には緊張感のない隙間が開いちゃって、なんともモヤモヤします。

calcWindowHeight() の中身ですが false 引数で呼んでいるので Window_Baseクラスの fittingHeight()メソッドが呼ばれていて、実質の処理はここにあります。

Window_Base.prototype.fittingHeight = function( numLines ) {
    return numLines * this.itemHeight() + $gameSystem.windowPadding() * 2;
};

windowPadding() は前回も出てきた、12の固定値を返す憎いあんちくしょうです。
ここで謎なのは、lineHeight() じゃなくてitemHeight() を呼んでるところです。
Window_Message の高さ決めるなら項目の高さじゃなくて行の高さで決めないとおかしくない? まぁ…両方とも同じ固定値36を返すんですけど、ここで使い分けなかったらなんで別名のメソッドを用意したんだって話ですよ。

結局計算されるウィンドウの高さ height は 4 * 36 + 12 * 2 + 8 = 176 の固定値なんですよね。
こんだけ固定値ばっかりだといっそ、YOU wh = 176 って書いちゃえよ! とか言いたくなります。

まとめ

リテラルがいきなり出てくるハードコーディングが多く、そうでなくてもツクール本体の設定やコマンドで変更できない固定値ばかりで、レイアウトの不自由さがすごい。
その上、行高さの計算式が異次元で、状況によってはフォントを大きく設定すると行高さが減るというわけのわからないことになっています。

ちょっと本気でメッセージウィンドウを新規開発した方がいいんじゃないかと思えてきた。
けど、コードを見れば全書き換えしたくなるのはプログラマの悪癖なので、プラグインでできるだけ対応します。
大体の理屈は分かったと思うので、どうにか TF_VectorWindow.js を完成できそう。

ではでは、エンジョイツクールライフ!! (無理やりな締め)

おまけ: 『RPGツクールMZ』文字表示関連のメソッド一覧

Discussion