Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事は Ebiten Advent Calendar 2018 6日目の記事です。
引き続きVirtualGamePadの話をします。今回はトリガーボタンの話をします!ʕ◔ϖ◔ʔ

トリガーボタンって?

マリオブラザーズでファミコンのAボタン押したらマリオがジャンプします。比較的新しいシューティングゲームでショットボタンを押しっぱなしにすると、ショットが連続的に出ます。

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

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

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

準備

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

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

TriggerButton

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

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

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

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

TriggerType

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

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

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を定義しました。

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

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

まとめ

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

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away