押してる間ずっとなのか、押して離した時なのか、それが問題だ（トリガーボタン編）

この記事は [Ebiten Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ebiten) 6日目の記事です。
引き続き`VirtualGamePad`の話をします。今回は`トリガーボタン`の話をします！ʕ◔ϖ◔ʔ

# `トリガーボタン`って？

[マリオブラザーズ](https://www.nintendo.co.jp/ngc/sms/history/sm1/)でファミコンの`Aボタン`押したらマリオがジャンプします。比較的新しいシューティングゲームでショットボタンを押しっぱなしにすると、ショットが連続的に出ます。

ゲームパッドの上記のようなボタンのことを、この記事では`トリガーボタン`と呼びたいと思います。
トリガーする条件が異なる以下の2パターンのトリガーボタンを試作しました。

- 押してる間ずっとトリガーしっぱなし
    - シューティングゲームのショットボタンとか、こんな感じのが多くなりましたよね？
- 押して離した時に1回トリガーする
    - ジャンプしたり技を使ったり、意思決定するボタンに多い動作だと思います。
    - 今回は自機のモード切り替えに使います。

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

# 準備

## まずはトリガーの仕方によらず使えるように`interface`

ゲームを制御する側からすれば、「トリガーボタン君、きみは今トリガーされてるのかね？」という問い合わせなどを`トリガーボタンの種類に依らず実施できる`とありがたいです。そこで、以下のような`interface`をまず定義しました。

**[TriggerButton](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/trigger_button.go#L22-L27)**

```golang:trigger_button.go
type TriggerButton interface {
	SetLocation(x, y int)
	Update()
	IsTriggered() bool
	Draw(*ebiten.Image) error
}
```

先に挙げた2種類のトリガーボタン、どちらもこの`TriggerButton`インターフェイスを満たすように実装します。

## トリガー条件でインスタンスを生成

トリガーボタンのインスタンスを作るときにトリガー条件を指定できるよう、`TriggerType`を定義します。

**[TriggerType](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/trigger_button.go#L10-L20)**

```golang:trigger_button.go
// TriggerType is the type for the TriggerButton.
type TriggerType int


const (
	// JustRelease is the button to be triggered when just released only.
	JustRelease TriggerType = iota
	// Pressing is the button to be triggered during pressed every frame
	Pressing
	// JustPressed is the button to be triggered when just pressed only.
	JustPressed
)
```

これを使って、以下のように生成します。(あ・・`JustPressed`結局作ってないや・・)

**[NewTriggerButton](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/trigger_button.go#L29-L50)**

```golang:trigger_button.go
// NewTriggerButton returns a new TriggerButton.
func NewTriggerButton(img *ebiten.Image, tt TriggerType) (TriggerButton, error) {
	sop := &ebiten.DrawImageOptions{}
	sop.ColorM.Scale(colorScale(color.RGBA{0, 148, 255, 255}))
	switch tt {
	case JustRelease:
		return &JustReleaseButton{
			baseImg:    img,
			normalOp:   &ebiten.DrawImageOptions{},
			selectedOp: sop,
			touches:    make(map[*touch]struct{}),
		}, nil
	case Pressing:
		return &PressingButton{
			baseImg:    img,
			normalOp:   &ebiten.DrawImageOptions{},
			selectedOp: sop,
		}, nil
	default:
		return nil, fmt.Errorf("unknown trigger type: %v", tt)
	}
}
```

# `押してる間ずっとトリガーしっぱなし`ボタン

[**PressingButton**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/pressing_button.go#L29-L65)

```golang:pressing_button.go
// Update updates the internal state of this button.
// Please call this before using IsTriggered method.
func (b *PressingButton) Update() {
	b.updateSelect()
	b.updateTrigger()
}

(中略)

func (b *PressingButton) updateTrigger() {
	b.isTriggered = false
	IDs := ebiten.TouchIDs()
	if len(IDs) == 0 {
		return
	}

	for i := range IDs {
		if isTouched(IDs[i], b.rectangle) {
			b.isTriggered = true
			return
		}
	}
}
```

ユーザーのタッチ位置がボタンの領域に含まれていれば、いつでも`isTriggered`です。至ってシンプルですね。

# `押して離した時に1回トリガーする`ボタン

## 考え方

`ボタンの上でタッチして、ボタンの上で指を離した場合`にトリガーするようにしたいと思います。
実現方法を以下のように考えました。

* `inpututil.JustPressedTouchIDs`で、ボタン上をタッチした時のタッチIDを保持
* タッチIDに対して、`inpututil.IsTouchJustReleased`で指を離したかチェック
    * さらに、離した位置がボタン上にあるかチェック

## タッチ情報

ゲーム更新のフレーム間でタッチ情報を保持する必要がありそうです。そこで、タッチ情報に`位置情報を更新する機能`や`指を離したか判断する機能`を付与して以下のような構造体`touch`を定義しました。

```golang:touch.go
type touch struct {
	id int
	x  int
	y  int
}

func (t *touch) Update() {
	if inpututil.IsTouchJustReleased(t.id) {
		return
	}
	t.x, t.y = ebiten.TouchPosition(t.id)
}

func (t *touch) IsReleased() bool {
	return inpututil.IsTouchJustReleased(t.id)
}
```

## 実現方法

この`touch`を保持するためのマップ`map[*touch]struct{}`を`JustReleaseButton`のフィールド`touches`として定義します。

これらの仕組みを使って実装してみたのが以下です。
[**JustReleaseButton**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/just_release_button.go#L31-L75)

```golang:just_release_button.go
// Update updates the internal state of this button.
// Please call this before using IsTriggered method.
func (b *JustReleaseButton) Update() {
	b.updateSelect()
	b.updateTrigger()
}

(中略)

func (b *JustReleaseButton) updateTrigger() {
	b.isTriggered = false

	IDs := inpututil.JustPressedTouchIDs()
	if len(IDs) != 0 {
		for _, id := range IDs {
			b.touches[&touch{id: id}] = struct{}{}
		}
	}

	for t := range b.touches {
		t.Update()
		if t.IsReleased() {
			delete(b.touches, t)
			in := image.Point{t.x, t.y}.In(b.rectangle)
			if in {
				b.isTriggered = true
				return
			}
		}
	}
}
```

# 補足

トリガーの判断に特化して話をするため上記では敢えて省略しましたが、ボタンを選択状態として表示するか否かのフラグ`isSelected`も毎フレーム更新しながら、それっぽくボタンが表示されるようにしています。

Webページでボタンにマウスオーバーした時やクリックした時に、ちょっと色味を変えて「ちゃんと反応してるで！」と主張してくれるような感じです。

# まとめ

この記事では`トリガーボタン`に特化してお話しました。
いやぁ、やっぱりボタン押してキャラクターがいろいろアクションすると楽しいですね！

[前回](https://qiita.com/KemoKemo/private/d3def4fd6abfbd82a172)と今回で`VirtualGamePad`を作ってみたお話は一旦終わりです。
次回は、今回ご説明した`押してる間ずっとトリガーしっぱなし`ボタンを使ってシューティングのショットを撃つ実装をした時に起きた問題点と解決したお話をしたいと思います。