[scratch-vm] pickメソッドで高度なスプライトの操作を制御する


2021/03/02

今回の内容もかなりディープな
scratch-render(以降ではrenderと略記)の使いこなしメソッドの一つであるpickメソッドを、スクラッチのアプリの核とも言えるscratch-vm(以降ではvmと略記)で使うための注意点を解説します。

vm内でrenderを使うときの問題点

あらためて
renderのpickメソッドは、ステージ上の座標からスプライトを検出してそのDrawableIDを返してくれるスクラッチゲーム開発には無くてはならないメソッドです。

このrenderはvmの標準ライブラリとして組み込まれているので、当然vmからもpickメソッドが使えるようになっているのですが、vmから呼び出して使う際にまず何が問題となるのか説明していきます。

vmでpickメソッドを使うときの問題点

まず理解しないといけないはvmのステージの座標はデフォルトでステージの左端:-240、右端:+240の右方向を正、かつ下端:-180、上端:+180とする上方向を正とする座標系をとります。

そこは
setStageSizeメソッドの実装をじっくりみるとなんなく分かると思います。

            /**
* Set logical size of the stage in Scratch units.
* @param {int} xLeft The left edge's x-coordinate. Scratch 2 uses -240.
* @param {int} xRight The right edge's x-coordinate. Scratch 2 uses 240.
* @param {int} yBottom The bottom edge's y-coordinate. Scratch 2 uses -180.
* @param {int} yTop The top edge's y-coordinate. Scratch 2 uses 180.
*/
setStageSize (xLeft, xRight, yBottom, yTop) {
    this._xLeft = xLeft;
    this._xRight = xRight;
    this._yBottom = yBottom;
    this._yTop = yTop;

    // swap yBottom & yTop to fit Scratch convention of +y=up
    this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);

    this._setNativeSize(Math.abs(xRight - xLeft), Math.abs(yBottom - yTop));
}
        
そして何も考えずにvmでpickメソッドを呼び出したとして、下の図のようにとある四角の枠(高さH)で表したスプライトを検出したいとしましょう。

おそらく画像の位置によっては、クリックしてステージ上のスプライト画像を触っているにもかかわらず画像は検出されないかもしれません。

かと思えば、画像から離れた上の何もなさそうな位置をクリックしたらスプライトを検出してしまうような良くわからない挙動もあるかもしれません。

このvmの謎の挙動の原因は、vmが内々に定義しているステージの座標が、renderのそれは別々になっているからです。

vmとrenderのステージサイズの違いを捉える

renderとvmとのステージの構成を以下の図で説明しましょう。

この図で左側がrender内部で制御しているステージ座標、右側がvm内部で制御しているステージ座標を表しています。

まずもっとも大きな違いとして、左から右方向を正とするのはスクラッチステージと同じですが、
vmの方が下から上方向を正とすることがrenderと逆になるので注意が必要です。

またステージ上の中心座標と定義している場所も異なります。

vmの場合には
(0,0)が原点でありステージの中心となりますが、renderの場合にはステージの左上が原点で、ステージの中心は(240,180)の位置です。

よって、vm上で
(x',y')というステージ上の座標をクリックしたとすると、実際にスプライトが描画されていてpickメソッドが調べているrender上の(x,y)の座標へ正しく変換しないと、vmのステージの画像をいくら叩いても検出されなかったわけです。

pick関数然り、scratch-renderの関数には、svgスキン(画像)の中心(画像の中点)を自動で割り出し、そこからの距離を起点にクリックされた座標との距離などを計算していますので、スクラッチはy方向を+に進んだつもりでも、svg画像としては逆(ー)方向に進んでしまうことを意味しています。

さて、vmとrenderのステージにおける座標の違いが理解できれば、pickを利用する際の引数をちょっとした工夫をすることで対応できます。

今回でいえば、

            x' = x + 240
y' = -y + 180
        
と座標値の変換を行えば正しい位置でpickメソッドが検出してくれます。

以降では折角ですので具体的な実装のポイントを交えてスプライトの検出に触れてみます。

簡単なpickメソッドの利用例

まずは簡単な50x50の正方形のsvg画像をクリック(pick)するターゲットにしてみます。

            <svg width="50" height="50" version="1.1" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
 <g fill="#00bb00">
  <rect x=".32977" y=".32977" width="49.34" height="49.34" stroke="#de9700" stroke-linecap="round" stroke-linejoin="round" stroke-width=".65955"/>
  <path d="m0 25.151h50" stroke="#000" stroke-width=".30238"/>
  <path d="m25.15 0v50" stroke="#000" stroke-width=".3"/>
 </g>
</svg>
        
なおpickを呼び出す時にsvg画像に設定されているビューポートの扱いがとても重要な働きをします。何かスクラッチ上でインライン化したsvgを利用して、表示が変になっていると感じたらまずビューポートの設定を確認してみてください。

svgのビューポートに関しては、
以前の記事で詳しく取り上げ説明していましたので、そちらをご参照ください。

もちろんvmのpickを使うためには拡張機能を自作する必要がありますが、ここでは実装の触りだけを以下に記載します。

            //👇拡張機能のindex.jsのgetInfo関数
getInfo () {
    return {
        //...
        blocks: [
            //....
            {
                opcode: 'pick',
                text: 'Pick from ([CX], [CY])',
                blockType: BlockType.REPORTER,
                arguments: {
                    CX: {
                        type: ArgumentType.NUMBER,
                        defaultValue: 0
                    },
                    CY: {
                        type: ArgumentType.NUMBER,
                        defaultValue: 0
                    }
                }
            },
            //....
        ],
        //...
    };
}

//👇pickブロックの中身
pick(args, util) {
    const cx_vm = Cast.toNumber(args.CX);
    const cy_vm = Cast.toNumber(args.CY);
    console.log(`${cx_vm}:${cy_vm}`);

    let pickedId_;
    const cx_rndr = _cx + 240;
    const cy_rndr = - _cy + 180;
    pickedId_ = this.runtime.renderer.pick(cx_rndr, cy_rndr);
    console.log(`Detected a drawble#${pickedId_}`);
    return pickedId_;
}
        
このカスタマイズしたpickブロックを使えば、スプライトを適当な位置に配置しても位置ズレを起こすことなくマウスのクリックの当たり判定に利用できるようになります。めでたしめでたし。

まとめ

今回はvmでpick関数を使う場合の注意点のお話を中心に行いました。

このテクニックを用いた拡張機能の具体的な実装は今後のブロック記事でじっくりと解説を行いたいとおもいます。

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

これからの"地方格差"なきプログラミング教育とは何かを考えながら、 地方密着型プログラミング学習関連テーマの記事を不定期で独自にブログ発信しています。