【GameMaker:Studio2】ホーミングミサイルの作り方 (GML)

# はじめに
今回はアクションゲームやシューティングゲームなどでよく使う、ホーミングミサイルの実装方法を解説します。

# 使用する関数
ホーミングミサイルを実装するにあたって、以下の関数を使うと簡単に実装できます。

* instance_nearest(): 一番近いオブジェクトを取得する →一番近い敵を追尾するため
* angle_difference(): 2つの角度の差を求める →ホーミングは旋回軌道により追尾を行う
* point_direction(): 2点間のなす角度を求める →目標に命中する角度を求める
* point_distance(): 2点間のなす距離を求める →一定距離内に達したら追尾を開始する

# 使用する素材
使用する素材はこちらにアップロードしているので、あらかじめダウンロードしておきます。
http://www.syun777.sakura.ne.jp/tmp/gms2/gms2_horming.zip

# プロジェクトの作成
今回は GML を使用するので、`GameMakerLanguage` のプロジェクトを作成します
<img width="415" alt="001.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/e4e563ea-7a23-e871-a1df-0c6d81229c9f.png">

# プロジェクトの設定
歯車アイコンをクリックして、`Game Options` を開きます。
`Game frames per second` の値を `60` にして、フレームレートを 60 にします。
<img width="490" alt="002.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/e30d4123-c3c8-9b7d-8aff-557ed5759d0e.png">

# プレイヤーの作成
オブジェクトを作成し、名前を `objPlayer` とし、スプライト作成します。
<img width="221" alt="003.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/c1199ed8-d8aa-a565-fcd9-7f42b4c0fd26.png">

スプライトの名前を `sprPlayer` とします。
`Import` ボタンを押して、素材画像 `player_strip2.png` をインポートします。
<img width="421" alt="004.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/71b63145-f49e-8ceb-04d8-7737fa8c8232.png">

アニメーションフレーム( `Frames Per Second` )を `3` とします。
<img width="379" alt="005.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/e60bed4a-6e56-a938-e955-825861929a16.png">

今回はスプライトの原点が中央であると都合が良いので、原点を `Middle Centre` に変更します。
<img width="553" alt="007.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/eab7c7b0-0cbf-a7db-d46b-81205766d76c.png">

# プレイヤーの配置
`objPlayer` をルームに配置します。 `room0` を開きます。
`Room Editor > Properties > Room Settings` から `Width` を `800` 、`Height` を `480` に変更します。
<img width="256" alt="006.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/6af34338-afc7-20b1-56cf-1443851c4316.png">

ルームサイズを変更したら、`objPlayer` をルームの左側に配置します。
<img width="581" alt="008.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/54fd6395-bcda-283d-fa75-666a14c2fb65.png">

実行して、objPlayerが表示されるのを確認します。

# プレイヤーの移動処理
`objPlayer` を開いて、移動処理を作成します。
`Add Event > Step > Step` でStepイベントを作成します。
<img width="511" alt="009.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/6d2f277a-7c28-f1c6-86cc-a7b7dcaf7cda.png">

コードは以下のように記述します。

```js:objPlayer>Stepイベント
// マウス座標との差を求める
var dy = mouse_y - y;

// 差の10分の1を足しこむ.
y += dy * 0.1;
```

このコードは現在の座標とマウス座標との差を求め、その差の10分の1をじわじわ足しこむ処理となります。
実行してみて動作を確認します。マウスの位置にプレイヤーが追従する動きをするようになりました。

ただ、プレイヤーが画面外に出て行ってしまうので、それを防ぐコードを追加します。

```js:objPlayer>Stepイベント
// マウス座標との差を求める
var dy = mouse_y - y;

// 差の10分の1を足しこむ.
y += dy * 0.1;

// 画面外に出ないようにする
y = median(32, y, room_height-32);
```

median関数を使うことで、yの値が 32〜room_height-32 の範囲に収まるようにしました。
実行して、プレイヤーが画面外に出ないことを確認します。

# ミサイルの実装
ホーミングミサイルを実装します。
オブジェクトを作成して、名前を `objShot` とします。
<img width="220" alt="010.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/15c638e9-d98b-b57c-6e85-cdb38e2f361d.png">

スプライトを作成し、名前を `sprShot` にして、`Import` ボタンを押して、人参の画像 `carrot.png` をインポートします。
<img width="407" alt="011.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/1dfe8d02-6014-74f3-34a7-854f9e6f1e05.png">

人参のスプライトの原点を中央にするため、`Middle Centre` に変更します
<img width="461" alt="012.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/26d18367-b388-0cf0-0267-b3e9608edeea.png">

`objShot` のイベントを追加します。
`Add Event > Other > Outside Room` を選択して、`Outside Room` イベントを追加します。
<img width="516" alt="013.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/6ec8429f-98fe-5792-73a4-ad2b5c2ef3be.png">

コードは以下のように記述します。

```js:objShot>OutsideRoomイベント
instance_destroy();
```

ルーム外に出たら破棄処理を行います。

さらに、`Add Event > Destroy` で破棄イベントを追加します。
<img width="526" alt="014.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/2e085cda-0705-69b2-c7cd-545a6fb7dffe.png">

コードは以下のように記述して、破棄されたらエフェクトを発生させるようにします。

```js:objShot>Destroyイベント
effect_create_above(ef_explosion, x, y, 1, c_white);
```

# プレイヤーからミサイルを発射する
`objPlayer` を開いて、Stepイベントのコードを以下のように修正します。

```js:objPlayer>Stepイベント
// マウス座標との差を求める
var dy = mouse_y - y;

// 差の10分の1を足しこむ.
y += dy * 0.1;

// 画面外に出ないようにする
y = median(32, y, room_height-32);

// ※※※※※※※※※※※
// ここから追加
// ※※※※※※※※※※※
if(mouse_check_button_pressed(mb_left)) {
	// 左クリックでミサイルを発射
	var inst = instance_create_depth(x, y, 0, objShot);
	with(inst) {
		// 0度方向に5の速度で発射
		motion_add(0, 5);
	}
}
```

マウスの左クリックで右側に向かって発射するようにしました。
では、実行して人参ミサイルが発射できることを確認します。
<img width="553" alt="015.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/edf529ea-5165-d573-0875-18f18ab0f0e8.png">

# 敵オブジェクトを作成
新しくオブジェクトを追加して、名前を `objEnemy` とします。
<img width="200" alt="016.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/53c9e743-d3e0-c06d-f506-2e9becc12bc1.png">

スプライトを追加して、名前を `sprEnemy` とし、`Import` ボタンを押して、素材画像 `nasu.png` をインポートします。
<img width="407" alt="017.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/e23db310-c6dc-18cc-1106-32e387194876.png">

そして、原点を中央 (MiddleCentre) に変更しておきます。
<img width="465" alt="018.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/871f6259-c47e-5f1b-28d2-2a57d9d70b5c.png">

`objEnemy` のイベントを追加します。
`Add Event > Step > Step` を選び、Stepイベントを追加します。
<img width="497" alt="019.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/d3e7f03b-aa51-8e79-103f-6638940a7d9f.png">

Stepイベントには以下のように記述します。

```js:objEnemy>Stepイベント
// 下に動かす
vspeed = 2;
// 画面外に出たら反対側から出現させる
move_wrap(true, true, 0);
```

ホーミングで命中させる対象を動かすだけなので、下に動かして画面外に出たら反対側に移動するだけのコードです。

では敵を配置します。room0を開いて、`objEnemy` を配置します。
<img width="563" alt="020.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/f5cd5c8b-66a2-d360-f57c-32bb9d6ee2fa.png">

それぞれの敵が、おおよそ均等に配置されていれば問題ありません。
では実行して、敵が上から下に移動するのを確認します。

# ミサイルと敵との当たり判定を実装
`objEnemy` を開いて `Add Event > Collision > objShot` を作成し、人参ミサイルとの衝突イベントを作成します。
<img width="517" alt="021.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/56875a6d-2c95-686c-65ce-431f2bfb6011.png">

コリジョンイベントのコードは以下のように記述します。

```js:objEnemy>Collisionイベント
with(other) {
	// ミサイルを消す
	instance_destroy();
}
```

衝突したミサイルを消すだけのコードです。
では実行して、敵にミサイルを当てると消滅することを確認します。
<img width="392" alt="022.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/4d81aa5f-b529-a1ec-4259-2cebcea3ea70.png">

# ホーミングの軌道を作成する
ようやくホーミングの軌道の実装です。
`objShot` を開いて、`Add Event > Step > Step` でStepイベントを作成します。
<img width="568" alt="025.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/75791b27-ad7d-3bf3-b360-f1be3811bbd3.png">

```js:objShot>Stepイベント
// ①一番近い敵を探す
var inst = instance_nearest(x, y, objEnemy);
if(inst == noone) {
	// 見つからなかったので何もしない
	return;
}

// ②目標に対する角度を計算する
var to_dir = point_direction(x, y, inst.x, inst.y);

// ③現在の角度と目標への角度の差を求める
var diff = angle_difference(image_angle, to_dir);
if(diff > 0) {
	// ④-a. プラスは時計回り
	image_angle -= 1;
}
else {
	// ④-b. マイナスは反時計回り
	image_angle += 1;
}

// ⑤回転値を移動角度にも反映する
direction = image_angle;
```

コードを詳しく解説すると、①のところで一番近い敵を探し、
②でそれに対する角度を計算します。
<img width="419" alt="023.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/aa37b15d-025a-1a4c-c1d8-dc9ba74e9ee7.png">

③で現在向いている角度から目標への角度差を求めます。
<img width="342" alt="024.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/2c201c01-12b8-d581-4f08-0c93bf672c3a.png">

そして、計算した角度差を元に旋回処理を行っているのが④と⑤となります。
`angle_difference()` で得られる数値の符合が逆のような気がしますが、そういうものとして反転して扱います。

では実行してみると、発射したミサイルが敵に追尾します。
<img width="336" alt="026.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/60d89881-c61c-e1f1-89fd-faeda2352b9c.png">

# 一定の範囲内に入ったら追尾するようにする
`objShot` を開いて、Stepイベントを修正します。

```js:objShot>Stepイベント
// ①一番近い敵を探す
var inst = instance_nearest(x, y, objEnemy);
if(inst == noone) {
	// 見つからなかったので何もしない
	return;
}

// ※ここを追加
var to_dist = point_distance(x, y, inst.x, inst.y);
if(to_dist > 100) {
	// 100px以内に入らないと追尾しない
	return;
}
// ※ここまで追加

// ②目標に対する角度を計算する
var to_dir = point_direction(x, y, inst.x, inst.y);

// ③現在の角度と目標への角度の差を求める
var diff = angle_difference(image_angle, to_dir);
if(diff > 0) {
	// ④-a. プラスは時計回り
	image_angle -= 3; // ※旋回速度を修正
}
else {
	// ④-b. マイナスは反時計回り
	image_angle += 3; // ※旋回速度を修正
}

// ⑤回転値を移動角度にも反映する
direction = image_angle;
```

※の部分が修正箇所です。
②の角度計算の前に距離を計算し、半径100px以内に入らないと追尾しないようにしています。
また、④の旋回速度の部分を少し大きくしています。

実行すると、基本的にまっすぐ飛びますが、敵の近くに行くと旋回して追尾するようになります。
距離制限のホーミングは、あまり使い道はないように思えるかもしれませんが、敵が発射するホーミングミサイルでこのような追尾条件を入れておくと、回避しやすくなって面白さが増す可能性があります。

あと、追尾範囲が見た目でわかりにくいので追尾する範囲を描画するようにしてみます。
`objShot` を開いて、 `Add Event > Draw > Draw GUI` イベントを追加し、以下のコードを追加します。

```js:objShot>DrawGUIイベント
draw_circle(x, y, 100-32, true);
```

追尾範囲は半径100pxなのですが、ナスの半径が32pxなので、100-32となっています。

では実行して追尾範囲が表示されるのを確認します。
<img width="537" alt="027.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/3c13c9af-b8a5-1cf3-d7e7-8be284e68cb1.png">

# 最後に
ここで作ったホーミングは、一番近い敵を追尾するようにしていますが、例えばロックオンした敵のみを追尾するなど、ゲームシステムによって柔軟に作り変えても良いと思います。

以上、ホーミングの作り方でした。