9.13 レイヤの自動移動(その1)

突然だけど、move タグを実行すると何でレイヤが移動するか知ってる?
ホントに突然だね…
move タグを拡張して拡大・縮小・回転できるようにするから、 最初に基本的なことを聞いとこうかなーって思ってね。
う〜ん… move タグを実行するとレイヤが移動するのが当たり前みたいな感じだから、 何で移動するのかって言われてもねぇ…
よくわかんない?
うん。
まぁ普通はそれでも問題ないんだけど、これから move タグを拡張してくわけだから、 何でレイヤが移動するのかってのを簡単に見ていきたいと思うんだけど、いいかな?
あ、うん。そーだね。その方がいいかもね。
じゃレイヤが移動する原理を見てくことにするね。
は〜い。
って言ってもレイヤを移動する原理はカンタンで、 こんなふうにレイヤの位置をちょっとずつずらしながら表示すれば、 レイヤが移動してるように見えるんだ。

<レイヤ移動の原理>

上の図の (a), (b), (c), (d) の画面はちょっとずつ画像の位置が違ってるでしょ。
これを (a) → (b) → (c) → (d) の順番に素早く切り替えると、レイヤが移動してるように見えるの。
なんかアニメーションみたいだね。
そう。簡単に言うとレイヤの移動はアニメーションとおんなじ原理を使ってるってコトだね。
まぁ普通 KAG でアニメーションする時は copy タグでレイヤの一部にセル画像をコピーしてくわけだけど、 レイヤの移動の方はもっとシンプルで、レイヤの位置とか不透明度(opacity のことね)を変えてるだけなんだ。
へぇ、そーなんだ。
ってワケで、move タグの原理はこんな感じで簡単なんだけど、 レイヤを自動的に移動させるスクリプトを書こうと思うともっと複雑になっちゃうんだよね。
え、そーなの?
じゃあどーいうスクリプトを書いたらいいと思う?
う〜ん…確かにどーやって作ったらいいんだろ?
基本的にはレイヤの位置とか不透明度をちょっとずつ変えてけばいいわけだから、 一定の時間ごとに繰り返しメソッドが呼び出されたりするシステムがあると作れそうだよね?
一定の時間ごとに繰り返しメソッドが呼び出されるってことは…タイマーとか?
※タイマーについては §3.9 参照。
ん、そのとーり。
タイマーを使えばレイヤを自動的に移動させられるよね。
あ、タイマー使うんだ?
んー、タイマーを使っても作れるんだけど、 move タグのシステムにはタイマーは使われてないんだ。
えっ? じゃあ何使ってるの?
move タグはコンティニュアスハンドラを使って作られてるの。
こんてぃ…??
コンティニュアスハンドラってのは、 絶え間なく(continuous)処理するもの(handler)って意味で、 タイマーとおんなじで予め登録したメソッドを繰り返し呼び出すんだけど、 一定の時間間隔でメソッドを呼び出すんじゃなくって、 「できる限り頻繁に」メソッドを呼び出すんだ。
できる限り頻繁にメソッドを呼び出すって?
例えばトランジションしてる途中は画面全体を切り替える処理をしなくちゃいけないけど、 クリック待ちしてる時って普通はクリック待ちカーソルの表示くらいしかやることないでしょ?
うん、そーだね。
コンティニュアスハンドラに登録されたメソッドは、 トランジションしてる時みたいにシステムが忙しい時はあんまり呼び出されないんだけど、 クリック待ちしてる時みたいにシステムが暇な時にはたくさん呼び出されるの。
へぇ、なんか変わってるね。
move タグでレイヤを動かす時には、 できるだけ頻繁に画面(レイヤ)の表示を更新した方がキレイに動いてるように見えるわけだけど、 そればっかりやってたらトランジションとかメッセージ表示とかの重要な処理が遅くなっちゃうでしょ。
そーだね。
コンティニュアスハンドラを使うと、基本的に出来る限り頻繁にレイヤを動かして画面の表示を更新するんだけど、 他に重要な処理があったらそっちの方を優先して実行できるんだ。
タイマーだと重要な処理が他にあるかどうかに関係なく一定の時間ごとに呼び出されるから、 move タグの処理にはコンティニュアスハンドラの方が向いてるってワケ。
なるほど、ちゃんと考えて作ってあるんだね〜。
だね。
さて、これで move タグでどうやってレイヤを動かしてるか大体解ったと思うから、 実際にこのやり方でレイヤを動かしてみよっか。
え、でもさっき move タグのスクリプトは複雑って言ってなかったっけ?
ん、だからもっと単純にレイヤを動かすスクリプトを書いてみるの。
あ、そーなんだ。
じゃ単純にレイヤを動かす SimpleMover クラスを作ってみるね。
スクリプトはこんな感じ。

<簡単なレイヤの自動移動を行う SimpleMover クラス>

class SimpleMover
{
    // メンバ変数
    var layer;     // 動かすレイヤへの参照
    var lLimit;    // 左方向にこの位置まで移動可能
    var rLimit;    // 右方向にこの位置まで移動可能
    var moveWidth; // 移動可能な幅
    var speed;     // 移動速度(ピクセル/秒)
    var startTime; // 移動開始時刻
    var moving;    // 移動しているか

    // コンストラクタ(引数に動かしたいレイヤを指定します)
    function SimpleMover(layer)
    {
        this.layer = layer; // レイヤへの参照を保存しておきます
        moving = false// 初期状態では移動していないので false を代入します
    }

    // デストラクタ
    function finalize()
    {
        stopMove(); // 無効化される前にレイヤの移動を停止します
    }

    // レイヤの移動を開始します
    function beginMove(speed = 200)
    {
        // 移動可能な左端の座標を計算します
        lLimit = -layer.width;
        // 移動可能な右端の座標を計算します
        rLimit = kag.scWidth;
        // 移動可能な幅を計算します
        moveWidth = rLimit - lLimit;
        // レイヤを初期位置(左端)に移動します
        layer.setPos(lLimit, (kag.scHeight - layer.height) \ 2);
        // 移動速度を設定します
        this.speed = speed;
        // コンティニュアスハンドラを追加します
        System.addContinuousHandler(handler);
        // 移動開始時刻を記録します
        startTime = System.getTickCount();
        // レイヤの移動が開始するので moving を true にします
        moving = true;
    }

    // レイヤの移動を停止します
    function stopMove()
    {
        if(moving) // レイヤが現在移動中であれば...
        {
            // コンティニュアスハンドラを削除します
            System.removeContinuousHandler(handler);
            // レイヤの移動が停止するので moving を false にします
            moving = false;
        }
    }

    // コンティニュアスハンドラとして呼び出されるメソッド
    function handler(tickCount)
    {
        try
        {
            if(layer isvalid// レイヤが無効化されていなければ...
            {
                // 移動距離を計算します
                var dist = (tickCount - startTime) * speed \ 1000;
                // レイヤの位置を計算・設定します
                dist %= moveWidth * 2;
                if(dist <= moveWidth)
                    layer.left = lLimit + dist;
                else
                    layer.left = rLimit - (dist - moveWidth);
            }
        }
        catch(e)
        {
            // try ブロック内で例外が発生した場合はレイヤの移動を停止します
            stopMove();
            // 再び例外を投げます
            throw e;
        }
    }
}

…単純っていうわりには結構難しそーだね…
まぁそれなりには、ね。
じゃスクリプト見ていく前に、まずどんなふうにレイヤが動くか実行して見てみよっか。
うん、そーだね。
それじゃスクリプトはここに置いとくから実行してみて。
りょーかい!

<実行結果>

※実際にはもっと滑らかに動きます。

なんか画像が横に動いてるね。
画像を左右に繰り返し動かすように作ってるからね。
ちなみに画面上でクリックすると画像の動きが止まるよ。
…ホントだ。クリックしたら止まったね。
…あ、もう一回クリックしたら最初の位置に戻ってまた動き出した。
クリックするごとに動いたり止まったりを繰り返すようにしてるの。
そーなんだ。
んじゃ動作も確認できたことだし、次はスクリプトを見ていこっか。
は〜い。
まずコンストラクタからね。
layer っていう引数があるでしょ。
これって動かすレイヤ?
そ。だから例えばこの layerkag.fore.layers[0] を指定すると…
表画面の 0 番の前景レイヤが動くんだね。
そういうこと。
じゃコンストラクタの中身を見てくね。
this.layer = layer; って何やってるの?
動かすレイヤへの参照を保存しとくために SimpleMover クラスのメンバ変数にも layer っていう名前の変数を作ってるんだけど、 引数の layer と同じ名前だから、 コンストラクタの中で単に layer って書くとメンバ変数の layer なのか引数の layer なのかわからなくなっちゃうよね。
まーおんなじ名前なんだったらどっちかわかんないよね。
同じ名前の変数がある時どうするかはスクリプトの仕様で決まるんだけど、 TJS の場合は引数の方が優先されるから、単に layer って書くと引数の layer って意味になるんだ。
あ、そーなの? じゃあメンバ変数の layer は?
メンバ変数の方の layer を指定したい時は this を付けて this.layer って書くの。
だから this.layer = layer; っていうのは、 メンバ変数の layer に引数の layer を代入してるってことになるわけね。
this については §3.2 参照。
ふぅん、そーなんだ。
でもなんかややこしくなるから別な名前にした方がいーんじゃない?
ま、それでもいいんだけどね。
今回みたいに同じ意味の変数には同じ名前をつけた方がわかりやすいこともあるから、 こういう書き方もできるってことは覚えとくといいと思うよ。
別にそんな難しい書き方でもないでしょ。
まー確かにね。
あと moving っていう変数に false を代入してるんだけど、 この変数はレイヤが移動してるかどうかを表してるんだ。
動いてなかったら false になってて、 動いてたら true になってるってこと?
そ。最初はレイヤが動いてないから false にしてるの。
moving は他のメソッドで使うから、またその時に詳しく説明するね。
りょーかい。
じゃ次はデストラクタね。
stopMove っていうメソッドを呼び出してるんだよね?
そう。stopMove メソッドはレイヤの移動を止めるメソッドなんだ。
SimpleMover クラスのオブジェクトを無効化する前には一応移動を止めとこうってことで呼び出してるの。
なるほどね。
stopMove メソッドの中身は後で詳しく見ていくことにするね。
うん、わかった。
それじゃ次はレイヤの移動を始めるメソッド、 beginMove メソッドを見てくね。
なんか色んなことやってるみたいだね。
レイヤの移動を始める前の準備とかがあるからね。
そっか。
まず最初にレイヤが移動できる範囲を計算してるんだ。
移動できる範囲って?
さっき実行した時にレイヤが右→左→右→左…って動いてたでしょ。
うん。
移動できる範囲ってのは、レイヤがどこまで右に行ったら左向きの移動に切り替わるかとか、 どこまで左に行ったら右向きの移動に切り替わるかっていうののこと。
あ、そーいうことなんだ。
…確か右方向に移動してる時も左方向に移動してる時も、 レイヤが画面の外に出て見えなくなったくらいのところで向きが変わってたよね?
ん、どっち向きに移動してる時も、レイヤが画面外に出た時点で移動の向きを変えるようにしてるんだ。
で、レイヤ全体がちょうど画面外に出た時の座標をここで計算してるの。
それって lLimitrLimit ってゆー変数のこと?
そ。lLimit が左端の x 座標で rLimit が右端の x 座標だよ。
ちなみにその次の moveWidth にはレイヤが移動できる幅を計算して代入してるの。
この辺は計算式だけだとちょっとわかりにくいと思うから、図にしてみるね。
kag.scWidthkag.scHeight については §9.9 参照。

<レイヤの移動可能範囲>

こんなふうに、レイヤが左に移動して画面外に出た時のレイヤの左端の座標(left プロパティの値のことね)が lLimit になって、 右に移動して画面外に出た時のレイヤの左端の座標が rLimit になるの。
で、lLimitrLimit の間の距離がレイヤが移動できる幅、 つまり moveWidth になるわけね。
lLimit は画面の左端からレイヤの幅(layer.width)の分だけ左側にあるから -layer.width で、rLimit は画面の左端から画面の幅(kag.scWidth)の分だけ右側にあるから kag.scWidth なんだよね?
そうそう。
で、次は setPos メソッドでレイヤを上の図の lLimit の位置に移動してるの。
レイヤは右→左→…って移動するから、初期位置は画面の左端なわけね。
setPos メソッドについては §3.1 参照。
第1引数が lLimit になるのはわかるんだけど、 第2引数の (kag.scHeight - layer.height) \ 2 ってドコなの?
前にレイヤとかウィンドウとか文字とかを表示した時に同じようにして位置を設定したよね?
§8.4§3.1§3.5 参照。
えーと、そーだったっけ? うーん…
あ、レイヤとかを画面の真ん中に表示した時だっけ?
そ。今回は y 座標だけ画面の真ん中になるように設定してるから、 画面を上下に半分に分ける直線上をレイヤが移動するわけね(ちょっとわかりにくい表現だけど、 要するにこの図に描いてるように移動するってコトね)。
なるほどね。
じゃ次いくね。
次はレイヤの移動速度を speed っていうメンバ変数に代入してるの。
speed の値は beginMove メソッドの引数に指定できるようにしてるから、 コンストラクタの時と同じように this.speed = speed; になってるわけね。
speed ってデフォルトで 200 になってるみたいだけど、 これってどれくらいの速さなの?
※メソッドのデフォルト引数については §1.17 参照。
移動速度の単位はピクセル/秒にしてるから、 200 だと1秒間に 200 ピクセル移動するってことになるね。
じゃあ画面の幅が 640 ピクセルだと 3 秒ちょっとで端から端まで移動するってことだね。
ん、そうなるね。
これでレイヤを動かすのに必要な設定が大体できたから、 ここでコンティニュアスハンドラにメソッドを登録してるんだ。
System.addContinuousHandler ってゆーメソッドを呼び出してるとこ?
そ。引数に呼び出したいメソッドを指定して System クラスの addContinuousHandler メソッドを呼び出すと、 指定したメソッドができるだけ頻繁に呼び出されるようになるの。
えっと、引数は handler になってるから、 handler っていうメソッドが呼び出されるようになるってことだよね?
うん。handler メソッドはレイヤを動かすためのメソッドなんだけど、 これはもうちょっと後で見ていくことにするね。
はーい。
んで、その次に System クラスの getTickCount メソッドを呼び出して、今の時間、つまりレイヤが動き始めた時間を startTime っていう変数に代入してるんだ。
System.getTickCount …って今までに使ったことないメソッドだよね?
あ、そーいえばまだ使ったことなかったね。
getTickCount メソッドは今の時間を教えてくれるメソッドなんだ。
今の時間ってことは…Date クラスのメソッドみたいに何時何分とかを教えてくれるメソッドってコト?
Date クラスのメソッドについては §1.3 参照。
ん〜、getTickCount メソッドが教えてくれる時間はそーゆーのじゃないんだよね。
じゃあどんな時間なの?
えっとね、getTickCount メソッドが教えてくれる時間は Windows が起動してからの時間なんだ。
え??
…なんだけど、getTickCount メソッドを使うのは Windows が起動してからの時間を知るのが目的じゃなくて、移動が始まってからどれだけ時間が経ったかを知るのが目的なの。
えっと…どーゆーコトかよくわかんないんだけど…?
例えば、getTickCount メソッドを呼び出した時に 1000000 っていう値が返ってきたとするね。
1000000 ってかなりおっきい値だよね?
getTickCount メソッドの戻り値はミリ秒単位だから、 1000000 は 1000 秒、つまり 16 分 40 秒ってことになるんだ。
あ、そーなんだ。
で、その後 5 秒(つまり 5000 ミリ秒だね)経ってからもう一回 getTickCount メソッドを呼び出すと、 今度は 1005000 が返ってくるの。
5000 ミリ秒経ったから 5000 増えて 1005000 になったってコト?
そうそう。
逆に言うと、2回目に呼び出した時に返ってきた 1005000 から最初に呼び出した時に返ってきた 1000000 を引くと 5000 になるから、5 秒経ったってわかるよね。
まー確かにね。
つまり、getTickCount メソッドを使うとどれだけ時間が経ったかを調べられるわけね。
そーなの?
じゃ実際にやってみるね。
ちょっとこのスクリプトを first.ks に書き込んで実行してみて。

wait タグで待った時間を測るスクリプト(first.ks の中身)>

; wait タグを実行する前の時間を記録しておきます
[eval exp="tf.startTime = System.getTickCount()"]

; 1000ミリ秒(1秒)待ちます
[wait time=1000]

; 待っている間に経過した時間を計算します
[eval exp="tf.waitTime = System.getTickCount() - tf.startTime"]

; 待っている間に経過した時間を表示します
[eval exp="System.inform(tf.waitTime + 'ミリ秒経過しました。')"]

それじゃ実行してみるね。
ん。

<実行結果>

※実行結果(経過時間)は環境によって異なります。

あれ? wait タグで 1000 ミリ秒待ったのに「1004ミリ秒経過しました。」って表示されたよ?
あぁそれはね、wait タグの time 属性に 1000 を指定しても 1000 ミリ秒ぴったり待つのは難しくて、 どうしてもちょっとはズレちゃうんだ。
だから実際に経った時間は 1004 ミリ秒なの。
へぇ、そーなんだ。
wait タグや eval タグに書いてある属性の値とかを読み込んだりするのにも少し時間がかかるから、 そういう処理とかやってるうちに 4 ミリ秒経っちゃったんだろうね。
なるほどねぇ…
じゃスクリプトの方も確認しとくね。
は〜い。
って言っても別に難しいことはやってなくて、 まず tf.startTimewait タグを実行する前の getTickCount メソッドの戻り値を代入してから wait タグを実行して、 tf.waitTimeSystem.getTickCount() - tf.startTime、 つまり wait タグを実行してる間に経った時間を計算して代入してるだけ。
えっと、つまり時間を測りたい時は、最初に getTickCount メソッドの戻り値を変数に記録しといて、何かやった後にもっかい getTickCount メソッドを呼び出して、 戻り値を最初に記録しといた値から引けば、どれだけ時間が経ったかがわかるってことだよね?
ん、そういうこと。
じゃ beginMove メソッドの話に戻るね。
うん。
まぁ後は movingtrue に設定して終わりなんだけどね。
beginMove メソッドを呼び出すとレイヤが動き始めるから movingtrue にしてるんだよね。
そ。beginMove メソッドでやってることは大体わかった?
うん。
…あ、そーいえば、なんで時間を測る必要があるの?
speed っていうメンバ変数があったでしょ?
レイヤが動く速さだっけ?
そう。speed は1秒間に何ピクセルレイヤが動くかを表してるから、 レイヤを動かす時は「レイヤの移動距離(ピクセル)= speed(ピクセル/秒)×経過時間(秒)」っていう式を使ってレイヤの移動距離を計算するんだ。
あ、経過時間がレイヤの移動距離の計算に必要だから時間を測るんだ?
そういうこと。
それじゃ次はレイヤの移動を止める stopMove メソッドを見てくね。
こっちは beginMove メソッドよりだいぶシンプルなんだね。
じゃどんな感じになってるかちょっと考えてみて。
えとね、まず最初に if ブロックがあって、 条件が moving になってるから…
レイヤが動いてたらブロックの中身が実行されるってことだよね。
ん、そうだね。
…で、if ブロックの中で System.removeContinuousHandler っていうメソッドを呼び出してるみたいだけど、これって?
addContinuousHandler メソッドで登録したメソッドをもう呼び出さなくてもいいよ、 ってシステムに伝えるメソッドだよ。
じゃあ System.removeContinuousHandler(handler); を実行したら、 その後は handler メソッドは呼び出されなくなるってこと?
うん。handler メソッドが呼び出されなくなるとレイヤは動かなくなるから…
レイヤの移動が止まるんだ?
そう。あと、今回に限らず addContinuousHandler メソッドで登録したメソッドは、 必要なくなったら必ず removeContinuousHandler メソッドで登録解除するようにしてね。
じゃないと CPU に余計な負荷がかかっちゃうから。
あ、そーなんだ。じゃ気をつけなくちゃいけないね。
ん。じゃ stopMove メソッドに戻って…
後は movingfalse にしてるだけだね。
stopMove メソッドは特に問題ないと思うんだけど、どう?
うん、だいじょぶだよ。
それじゃ次に handler メソッドを見てこっか…って言いたいとこなんだけど、 今回はだいぶ長くなっちゃったから、handler メソッドは次回にするね。
うん、りょ〜かい。
それじゃ、また次回ね!


前へ | TOP | 次へ