コアスクリプト:refreshCursorForAll の問題点について

対象が全員のアイテム・スキルを使用する際の選択用カーソルを生成するとき、
MZでは「Window_Selectable.prototype.refreshCursorForAll」で新しく定義されています。
JavaScript:
Window_Selectable.prototype.refreshCursorForAll = function() {
    const maxItems = this.maxItems();
    if (maxItems > 0) {
        const rect = this.itemRect(0);
        rect.enlarge(this.itemRect(maxItems - 1));
        this.setCursorRect(rect.x, rect.y, rect.width, rect.height);
    } else {
        this.setCursorRect(0, 0, 0, 0);
    }
};
ここに「rect.enlarge」という記述があり、これで選択範囲のRectを拡大しているようです。
そのためMZでは選択範囲のカーソルを上からかぶせるような形で実装していることがわかりました。

ここからが本題なのですが、実際プラグインで起こったバグを調べていた時に、
このrect.enlargeで拡大するロジックに問題があるような気がしました。
上記は端的に言うと、「最初から末尾までitemRectを拡大する」といった指定になっていて、簡単に図で表すと・・
02.png
こんな感じになり、1列、1行の並びでは問題は起こらないようです。
問題は2列、2行以上にした場合で・・

03.png
このように奇数の数の時に末尾がずれる問題が起こります。(奇数の場合、末尾が左下になってしまうため)
ちなみにこの事象はMVでは起こりませんでした。

個人的にはバグなような気もするのですが、バグというにはちょっとグレーゾーンなところかなという感じで
対策として下記のように列数で丸める方法しか思いつきませんでした・・。
JavaScript:
    Window_MenuStatus.prototype.refreshCursorForAll = function() {
        const maxItems = this.maxItems();
        if (maxItems > 0) {
            const rect = this.itemRect(0);

            // 最大数を列数で丸める
            let maxRectCnt = maxItems;
            if (maxRectCnt > maxCols) {
                maxRectCnt = Math.ceil(maxRectCnt / maxCols) * maxCols;
            }
            rect.enlarge(this.itemRect(maxRectCnt - 1));

            this.setCursorRect(rect.x, rect.y, rect.width, rect.height);
        } else {
            this.setCursorRect(0, 0, 0, 0);
        }
    };

もしほかにいい対策法がありましたら、参考までにぜひ教えてほしいです。
ここまで読んでくださり、誠にありがとうございました。
 

fspace

ユーザー
コアスクリプトはドキュメントがないので何とも言えませんが、一応仕様……なのかなと思います。いい設計だとはまったく思いませんが……。

Window_SelectableではmaxCols1に固定されていて、二列にはならないのでこの範囲ではとりあえず正しく動きます。refreshCursorForAllだけがわざわざ別のメソッドとして切り離されていることを考慮すると、maxColsを書き換えたら絶対refreshCursorForAllも一緒に書き換えろよ、というメッセージなのかもしれません。

refreshCursorForAllをどう書き換えるべきかはウィンドウによると思います。コアスクリプトにはドキュメントがないので、どのメソッドにどんな性質を期待していいのかまったくわかりません。例えば、itemRectが返す矩形がきれいに並んでいる保証すらないので、enlargeを使うこと自体が正しいかどうかもわかりませんし、そもそもひとつの矩形でカーソルを表そうとしていること自体に無理があるケースも考えられます。

とりあえず問題としている複数行複数列にきれいに並んでいるパターンだけに絞って考えると、最もシンプルな方法は全要素に対してitemRectを実行してすべてをenlargeすることです。愚直な方法ですが、必ずすべての要素を含む矩形が得られます。少し嫌な点としては、どのように並んでいるか知っていることを前提とした処理としてはやや無駄が多いことですね。

並び方の知識を利用して、最小のitemRectでこれを行おうとするとルルさんの方法になります。ただし、この方法はひとつ問題があって、itemRectmaxItems以上のインデックスで正しく動くという保証がありません。itemRectは指定されたインデックスの要素の境界矩形を返すメソッドなので、存在しない要素を指定した際に競合を起こしてエラーになる可能性は十分に考えられます。左上端と右下端ではなく、右上端と左下端の矩形を使うようにすると改善されると思います。

しかし、よくよく考えてみると並び方を知っているということは、itemRectのインターフェースではなく実装に依存しているということになります。これはつまり、itemRectの挙動が変わればrefreshCursorForAllの挙動が壊れる可能性があるということで、だったらわざわざitemRectに依存した実装にする必要もないはずです。直接全要素を含むような矩形を計算して返すのもひとつの方法と言えます。

ごちゃごちゃといろいろ書きましたが、「結局どれがいいの?」と言われるとどれとも言い難いです。自分でも状況と気分次第で変わる気がします。整理されていない上に雑な結論で申し訳ない……。


ちなみに、Rectangle.enlargeはPixi.jsのメソッドで、これ自体に問題があるわけではないので、タイトルはちょっとわかりづらいかなと思います。
 
fspaceさん、ご返信ありがとうございます!
とてもわかりやすく解説していただきありがとうございます。
refreshCursorForAllが独立したメソッドを持っているのは、
確かにここも書き換えるべき意図があってのことなのかもしれませんね。

最もシンプルな方法は全要素に対してitemRectを実行してすべてをenlargeすることです。
これが現状の最適解かもしれません・・。
最初に書いたmaxColsで末尾をそろえるやり方では、
並び順が不規則の時に先頭と末尾を結ぶだけだと間のitemがはみ出す可能性があり得ますね。
そこに気づけただけでも大きな収穫だと感じました。

今のところこれが正解といえるようなベストな実装方法は思い付きそうにありませんが、
ご意見をいただけたことで少しスッキリできました。
ありがとうございます。

ちなみに、Rectangle.enlargeはPixi.jsのメソッドで、これ自体に問題があるわけではないので、タイトルはちょっとわかりづらいかなと思います。
確かにご指摘の通りです。Rectangle.enlarge自体には何も問題はありませんよね・・。
このあと件名修正しておきます。
 
トップ