ユーザーがボタン押しっぱなしの状況で、いかにそれっぽくショットを連射するか

この記事は [Ebiten Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ebiten) 7日目の記事です。
作戦は大体いつも「ガンガンいこうぜ」な @kemokemo ですʕ◔ϖ◔ʔ
前回、前々回と解説してきた`VirtualGamePad`を使ってシューティングのショットを撃とうとしたときの話をします。

# ボタンベタ押しで普通のショットを撃ちたい

![Screenshot_2018-12-02-17-57-37.jpeg](https://qiita-image-store.s3.amazonaws.com/0/166225/f0089ea9-35e8-1200-a396-d4ea0b76cb8c.jpeg)

ボタンがトリガーされている分だけショットを出し続けた結果がこれです。
シューティングゲームでたまに見かける、`短射程で高火力な火炎放射系のショット`を実現するには良さそうです。

今回は、自機のノーマルショットなどに多い`ボタンをベタ押ししている間ずっと適度な時間間隔で出るショット`を実現したいので工夫します。また、自機のモードを変えてショットの特性（画像、連射速度など）の切り替えもしたいので、その辺も工夫します。

では見ていきましょうʕ◔ϖ◔ʔ

# 準備

## 銃と弾にいろんな種類があってね

ショットを実現する仕組みとして、以下のように考えました。

* ショットを撃つ銃`Gun`構造体を作り、どんな時間間隔でどの方向にショットを出すか制御する
    * 後述の`Bullet`を複数生成しておいて管理する
* ショット自体の`Bullet`構造体を作り、発射された後の移動処理や自分自身の描画処理を行う

複数方向に撃つショットや連射速度重視のショットなどなど、いろんなバリエーションのショットを作る際に特に便利なのではないかと思います。今回の試作では`1-way`ショットを2種類作りました。

## さらに種類を増やしたいあなたに

さらに、今回はそこまで実施していませんが、`Gun`を`interface`にして以下の3つのメソッドを定義しておけば、どのバリエーションのショットもゲーム制御コードから同じように扱えて楽できそうです。

* Update
* Draw
* Fire

# 実装内容のご紹介

## ショットの時間間隔を調整する

### `Gun`の生成と`Bullet`の扱い

`弾をいい感じの間隔で発射する`、これは弾を発射する銃`Gun`の役割です。
弾の画像や発射時間間隔などを引数で渡しながら、`Gun`のインスタンスを作りましょう。

[**NewGun**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/gun/gun.go#L25-L34)

```golang:gun.go
// NewGun returns a new gun.
// Please sets the area of movement for bullets.
func NewGun(img *ebiten.Image, area image.Rectangle, interval time.Duration) (*Gun, error) {
	g := &Gun{interval: interval}
	err := g.createBullets(img, area)
	if err != nil {
		return nil, err
	}
	return g, nil
}
```

`Gun`は、自分が発射可能な弾`Bullet`のインスタンスを十分な数だけ生成して保持しておく仕組みを採用しました。ショットはゲーム中たくさん撃つものなので、インスタンスの生成と破棄をずっと繰り返す処理は避けようと考えたからです。

### `Gun`を使う

ゲーム制御コードからは、以下の`Update`と`Draw`メソッドを呼び出して、内部ステータスの更新や描画処理を実施します。

[**Update and Draw**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/gun/gun.go#L51-L68)

```golang:gun.go
// Update updates the bullets status.
func (g *Gun) Update() {
	for index := 0; index < bulletsCount; index++ {
		g.bullets[index].Update()
	}
}

// Draw draws bullets.
func (g *Gun) Draw(screen *ebiten.Image) error {
	var e error
	for index := 0; index < bulletsCount; index++ {
		e = g.bullets[index].Draw(screen)
		if e != nil {
			return e
		}
	}
	return nil
}
```

ユーザー入力などにより弾を発射する指示があれば、ゲーム制御コードから`Fire`メソッドを呼び出します。

[**Fire**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/gun/gun.go#L70-L89)

```golang:gun.go
// Fire fires a bullet.
// If the duration time is less than the interval from previous fire,
// this function do nothing.
func (g *Gun) Fire(point image.Point) {
	t := time.Now()
	if t.Sub(g.fired) < g.interval {
		return
	}
	g.fired = t

	if g.bulletsIndex < bulletsCount-1 {
		g.bulletsIndex++
	} else {
		g.bulletsIndex = 0
	}
	g.bullets[g.bulletsIndex].Fire(
		image.Point{
			point.X - g.bulletSize.X/2,
			point.Y - g.bulletSize.Y/2})
}
```

このメソッドのコメントにもあるように、`前回弾が発射されてから経過した時間`と予め設定された`発射時間間隔`とを比較して、前者の方が短ければ何もしない（=弾の追加発射しない）です。

## 発射されたら動くし描画する

次は弾`Bullet`を見ていきましょう。

### `Bullet`を使う

`Gun`経由で以下の`Update`や`Draw`メソッドが呼び出されます。

```golang:bullet.go
// Update update the internal state of this bullet.
func (b *Bullet) Update() {
	if !b.visible {
		return
	}
	b.move()
	b.checkArea()
}

// move moves this bullet.
func (b *Bullet) move() {
	b.point = b.point.Add(b.velocity)
	b.op.GeoM.Translate(float64(b.velocity.X), float64(b.velocity.Y))
}

// checkAreac checks whether this bullet is out of the
func (b *Bullet) checkArea() {
	if !b.point.In(b.area) {
		b.visible = false
	}
}

// Draw draws the image of this bullet.
func (b *Bullet) Draw(screen *ebiten.Image) error {
	if !b.visible {
		return nil
	}
	return screen.DrawImage(b.baseImg, b.op)
}
```

前述のように`Bullet`のインスタンスは使い回すことを考えていますので、`visible`フラグで以下のように制御します。

* 発射前または一旦使い終わった弾: `visible`が`false`で位置情報の更新も描画もしない
* 発射後の弾: `visible`が`true`で位置情報の更新も描画もする

`Gun`の`Fire`メソッドから、個々の弾である`Bullet`の`Fire`メソッドが呼び出されます。

```golang:bullet.go
// Fire sets the initial position and make this bullet
func (b *Bullet) Fire(point image.Point) {
	b.point = point
	b.visible = true

	b.op.GeoM.Reset()
	b.op.GeoM.Translate(float64(b.point.X), float64(b.point.Y))
}
```

引数で発射位置の情報をもらいます。この処理により`Bullet`の`visible`フラグが`true`になり、以降画面外に出るまで移動し描画されます。

# こんな感じになります

ここまで見てきた仕組みにより、連射速度も画像も異なるショットが実現できました。やったね！ʕ◔ϖ◔ʔ

![shot_type_01.jpeg](https://qiita-image-store.s3.amazonaws.com/0/166225/760923f8-3d70-a85d-a4c4-8f13b3802e7e.jpeg)

![shot_type_02.jpeg](https://qiita-image-store.s3.amazonaws.com/0/166225/d562a5d3-ca13-c5f9-8209-6474e53049e6.jpeg)

# まとめ

なんだかこの勢いでシューティングゲームを作り込みたくなりましたが、私には[「黒菜んダッシュ」](https://github.com/kemokemo/kuronan-dash)完成という使命があるので気持ちを切り替えて行きたいと思います。

そもそも`VirtualGamePad`も「黒菜んダッシュ」のための試作目的がメインだったのですが、思っていた以上に面白い題材でガッツリのめり込んでしまいました(；´∀｀)

皆様も、ぜひ [**ebiten**](https://github.com/hajimehoshi/ebiten) でゲーム作りしましょう！ʕ◔ϖ◔ʔ