縦スクロールシューティング 03 高速ループ入門編


高速ループに挑戦:初級

習うより慣れろです。簡単なものから始めて徐々に複雑な処理をやってみましょう。

例題を出すので例題に沿って自分でまず一回試しに作ってみてください。最初は高速ループというより「アクティブ」の設定に絡んだ例題です。

高速ループ例題 : 注文の多いおにぎり屋サン

1:おにぎり(スプライト)新規に 10 個、これを高速ループを使って作ってください。キーボードのリターンキーを押した時に新規で 10 個作ってくれなきゃ嫌です。10 個以上は作れないようにもしてください。自分で数えて確認するのも面倒だからカウンタとかも使っておにぎりの総数は表示してください。

動作確認用 Java 出力→

動作確認用はカンッペキに重なった位置にわざと表示してありますが作ったおにぎりは紛れも無く 10 個。ポイントは・・・

  1. アクティブのプロパティで「開始時には作成しない」にチェック
  2. キーボードの二重押し対策としてグローバル変数を用いている
今回「高速ループをちゃんと 10 回廻して作った」人と、「 9 回廻して帳尻を合わせた人」と 二種類いるんじゃないかなという点が気になります。9 回廻して帳尻を合わせた人は高速ループとは関係ないけどアクティブオブジェクトのプロパティをもう一度確認してください。「開始時には作成しない」というチェック項目はさりげなく重要なので、この設定を知っててあえて切ったから筆者はループを 10 回回しています。「新規に 10 個」ってのはそういう意味だったんですね。あらかじめ一個作っておいてそこから 9 回ループ廻して作ったのなら、新規に 10 個という条件を満たせていません。細かいようですがこれが掟です。
解答:ソースコード:→

では次のお題、更に細かい注文が来ました。

2 : ウィンドウのサイズは横 200 ピクセル、縦 300 ピクセルです。この中におにぎりを新規に 10 個、高速ループを使って作ってください。オニギリのサイズは縦横 64 ピクセル。高速ループは一個しか指定できません、その上で今度はおにぎりが重ならないように、ちゃんと 10 個あることが確認できるような配置をしてください。そして配置する際にループインデックスを必ず利用してください。通し番号( アクションループ機能 )とかは使ったら反則です。他の注文は 1 と同じ。

動作確認用 Java 出力→

ループインデックスを必ず使用してくださいと注文したのは、ループインデックスの使い方は大丈夫ですかという確認です。変数への代入はやり方によってはループインデックスを利用せずに通し番号を利用するやり方もあるし、他にも先に 5 個作ったら次にもう一回別の高速ループを回して 5 個作る方法もあるし、いろいろ考えを絞れば今回のサンプルは細かく条件を指定して制限をしないと一つの例題に対して複数の解答が簡単に思いつきます。その中であえて強引に一つの高速ループでループインデックスを必ず利用した場合の座標指定の方法を考えてもらいました。今回のポイントは?

  1. ループインデックスは0から始まります

  2. ループインデックスが4以下の処理と5以上の処理を条件指定して分けてあります

画像位置が重なるのを解消する目的なので、画像のサイズ( 64 px )にループインデックスナンバーを乗算して重ならない Y 座標を指定します。ループ一回目は 64 × 0 = 0、二回目は 64 × 1 = 64 です。でも実ウィンドウサイズが縦 300 と指定されてるからインデックスナンバーが 5 以上だとこれ以降は画面サイズ的にオニギリは画面外に出てしまうのです。

注文を満たすためにはループ中に処理を分けて、5 以上のループインデックスの時は X 値をまず右にずらして、Y座標もループインデックスが 0 から始まった時のように指定したいから取得したループインデックスに減算をしたり工夫がしてあります(制限さえなければこんなやり方を推奨もしません)。
解答:ソースコード:→

オブジェクトの移動とコリジョン(衝突検知)

今度はオブジェクトの作成ではなくオブジェクトの移動とコリジョン(衝突検知)を高速ループを使ってやってみましょう。最初は簡単な「移動だけ」を例題として取り上げます。

3:今回はまず動作サンプルをご覧ください。

動作確認用Java出力→

解答:ソースコード:→

動作サンプルを見て同じような動作を作って見てください。高速ループを使って Duke を上から下に高速移動させています。ヒントとして移動量は 1 pixel 以下、高速ループは 13 回という情報を与えます。

ヒント:変数を使う

ヒント:変数を使う

高速ループを廻すときに移動量という考え方は常に頭に入れて下さい。

ソースコードではまず一番最初「フレームが開始」時に、変数 Ypos にスプライトのY位置を代入しています。次に移動量を 1 と定義しました。この移動量ってのは文字通り、移動する量、双六で言うとあなたのサイコロは常に1しか出ないよう賽の目に細工がしてあります。なんでこんなことをするのでしょうか?

双六で最小の移動単位は 1 ですが、MMF2 でも有効な最小の移動単位は 1( pixel )なのです。

高速ループを回しても動かない例:Y座標に直接値(少数)を代入してる

高速ループを回しても動かない例: Y 座標に直接値(少数)を代入してる


サンプルではスプライトのY座標を動かす前に、変数( Ypos )に変数(移動量)を加算して、そこで得られた値をY座標としてスプライトの実 Y 座標に利用しています。

移動量は基本的には 1.0 以下で良いです。MMF2 はスプライトの座標にサブピクセル( 1 ピクセル以下の単位)をサポートして無いのでこれ以下の値をスプライト座標に直接代入あるいは加算しても全部無視(少数部の切り捨て)されます。

例えばスプライトの Y 座標が 10 だった場合、Y 座標に 0.3 を何回加算しても何回高速ループを回してもこのスプライトは絶対に座標を変えることはしません、動きません。本来 10 + 0.3 = 10.3、だからこの加算処理を 10 回繰り返せば最終的に 13 になるはずだけど、座標に浮動少数( float )を加算しても 1 ピクセルという単位より下は毎回切り捨てされてる。だから 10 + ( 0.3 × 10 ) の結果として 10 が返ってきます。そしてスプライトもこれだと座標を変えません。

座標指定には変数を使って移動量の加算を行ってから、実際に座標を指定する際に変数の値を利用している理由はこういうことです。

変数( Ypos )に変数(移動量)を加算する

変数( Ypos )に変数(移動量)を加算する

変数に移動量を加算し、計算結果を Y 座標として代入すれば動く

変数に移動量を加算し、計算結果を Y 座標として代入すれば動く


MMF2 の数値型変数の基本型は long (整数)ですが、これに浮動少数を加算してやれば自動的に型の変換が行われ、アクティブの数値型変数の型は代入するだけで自動的に float (浮動少数)となります。言語としては Java 風に言い換えると「変数がより広い型への変換を要求された時に、(表現できる範囲内で一番近い値へ)暗黙的な型変換が行われます」ってやつです。

Lua だとこれが最初から double 相当なので参考にならないから Java を例に持ってきました。

このような暗黙的型変換あるいは明示的型変換(キャスト)は Java や C のようなしっかりとしたプログラム言語の経験者はすぐに馴染むはずなのですが、多くの MMF2 のユーザーは MMF2 ってこういうものと一旦理解をあいまいにして覚えざるを得ないのかもしれません。ただこれは MMF2 が特殊なのではなく、コンピューターがさまざまな数値を扱う上でそういう仕様になっているのだと覚えた方が正解に近いはずです。

参考少し詳しい型変換の説明

仮に移動量を 1 ピクセルより小さい値(少数)にしたら、高速ループを何回廻すか、つまり高速ループのループ回数で見た目の移動速度を決定するようにします。高速ループを使わないで 1 フレーム 13 pixel 右へ移動とか座標指定してスプライトを動かすことは可能ですが、それだと 13 pixel という空間の連続性を全部無視しているので、その13 pixel という空間になにかオブジェクトがあってしかもそのオブジェクトとの接触を判定したい場合、当然ですが衝突は検出しません。下の図を見てください。1 フレームで移動を完了する場合、その過程の違いを図示しています。

緑の枠はピクセルを表す

緑の枠はピクセルを表す

高速ループ

高速ループ

高速ループ双六

高速ループ双六


例えるなら高速ループを使わない座標移動というのは瞬間移動(ワープ)です。

空間に何かがあるとか無いとか、オブジェクトや背景(障害物)との位置関係なんかを全部無視して瞬時に指定した座標へ移動することができます。あらゆるしがらみを無視して指定した座標へ唐突に出現することができる能力、これが「 MMF2 のメリ込み」と繋がります。「 MMF2 のメリ込み」は瞬間移動に失敗して壁の中に出現してしまったため身動きが取れない超能力者、そんな漫画みたいなシチュエーションです。

MMF2 の標準動作を使うとメリ込む確立が多いのは残念なのですが、MMF2 の標準動作がいまいち「使えない」のもこれが原因となっています。

高速ループと移動量を使うことでこのふざけた関係(メリ込み)の解消を目指します。

具体的には簡単なこと、13 pixel という空間を MMF2 における最低の移動単位である「 1 pixel 」ずつ小刻みに移動することで空間とオブジェクトと障害物との関係を手探りで探しながら一歩ずつジリジリと移動するのが高速ループと移動量の役割です。つまり 1 フレームで 13 pixel 移動させたいなら高速ループを 13 回廻して、移動量は?これは 1 でも良いですし、例えば 0.3 でも良いです。その代わり移動量を 0.3 にするなら高速ループは 44 回廻してください。高速ループを 13 回廻して移動量は 1 ピクセルにするのが一番効率は良いのは分かりますよね。

あるいは当たり判定をもっと適当に処理しても良いのなら移動量を2ピクセル、3 ピクセルとしてその分ループ回数を少なくするのもスマートな手段と言えます。

1 ピクセルの当たり判定にはこだわり過ぎないようにしよう

高速ループと移動量の関係は分かった。しかしなんで 1 ピクセルに拘るなと言うかというとループ廻しすぎの場合があるからです。高速ループを使った移動というのは 1 フレーム内で不確かな足場を叩きながら足場を探して渡る慎重な動作です。そこまで慎重になってでも是非やるべき処理と、そこまで慎重にならなくても良い動作は見極める必要がある。

なぜならば 1 フレームで通常フレームレートの遅延も無く使用できる高速ループの回数というのは CPU の処理能力で左右されるからです。理想的には高速な CPU でも低速な CPU でも等しく軽く処理できるようなスマートさが開発者に求められています。個人開発なら動作保障を自分の持っている PC の処理能力に定めて、それ以下の処理能力しか持っていない PC での動作はサポートしなくても良いです。でも敷居が高すぎるよりも敷居が低いほうに合わせられるならそれがユーザーフレンドリーってものじゃないかと思うのです。

例えば今回 Java ビルドでシューティングを作っていますが、あるいは弾幕系と呼ばれるシューティングの弾何十何百を全部正確に 1 ドット( 1 pixel )単位で高速ループ使って計算するのはゲーム性の追求なのか自己満足による追求なのかよく考える必要があると思います。

それではお疲れ様、以上長文を読んだらソースコードも見て実際いろんな速度を試してみてください。