この記事は Ebiten Advent Calendar 2018 6日目の記事です。
引き続きVirtualGamePad
の話をします。今回はトリガーボタン
の話をします!ʕ◔ϖ◔ʔ
トリガーボタン
って?
マリオブラザーズでファミコンのAボタン
押したらマリオがジャンプします。比較的新しいシューティングゲームでショットボタンを押しっぱなしにすると、ショットが連続的に出ます。
ゲームパッドの上記のようなボタンのことを、この記事ではトリガーボタン
と呼びたいと思います。
トリガーする条件が異なる以下の2パターンのトリガーボタンを試作しました。
- 押してる間ずっとトリガーしっぱなし
- シューティングゲームのショットボタンとか、こんな感じのが多くなりましたよね?
- 押して離した時に1回トリガーする
- ジャンプしたり技を使ったり、意思決定するボタンに多い動作だと思います。
- 今回は自機のモード切り替えに使います。
では見ていきましょうʕ◔ϖ◔ʔ
準備
まずはトリガーの仕方によらず使えるようにinterface
ゲームを制御する側からすれば、「トリガーボタン君、きみは今トリガーされてるのかね?」という問い合わせなどをトリガーボタンの種類に依らず実施できる
とありがたいです。そこで、以下のようなinterface
をまず定義しました。
type TriggerButton interface {
SetLocation(x, y int)
Update()
IsTriggered() bool
Draw(*ebiten.Image) error
}
先に挙げた2種類のトリガーボタン、どちらもこのTriggerButton
インターフェイスを満たすように実装します。
トリガー条件でインスタンスを生成
トリガーボタンのインスタンスを作るときにトリガー条件を指定できるよう、TriggerType
を定義します。
// 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 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)
}
}
押してる間ずっとトリガーしっぱなし
ボタン
// 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
を定義しました。
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
// 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
を作ってみたお話は一旦終わりです。
次回は、今回ご説明した押してる間ずっとトリガーしっぱなし
ボタンを使ってシューティングのショットを撃つ実装をした時に起きた問題点と解決したお話をしたいと思います。