Posted at
Siv3DDay 22

Siv3DでSTG「ながれぼシュー」 敵の弾の作り方

More than 1 year has passed since last update.

Siv3D Advent Calendar 2016 22日目の記事です。


まえおき

今年自分は初めてシューティングゲームをつくりました。それが「ながれぼシュー」です。

事の発端は、9月にウディタ(Wolf RPG エディター)を使っているクリエイターのぴけさんが主催する、第6回ぴけコンに参加したことがキッカケでした。ぴけコンは、不定期に開催されるたった3時間でゲーム制作するイベントです。ネットワーク上で行われるもので、ウディタ使いでない人も参加できます。

お題が「流れ」だったので、流れ星が真っ先に浮かび流れ星のシューティング・・・という発想でできました。

3時間では自機と敵が弾を発射するところまでしかできなかった。

その後、2週間かけて完全な形に仕上げました。ステージを追加し、敵の攻撃パターンを35種(+10)、難易度は2段階なのでその2倍実装しました。弾の出現や移動処理についてはある程度使いまわしが効くように設計しました。

完成したものはここにあります。挑戦してみてね。

前置きが長くなりましたが、どんなふうに弾を出現させているかをこれから解説していきます。

今回は敵弾、いわゆる弾幕っぽいなにか、の作り方をメインに説明していきます。Siv3D製STGの作り方の基本(自機の実装やあたり判定など)は他のQiita記事等もあるので、一部省略します。


ほんだい・基本構造


クラス作成

弾はクラス分けでBulletクラスでも作っておきます。とりあえずこんな風に作っておきましょう。


たまクラス


class Bullet {
private:
int32 type; // たまの種類
Vec2 pos; // たまの位置
Circle hit; // あたり判定
Vec2 target; // 進行目標
Vec2 direction; // 弾の移動方向(移動スピード)
Color col; // たまの色(ここではColorの乗算を使用して色付けしている)
int32 clearFlag; // 弾が消滅するフラグ
int32 hitDamage; // 当たった時のダメージ量
int32 timer; // タイマー(Nフレーム後に移動方向を変える……など)
int32 optionInt; // 特殊な動作をするときのオプション

public:
Bullet(int32 t, Vec2 p, Vec2 tgt, double speed, double angle); // コンストラクタ

~Bullet() {}; // デストラクタ
int32 update(); // アップデート

void draw(); // 描画
};



たまの初期化

たまの初期化には引数を使って、パラメータを設定できるようにする。

ここでは、弾の種類、発生源の位置、目標方向、弾の速度、発射角度を設定できるようにしている。

弾の種類によって、初期化方法が異なるので、中はswitch分岐している。

このswitch分岐は弾が移動するupdate関数にも存在している。特定の種類において速度変化弾とかカーブ弾とかにしたいときに分岐させたり。色が変わるキラキラ弾もこの分岐で実装していたり。


たまのしょきか

Bullet::Bullet(int32 t, Vec2 p, Vec2 tgt, double speed = 4.0, double angle = 0.0)

{
optionInt = 0; // オプション設定
timer = -1; // -1の時はタイマーを使わない
target = tgt; // 目標位置
type = t; // 種類
pos = p; // 現在位置 = 発生源
hit = Circle(p, 6); // あたり判定の初期化
col = Color(255, 255, 0); // たまの色
clearFlag = 255; // 消去フラグ。255で存続、254以下で消滅開始(透明度が下がっていく演出)、0で完全消滅(オブジェクトの破棄)
hitDamage = 10; // 当たった時のダメージ

// 弾の種類に応じて、変更要素が変化
switch (type)
{
// (弾の移動方向と速度を、ベクトルで直接入力するもの)
case 0:
direction = { tgt };
break;
// (自機狙い弾用)
case 1:
direction = { (tgt - pos).normalized() * speed };
break;
// (でかくて赤い弾)
case 2:
col = Color(255, 64, 0);
hit.r = 12;
hitDamage = 20;
direction = { tgt };
break;
// (速度固定)
case 5:
direction = { tgt.normalized() * speed };
break;

// 以下省略

}
direction.rotate(angle); // さいごに、angleの角度分、移動方向を回転させる。
}



弾の発生

弾はたくさん出現します。いくつに出現するかは決まっていません。そのため、普通の要素数が固定の配列は使いません。

弾のオブジェクトは可変長配列(Siv3DのArray)の中に入れて、何個でも出せるようにしましょう。新たにたまを作るときはpush_backする。

置いテクの場合、フレームカウントを発射間隔で剰余(%演算子)計算をして、弾の発射タイミングを決定している。ifをさらに組み合わせることによって、マシンガンのように連射→しばらく待機というようなものも作れる。

画面外に出たり、自機にあたった弾はeraseFlagを変更し「消す」状態に変更して、毎フレーム呼び出される消去判定を使って消す。


たまのせいせい

// 30フレームごとに弾を発射する。enemyCountは、敵が行動を開始してからのフレーム数

if (enemyCount % 30 == 0)
{
enemyBullet.push_back(Bullet(0, enemyPos, { 0.0, 4.0 })); // ここで弾を発生させる。
// (実際には発射の効果音もここで鳴らす)
}


弾発生の基本構造はこんな感じ。つぎは、これを使って弾幕っぽいものを組んでみます。


ほんだい・攻撃パターンをつくる


自機狙い

自機狙い弾は、自分と相手の位置の差の方向に弾を出します。そのままだと1フレームで到達するスピードなので、ベクトルをノーマライズして、速度をかける。

ながれぼシューの設定では、普通の自機狙い弾の種類IDは「1番」に設定しています。

初期化の時に弾の発生源と目標位置の差を移動方向にするようになっています。


スタンダード1-4

if (enemyCount % 27 == 0)

{
enemyBullet.push_back(Bullet(1, enemyPos, playerPos, 4));
}

自機狙い弾の発生源と目標座標を平行にずらすことによって、Siv3D弾のようなかたまりを飛ばすこともできます。


スタンダード4-1

if (enemyCount % 20 == 0)

{
// デザイン重視のため、全弾手打ちです。
// ID6番が大きい弾、7番が小さい弾です
enemyBullet.push_back(Bullet(6, enemyPos + Vec2(-12, 10), playerPos + Vec2(-12, 10), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(-22, 8), playerPos + Vec2(-22, 8), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(-28, 5), playerPos + Vec2(-28, 5), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(-30, 0), playerPos + Vec2(-30, 0), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(-30, -7), playerPos + Vec2(-30, -5), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(-25, -15), playerPos + Vec2(-25, -15), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(-20, -20), playerPos + Vec2(-20, -20), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(-10, -25), playerPos + Vec2(-10, -25), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(0, -25), playerPos + Vec2(0, -25), 6));

enemyBullet.push_back(Bullet(6, enemyPos + Vec2(12, -10), playerPos + Vec2(12, -10), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(22, -8), playerPos + Vec2(22, -8), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(28, -5), playerPos + Vec2(28, -5), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(30, 0), playerPos + Vec2(30, 0), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(30, 7), playerPos + Vec2(30, 5), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(25, 15), playerPos + Vec2(25, 15), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(20, 20), playerPos + Vec2(20, 20), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(10, 25), playerPos + Vec2(10, 25), 6));
enemyBullet.push_back(Bullet(7, enemyPos + Vec2(0, 25), playerPos + Vec2(0, 25), 6));
}



自機外し

自機狙いの目標座標を左右にずらしたり、自機狙いの移動方向を回転させると、自機外しになります。プログラムでは、弾生成時に入れる「回転」引数を使う。ラジアン単位で回転することに注意。

自機外しの例は、ステージ7-2。弾がUターンして加速する弾や、逆方向に飛んでいく黄色い弾があるけど、あれ、全部自機外し(ネタバラシ)。画面下の方で待機してたら当たらないので、試してみるといい。


ハイパー7-2

// 赤い弾。Uターンする。

if (enemyCount % 32 <= 21)
{
enemyBullet.push_back(Bullet(10, enemyPos + Vec2(40, 0), playerPos + Vec2(-40, 0), 4.0));
enemyBullet.push_back(Bullet(10, enemyPos + Vec2(-40, 0), playerPos + Vec2(40, 0), 4.0));
}
// 黄色い弾。自機の逆方向に飛んでいく
if (enemyCount % 8 == 5)
{
enemyBullet.push_back(Bullet(1, enemyPos + Vec2(40, 0), playerPos + Vec2(-40, 0), 4.0, 0.75 * Pi));
enemyBullet.push_back(Bullet(1, enemyPos + Vec2(-40, 0), playerPos + Vec2(40, 0), 4.0, -0.75 * Pi));
enemyBullet.push_back(Bullet(1, enemyPos + Vec2(40, 0), playerPos + Vec2(-40, 0), 4.0, 0.5 * Pi));
enemyBullet.push_back(Bullet(1, enemyPos + Vec2(-40, 0), playerPos + Vec2(40, 0), 4.0, -0.5 * Pi));
enemyBullet.push_back(Bullet(1, enemyPos, playerPos, 4.0, Pi));
}


Uターン弾

上の自機外しにも登場した赤い弾。

実装の仕方は、毎フレームごとに行われる弾の移動、その速度をいじるだけ。速度変化の特別な処理は、弾のupdate関数にかいている。

自機狙いの場合、最初はマイナス1.0倍、次は-0.966倍、-0.933倍というように速度の倍率を変化させる。(変化量はお好みに調整)

やがて速度の符号が逆転し、自機いるの方向に向かってくるようになる。等速の弾とは違う軌道を描くのだ。弾の経過時間については、Uターン弾を生成するときにタイマー(値:1000)が設定され、そのタイマーが1フレームごとに1ずつ減少することを利用して計算できる。


速度変化

int32 update() 

{
// 弾の種類別に行われる、特殊な処理をここにかく
switch (type)
{
case 10:
pos.moveBy(direction * (1000 - timer) / 30.0);
break;
// ほかの種類の処理は省略

// 特別な動きのない通常の弾の場合
default:
pos.moveBy(direction);
break;
}
if (timer > 0) --timer;
if (timer == 0)
{
timer = -1;
return 1;
}
return 0;
}



移動と連動した攻撃

ながれぼシューの場合、敵の多くの攻撃は共通して「左右にゆらゆら揺れる」動きをするが、それをも変化させるとができる。

特定のステージの場合のみ、特殊な動きをする攻撃も用意できるのだ。


敵の移動

void MainGame::enemyMove() {

switch(stage)
{
// ステージ5-3(22)のみの特殊パターン 画面全体に円を描くように移動し、その後画面上部をうろうろする。その繰り返し。
case 22:
if (enemyCount % 300 < 25 || enemyCount % 300 >= 250) enemyPos2 = { 400, 50 };
else if (enemyCount % 300 < 125) {
enemyPos2.x = Sin(((enemyCount % 300) - 25) / 45.0 * Pi) * 400 + 400;
enemyPos2.y = -Cos(((enemyCount % 300) - 25) / 45.0 * Pi) * 300 + 300;
}
else {
enemyPos2 = { Sin(((enemyCount % 300) - 115) / 45.0 * Pi) * 150 + 400, Sin(((enemyCount % 300) - 115) / 22.5 * Pi) * 10 + 100 };
}
break;
// 通常の動き
default:
enemyPos2.x = -Sin(enemyCount / 120.0 * Pi) * 200 + 400;
enemyPos2.y = Sin((enemyCount) / 50.0 * Pi) * 30 + 150;
break;
}
}

ステージ5-3は停止弾を円状にばらまき、その停止弾が時間経過で爆発し小さい弾となってランダム方向にばらまかれるというもの。


おわりに

弾幕っぽいものを作るのって、やっぱり大変。 ながれぼシューのためにたっくさん種類を作ったけど、1つ1つ作るのに何度も試行錯誤したし、頭使う作業だった。

こんな記事書気ながら言うのは難だけど、発想力が自分には足りなかった。

あと、もっとSiv3Dくんはやらせようぜ!

ということで、おわりです。

明日は @Pctg-x8 さんです。よろしくおねがいします。