2015年4月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
無料ブログはココログ

« 2013年12月 | トップページ | 2014年4月 »

2014年3月の1件の記事

2014年3月22日 (土)

MOON で使い回しの効く GUI の作り方?

ああ、S-II に間に合わない

前回の記事を書いたのが 12/15. あれから 3 ヶ月も記事を上げていません。

何か一つシールを作成してからブログを書こうと思っていたのですが、はまり続けたり、仕事が忙しくなったりして進捗よろしくありません。

どうやら enchantMOON S-II/MOONPhase 2.9.0 update までには何も完成しそうになく、なんとなく口惜しいので、駆け込みで記事を一本あげておこうと思います。

GUI を使いまわしたい

年末から作りたいシールがあって、その準備としてあれこれ試しているあいだに、気が付けば 3 ヶ月以上。もっとも、2.9.0 になると結局 JSON 周りを作り直しになるようなので、怪我の功名なのかもしれませんが。

それはともかく、最終目標には届かないものの、試した中でつかめたことがあるので、まとめておきます。それは、

enchantMOON 或いは enchant.js で、再利用の聞くGUI の作り方。

いや、まぁ、これがベストではないのかもしれませんが。

とりあえず、ノウハウ収取目的に、作ろうとしていたのは、このようなものです。

画面に文字をいくつか書いておく



そのうちの一つを画面から切り抜く



切り抜いた文字を画面の中で自由に動かす



その前段階として、とりあえず Sprite に張り付けて、touch event を受け付けるようにする

二段階目の「画面に描いた stroke を切り出す」というのは、去年作った「コピペシール」の GUI ライブラリがそのまま使えます。ということで、ライブラリを使って書いた、最初のコードのコア構造だけを抜き出すと、以下のようになります。


リスト-1

ontap = function(){
    var GUI_A = function( 引数s, callback ){
        var gui = new Game(...);
        gui.onload = function(){
            .....
        }:
        gui.start();
    };

    var GUI_B = function( imageData ){
        var gui = new Game(...);
        gui.onload = function(){
            var scene = gui.rootScene;

            var sprite = new Sprite(...);
            sprite.image = new Surface(...);
            var ctx = sprite.image.context;
            ctx.putImageDate( imageData, 0, 0 );

            scene.addChild( sprite );
            sprite.addEventListener( 'touchstart', function(){
                MOON.finish();
            });
        };
        gui.start();
    };

    GUI_A( 引数s, function( imageDate ){
        GUI_B( imageDate );       
    });
};

このコードを実行すると、まず、GUI_A が呼び出され(実際にはここでライブラリを呼び出しています)ます。内部では esumasui さんの作られた、storoke data をラスタライズするライブラリを呼んで、imageData を取得します。

imageData を callback 関数に渡すと、その中で GUI_B を呼び出して、ここで GUI が切り替わります(という目論見だったのですが…言い訳は後ほど)。

GUI_B では、Sprite を一つ生成して、imageData を canvas に張り付け。sprite を scene に追加して表示、sprite に event listener を登録。思惑通りなら、sprite をタップすれば、シールが終了するはずです。

実際に走らせてみると、画面から切り出した文字を張り付けた sprite が画面に表示されるところまでは、期待通りに動作します。ところが、sprite をタップしても終了しません。どうやら、event が sprite に渡らず、他の画面要素に奪われているような印象です。或いは、画面要素の重なり方向の順序を適切に指示できていないような。

この設計ではムリだ。というのがわかりましたので、ライブラリを使わずに、ベタに書き下してみました。


リスト-2

ontap = function(){
    var gui = new Game(...);

    gui.onload = function(){
        var scene = gui.rootScene;

        //
        // gui parts set 1 の生成、scene に登録 - 詳細略
        //

        ok_button.addEventListener( 'touchstart', function(){
            //
            // imageData を esumasui さんのライブラリで作成してから
            //
            changeGUI( imageData );
        });

        var changeGUI = function( imageData ){

            //
            // scene から gui parts set 1 を削除
            // 新しく、gui parts set 2 を生成、scene に登録
            // sprite に imageData を張り付け
            //

            sprite.addEventListener( 'touchstart', function(){
                MOON.finish();
            });
        });
    };

    gui.start();
};

最初に書いたコードとライブラリを切り貼りして、上記のようなところまで書き下してみると、今度は期待通りに動きました。(実際には、ここまでくるのにかなり試行錯誤しましたが)結局 Game オブジェクトを複数使うと、event の取り回しが難しくなって、思ったようなプログラムを(少なくとも簡単には)書けない、ということのようです。

で、書き下せば動くのですが、これでは GUI をライブラリ化していって、気軽に組み合わせてアプリケーションを作っていくことが、えらく難しくなってしまいます。それはあんまりだ、何かいい方法はないか、と、解決策/設計方針が決まるまで、随分時間がかかりました。

漸く納得できた設計のコア構造は、以下のようになりました。


リスト-3

ontap = function(){
    var gui = new Game(...);

    var make1stScene = function( game, callback ){
        var guiWidth = game.width;    // 画面幅
        var guiHeight = game.height;  // 画面高さ
        var scene = new Scene();

        var widget1 = new ...;
        var widget2 = new ...;
        var widget3 = new ...;
        var widget4 = new ...;

        scene.addChild( widget1 );
        scene.addChild( widget2 );
        scene.addChild( widget3 );
        scene.addChild( widget4 );

        widget1.addEventListener( ... );
        widget2.addEventListener( ... );
        widget3.addEventListener( ... );
        widget4.addEventListener( ... );

        ok_button.addEventListener( 'touchstart', function(){
            //
            // imageData を esumasui さんのライブラリで作成してから
            //

            callback( imageData );
        });

        return scene;
    };

    var make2ndScene = function( imageData, game ){
        var guiWidth = game.width;    // 画面幅
        var guiHeight = game.height;  // 画面高さ
        var scene = new Scene();

        var widget1 = new ...;
        var widget2 = new ...;
        var sprite = new Sprite();
        //
        // sprite に imageData を張り付ける
        //

        scene.addChild( widget1 );
        scene.addChild( widget2 );

        widget1.addEventListener( ... );
        widget2.addEventListener( ... );

        sprite.addEventListener( 'touchstart', function(){
            MOON.finish();
        });

        return scene;
    };

    gui.onload = function(){
        var firstScene = make1stScene( gui, function( imageData ){
            var secondScene = make2ndScene( imageData, gui );

            gui.popScene();
            gui.pushScene( secondScene );
        });

        gui.pushScene( firstScene );
    };

    gui.start();
};

これで動きます (^_^)

最終的に、どういう設計方針にしたのか。

Game() の下に GUI を閉じ込めて、これをライブラリとして順番に呼び出すアプローチは、「リスト-1」のパターンで、失敗しました。

「リスト-2」のパターンなら動きますが、再利用が効きません。無論、このパターンでも、全て一から書き下しているわけではなく、widget 的な GUI パーツ(複数の Sprite を組み合わせて、全体をまとめてドラッグできたり、どのボタンを押したかの結果を返すことができるようなもの)はライブラリから引っ張ってくるのですが、これだと「特定の機能」をパッケージ化できません。

欲しかったのは、「このパッケージを呼び出すと、特定の操作を可能にして、その結果をメインルーチン側に戻せる」ような複雑な機能のパッケージを再利用しやすい形で成立させられる方法です。

で、結論が「リスト-3」。Scene の単位でパッケージ化するのが、私が今考え付く BEST です。

漸く結論が出たので、コピペシールの時に作った、画面の上に重ねて書いた stroke を返すライブラリの構造も書き変えました。


書き変え前

GUI_GetStroke.single = function( buttons, caption, callback ){
    var gui = new Game(...);
    var screenWidth = 768;
    var screenHeight = 1024;

    gui.onload = function(){
        var scene = gui.rootScene;

        // GUI パーツを生成・配置
        // event listener を割り付け

        ok_button.addEnvenListener( 'touchstart', function(){
            callback( strokeData );
        });
    };

    gui.start();
};

書き変え後

GUI_GetStroke.single = function( buttons, caption, callback ){
    var gui = new Game(...);

    gui.onload = function(){
        gui.pushScene( GUI_GetStroke.sceneSingle( buttons, caption, gui, callback );
    };

    gui.start();
};


GUI_GetStroke.sceneSingle = function( buttons, caption, game, callback ){
    var screenWidth = game.width;
    var screenHeight = game.height;

    var scene = new Scene();

    // GUI パーツを生成・配置
    // event listener を割り付け

    ok_button.addEnvenListener( 'touchstart', function(){
        callback( strokeData );
    });

    return scene;
};

これで、お手軽に単機能として使いたい場合は、GUI_GetStroke.single() を呼び出せば、内部で Game() も生成して結果を返してくれる。他の GUI と組み合わせて、少し凝ったものを作りたい場合は、呼び出し側で Game() を生成して、GUI_GetStroke.sceneSingle() を呼び出す、という両方の使い方が可能になりました。

しかし、javascript って・・・

いやぁ、しかし、javascript 難しいです。

オブジェクトも高階関数も使えて抽象度は高いのに、文法要素が primitive 過ぎて、名前空間を使い分けるだけで一苦労。デバッグのコツもわからないし、callback, 非同期処理の書き方も、理解が追いつくのにかなり苦労しています。

よくできたライブラリに沿って使うなら難しくないのかもしれませんし、使いこなせば primitive な分だけ「なんでもできる?」的な処もありそうですが、少し踏み込んだことを書こうとすると、とたんに「ワザ」を要求される感覚で、言語のリファレンスを見ただけでは何も書けません。

巷では、「今、初心者がプログラミングに手を出すなら javascript だ」、というような記事が溢れていますが、確かに処理系はすぐ手にはいりますし、出力もすぐ得られるという処は利点ですが、ほんとに初心者が最初に手を出すのに向いている言語なんですかねぇ。非同期処理なんかは、これまでに GUI のフレームワークや event driven の概念を知っていたので理解が追いついてきましたが、そうでなければ今でも理解できていなかったと思います。

いろいろと批判もあった BASIC ですが、効率がわるい書き方でもとにかく動き、アルゴリズムにだけ集中すれば、書き方の「ワザ」をあまり要求されなかったのは、よい言語だったのだな、と、今更ながら感じるようになりました。もっとも、VB はライブラリの機能と、文法要素の貧弱さのバランスが悪すぎて、使いたくない言語ではありますが。

4 月からの enchantMOON も

展開が楽しみです。javascript 難しいですが勉強して、軌道速度に振り落とされないように、しっかりついていきたいと思います。

というか、この機会を取り洩らしたら javascript 深められずに一生おわっちゃうだろうな。

« 2013年12月 | トップページ | 2014年4月 »