いよいよ今回は「オブジェクト弾」について話したいと思います。オブジェクト弾を使えるようになれば、一気に弾幕の世界が広がります。
Obj_Create
で作られた弾やレーザーのことをオブジェクトと言います。それではいってみましょう。
前回、とりあえず弾を出すところまで作りました。ただ、波符「
幻視が起こると、一定時間弾の当たり判定が消え、弾の位置がズレていきます。しかし、このような特殊で複雑な挙動は、CreateShot01
では実現できませんし、そのような特殊な挙動を行う弾ごとに標準の関数を提供するのは現実的ではありません。このような特殊な挙動を自分で制御できる仕組みを与えた方がいいのです。
幸い、東方弾幕風ではそのような仕組みが存在します。1つは弾スクリプトであり、敵の挙動を指定するのと同じように、弾の挙動を指定することができるというものです。しかし、弾スクリプトは低速です。東方弾幕風が進化し、弾スクリプトの代替手段が発達した今では、その別の便利な機能を使うのが一般的です。
その代替手段はオブジェクトというものです。オブジェクトを使えば、複雑な挙動を行う弾を制御することができます。この点で言えば、設置型レーザーで使った CreateLaserA
によく似ていると言えるでしょう。
しかし、CreateLaserA
は扱いが手軽な反面、ある欠点があります。それは、FireShot
する前に全ての挙動を指定しなければならず、FireShot
してしまった後の情報を使って弾の挙動を変化させられないのです。また、制御できる挙動にも大きな制限があります。
このような制御を必要とする一番分かりやすい例は、ホーミング弾です。ホーミングは弾を撃った後も、自機の動きに合わせて軌道を変化させなくてはなりませんね。
一方オブジェクトを使えば、弾を撃った後もずっと制御が可能なのです。弾を撃ってから弾が消えるまで、ずっと複雑な変化を与えることができるのです。もちろん、ホーミング弾を作ることもできますし、幻視のようなもっと特殊で複雑なことも実現できるのです。
では、オブジェクトはどのように使えばいいのでしょうか? オブジェクトを使うには先ず、オブジェクトを作成する関数 Obj_Create
を使います。
obj = Obj_Create(type);
// obj : オブジェクト ID
// type : オブジェクトの種類
// OBJ_SHOT / 弾
// OBJ_LASER / レーザー
オブジェクトを作ると、そのオブジェクトを表す値が返されます。これは CreateLaserA
で言うところの ID のようなものです。設置型レーザーでは、ID を使ってレーザーを区別し、目的のレーザーに対して設定を行ったり、レーザーを配置したりしました。それと同じように、この返された値を使ってオブジェクトを区別し、目的のオブジェクトの挙動を変化させることができるのです。
今は弾を作りたいので、例えばこのようにしてオブジェクト弾を作ってみましょう。
obj = Obj_Create(OBJ_SHOT);
ただ、これだけではあまりにも情報不足です。弾の位置も指定していなければ、速度も角度も何もかもが不明です。そこで、これらの情報を与えるための関数を使います。例えば、弾の位置を (100, 200) に移したい場合は
Obj_SetX(obj, 100); Obj_SetY(obj, 200);
とします。弾の状態を変化させたい場合には、それぞれ対応する関数を呼ぶことになります。これらの関数には、変化させたいオブジェクトの ID と、変化後の状態を指定します。
一気に複数の状態を変化させる関数はないので、それぞれの状態に対して個別に呼んでやるか、一気に変化させる関数を自前で作るかになります。これは SetLaserDataA
とは大きく違う点です。手軽さは減るものの、このことがオブジェクト弾の自由度の幅を広げているとも言えるでしょう。
では、このオブジェクト弾を使って幻視をどのように実装すれば良いかを考えてみましょう。幻視の詳しい挙動は次の通りです。
1つずつ見ていきましょう。
先ず 1 の当たり判定消失ですが、オブジェクト弾の当たり判定の有無は Obj_SetCollisionToPlayer
という関数で設定できます。デフォルト(初期設定)では当たり判定有り(true
)です。当たり判定を消すには、
Obj_SetCollisionToPlayer(obj, false);
とします。
次に 2〜4 の弾がズレる挙動です。オブジェクト弾には速度を指定することもできるのですが、その場合は指定した角度の方向にしか進むことができません。そのため、その機能をそのまま使ってしまうと 4 の挙動が再現できなくなります。そこで、さっき使った Obj_SetX
と Obj_SetY
を使って、自前で座標計算を行ってスベらせるといいでしょう。
最後に 5 の挙動です。弾の透明度を変化させるには、Obj_SetAlpha
という関数を使います。正確にはα値というものを設定します。α値は透明度ではなく不透明度で、255 は完全に不透明、0 は完全に透明であることを表します。ここでは半透明なので、真ん中の 128 くらいの値にしておけばいいでしょう。
Obj_SetAlpha(obj, 128);
さて、このようにしてオブジェクト弾を作り、制御できることは分かりましたが、これらの処理はどこに書けば良いのでしょうか? これらの処理は弾1つ1つに対して行う必要があります。オブジェクト ID を配列にでも溜めておいて、それぞれに対して処理を行えば良いのでしょうか? 確かにそれでもできるでしょうが、それよりもっといい方法があります。それはマイクロスレッドを使う方法です。弾を1つ撃つごとにマイクロスレッドを起動し、その中で弾の制御を行うのです。
第12回の図 3 を思い出してみましょう。
|
弾1をマイクロスレッド1が、弾2をマイクロスレッド2が、弾3をマイクロスレッド3が制御していると考えてください。すると、メインスレッドで yield
するごとに1〜3全ての弾の制御が自動的に行われます。弾が 100 個あっても 1000 個あっても同じです。マイクロスレッドを使えば、あたかも CreateShot01
のような関数を呼んでいるかのごとく、弾用のタスクを起動することにより、弾を撃つことができるのです。その弾の制御はマイクロスレッド内で行われ、メインの処理とはほぼ独立した形で制御できるのです。
では、実際に弾用のタスクを作ってみましょう。
// 幻視でブレる弾 // x, y : 発射地点 // v : 速度 // angle : 角度 // type : 弾のタイプ // right : 幻視で右に移動するかどうか task TMagicShot(x, y, v, angle, type, right) { let obj = Obj_Create(OBJ_SHOT); let dx; let dy = 0.2; if(right) { dx = 0.25; } else { dx = -0.25; } Obj_SetX(obj, x); Obj_SetY(obj, y); Obj_SetAngle(obj, angle); ObjShot_SetGraphic(obj, type); loop { if(Obj_BeDeleted(obj)) { break; } Obj_SetSpeed(obj, v); Obj_SetAlpha(obj, 255); Obj_SetCollisionToPlayer(obj, true); wait(幻視が始まるまで???); Obj_SetSpeed(obj, 0); Obj_SetAlpha(obj, 128); Obj_SetCollisionToPlayer(obj, false); loop(100) { Obj_SetX(obj, Obj_GetX(obj) + dx); Obj_SetY(obj, Obj_GetY(obj) + dy); yield; } } }
先ず最初に座標(Obj_SetX
, Obj_SetY
)と角度(Obj_SetAngle
)とグラフィック(ObjShot_SetGraphic
)を指定しています。
次に、ループ内で速度(Obj_SetSpeed
)とα値(Obj_SetAlpha
)と当たり判定(Obj_SetCollisionToPlayer
)を指定しています。これらの情報は幻視が発生すると変化するので、幻視が終了後に元に戻す必要があります。そのため、ループ内で設定するようにしてあります。
幻視状態に入ると、弾を半透明にし、当たり判定を消しています。そして、自前で位置を変化させるため速度を 0 にし、Obj_SetX
, Obj_SetY
を使って位置を変化させています。
また、弾が画面外に出て消えてしまってもずっと制御するのは無駄なので、弾が消えたら処理を終える必要があります。何の対処もしていなければ、画面内の弾の数は大したことがなくても、弾の延べ数が多くなると非常に処理が重くなってしまいます。そこで、弾が消えたかどうか判定する関数 Obj_BeDeleted
を使って、弾が消えていた場合(真)はループを終了するようにしています。
...というわけで、全体の流れは把握できたと思います。ただ、一箇所だけ説明しなかった部分があります。そこは、上のプログラムで強調してある wait(幻視が始まるまで???);
という部分です。これは幻視が始まるまで待ちたいということを主張しているのですが、もちろんこのままではエラーになります。
幻視から次の幻視までの間隔は 100 フレームなので、ここは普通に wait(100);
とすればいいようにも思えます。しかし、弾が撃たれた最初の1回だけは違うのです。幻視が終わった直後に撃たれる弾では 100 フレーム待てばいいのですが、次の弾では 75 フレーム、さらに次の弾では 50 フレーム待つ必要があります。
これにどう対応するかに関しては、いくつかの方法が考えられます。1つには、最初の1回の待ち時間を引数として渡す方法です。しかし、今回の場合は3種類だからそれでも簡単に実装できますが、弾がもっと色んなタイミングで発射される場合、待ち時間をいちいち計算するのは面倒な場合もあると思われます。
そこで、このような場合には次のようにすると簡単に制御することができます。先ず、ある変数を作っておきます。そして、幻視が始まる時にその変数の値を真に、終わるときに偽に変えてやります。すると、wait(幻視が始まるまで???);
の部分は、この変数が真になるまで待つようにすればいいのです。あと、幻視の間に弾をズラしている処理の部分も、この変数が真の間だけズラすような処理にすれば、100
という気持ち悪い数字を消すことができます(変数を使って phantasm
とこことに指定すればいいだけではありますが)。
// 幻視状態かどうか let onPhantasm = false; // 幻視 sub phantasm { onPhantasm = true; wait(100); onPhantasm = false; } // 幻視でブレる弾 // x, y : 発射地点 // v : 速度 // angle : 角度 // type : 弾のタイプ // right : 幻視で右に移動するかどうか task TMagicShot(x, y, v, angle, type, right) { let obj = Obj_Create(OBJ_SHOT); let dx; let dy = 0.2; if(right) { dx = 0.25; } else { dx = -0.25; } Obj_SetX(obj, x); Obj_SetY(obj, y); Obj_SetAngle(obj, angle); ObjShot_SetGraphic(obj, type); loop { if(Obj_BeDeleted(obj)) { break; } Obj_SetSpeed(obj, v); Obj_SetAlpha(obj, 255); Obj_SetCollisionToPlayer(obj, true); loop { if(onPhantasm) { break; } yield; } Obj_SetSpeed(obj, 0); Obj_SetAlpha(obj, 128); Obj_SetCollisionToPlayer(obj, false); loop { if(! onPhantasm) { break; } Obj_SetX(obj, Obj_GetX(obj) + dx); Obj_SetY(obj, Obj_GetY(obj) + dy); yield; } } }
あとは、この弾用タスク TMagicShot
を CreateShot01
の代わりに使えばOKです。以上の変更を加えたプログラムをここに置いておきます。見事に幻視が再現されていると思います。
Obj_Create
で作られた弾やレーザーのことをオブジェクトと言います。今回でとりあえずスペルは完成しましたが、次回もあとちょっとだけ手を加えてみたいと思います。