スマホでもゲームパッド感覚で遊びたい？よろしい、ならばVirtualGamePadだ！（十字キー編）

この記事は [Ebiten Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ebiten) ５日目の記事です。
Advent Calendar初参加でドッキドキですが、張り切っていきたいと思います！！ʕ◔ϖ◔ʔ

# ゲームパッドが好き

こんにちは！ファミコン世代の@kemokemoと申します！
最近息子たちもゲームパッドに慣れてきて、我が家の「[ニンテンドークラシックミニ](https://www.nintendo.co.jp/clv/index.html)」と「[ニンテンドークラシックミニ スーパーファミコン](https://www.nintendo.co.jp/clvs/index.html)」の稼働率はかなり高めです。

突然ですが、私、ゲームパッドの操作性でゲームをするのが好きなんです。
スマホやタブレット用のゲームでも、ゲームパッドっぽいのが画面上に表示されて`方向キー`や`トリガーボタン`（押したら技とか出るやつ）をグリグリ、バシバシ操作して遊ぶようなものが結構ありますよね？ああいう感じです。

# この記事について

上記で挙げたような`画面上に表示された擬似的なゲームパッド`のことを、自分は便宜上`VirtualGamePad`と呼んでおりまして、少し前に[*ebiten*](https://github.com/hajimehoshi/ebiten)を使って実装してみました。

> https://github.com/kemokemo/ebiten-sketchbook/tree/master/virtual-gamepad

この記事では特に`十字キー`にスポットを当ててお話したいと思います。
最後までお付き合いいただけますと幸いです！ʕ◔ϖ◔ʔ

# 前説

「Go言語も`ebiten`ライブラリも知ってるよ！」という方は読み飛ばして[実現イメージの確認](#実現イメージの確認)へと進んでください。

## Go言語はゲームだって作れます

日々、多くのオープンソースや企業のプロダクトで採用されている`Go言語`、その魅力については敢えて語るまでもないかと思います。簡潔に分かりやすく書けてパフォーマンスも最高、そんな`Go言語`でゲームだって作れます。そう、@hajimehoshi さんが作っておられる [*ebiten*](https://github.com/hajimehoshi/ebiten)ライブラリを使えばね :smile:

## 準備

この記事では、`ebiten`を使ったゲーム制作の始め方的な内容は割愛いたします。
その代わりと言ってはなんですが、参考情報を以下に書きます。

### 開発環境

開発環境の整え方について[公式WikiのPlatforms](https://github.com/hajimehoshi/ebiten/wiki#platforms)にプラットフォーム毎の内容がありますので、そちらをご覧になってください。Windows、Ubuntu Desktop、macOSなど、どこでもサクッと始められます。実際にやってみたことある私が保証しますʕ◔ϖ◔ʔ

### チュートリアル

「`ebiten`に興味出てきた、簡単な例を実際に動かしてみたい」という方には[公式WikiのTutorials](https://github.com/hajimehoshi/ebiten/wiki#tutorials)がオススメです。「チュートリアルも終わってもっといろいろ動かしてみたいぞ」という場合には[公式リポジトリのexamplesフォルダ](https://github.com/hajimehoshi/ebiten/tree/master/examples)にあるたくさんのサンプルが最高の教材になると思います。

# 実現イメージの確認

改めまして、今回ご紹介する`VirtualGamePad`を使ったサンプルの画面は以下です。
動かしてみた動画はYouTubeにアップしたので [こちら](https://www.youtube.com/watch?v=Be1VR2TgNUU) からご覧いただけます！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/166225/20773a41-9e42-02f3-554a-76cf00f7252d.png)
主な特徴は以下のような感じです。

* 画面上の`十字キー`を指でグリグリ押したら、キャラクターがグリグリ動く
* `Aボタン`を指で押して離したら、キャラクターがモードチェンジ（色が変わって、ショットの種類も変わる）する
* `Bボタン`を指で押している間、ショットが出続ける

こうやって改めて見ると、そこはかとなく[*ゲームウォッチ*](https://www.google.com/search?q=%E3%82%B2%E3%83%BC%E3%83%A0%E3%82%A6%E3%82%A9%E3%83%83%E3%83%81&client=firefox-b-ab&source=lnms&tbm=isch&sa=X&ved=0ahUKEwi94u7T-LfeAhUMerwKHRifBhkQ_AUIEigB&biw=1422&bih=873)っぽいですね(；´∀｀)

# `十字キー`の実現方法について説明しますよ！

<img width="240" alt="directional_pad.png" src="https://qiita-image-store.s3.amazonaws.com/0/166225/1a57b770-1b0f-ecdc-f710-a447f44bb3f4.png">

このように画面上に十字キーを表示して **単純にタッチ位置と方向キー画像の当たり判定だけ** を行うと、以下のような問題が起きました。

* **上と下同時押し** といった、現実世界のゲームパッドでは発生しないような入力ができてしまう
    * ユーザーの入力方向をどっちやと判定すればよいのか・・
* 押してるつもりなんだけど反応しない、反応がシビアすぎ

改めて考えてみると、ファミコンのゲームパッドとかって上下や左右は同時に押せないようになってるんですよね。以下は[Wikipediaの十字キーの記事](https://ja.wikipedia.org/wiki/%E5%8D%81%E5%AD%97%E3%82%AD%E3%83%BC)からの引用です。

> 十字の中心には支点があり、カバーがシーソーのように動くことで、上と下または左と右のスイッチが同時に押せないようになっている。十字キーを斜めに押すことで上と右、下と左のように隣り合う2つのスイッチを同時に押せるようになっており、これにより8方向の入力が可能になっている。

また、現実の十字キーは方向キーの真ん中に指が当たっていなくても **方向キーの端っこに指がかかっているだけで入力状態になります** 。上記の説明にあるような機構のため、方向キーの端っこを押しただけでもキーパッド全体が傾いて入力として受け付けてくれるからです。

これらの特徴を実現することで、より現実のゲームパッドに近い操作感が得られそうです。

## 考え方と作り方

上記に挙げたような現実のゲームパッドの機構を考慮しながら、よりリアリティが感じられる`VirtualGamePad`の作成に取り掛かりましょう。

まず上記で挙げた問題点を解決すべく、以下のような対策案を考えました。

* ユーザー入力と複数の方向キーの当たり判定結果から、総合的に「入力方向」を確定する仕組みを作る
* 動かせるモックをいじりながら、タッチ入力に対する方向キーの当たり判定範囲をいい感じに広げる

この考えを実現しつつ、以下のような作り方を実践しました。

* 上下左右に4つ配置した方向キーを管理する`DirectionalPad`を作る
* 外部からもらった座標を元に、ゲームパッドをいい感じに配置する
* `ebiten`から毎フレーム呼ばれる`Update`メソッドで以下の処理を行う
    * ユーザーのタッチ位置から各方向キーの押下状態を更新する
        * 各方向キーの押下状態から総合的に判断してユーザー入力方向を判定する
    * 押される方向キー`directionalButton`に「君、押されてるで」と教える
* 方向キーの当たり判定範囲をいい感じに調整する

## 上下左右に4つ配置した方向キーを管理する`DirectionalPad`を作る

[**NewDirectionalPad**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/directional_pad.go#L18-L46)

```golang:directional_pad.go
// NewDirectionalPad returns a new DirectionalPad.
func NewDirectionalPad(pad, button *ebiten.Image) (*DirectionalPad, error) {
	dp := &DirectionalPad{
		baseImg: pad,
		op:      &ebiten.DrawImageOptions{},
	}

	err := dp.createButtons(button)
	if err != nil {
		return nil, err
	}
	return dp, nil
}

func (dp *DirectionalPad) createButtons(img *ebiten.Image) error {
	if dp.buttons == nil {
		dp.buttons = make(map[Direction]*directionalButton, 4)
	}

	ds := []Direction{Left, Upper, Right, Lower}
	for _, direc := range ds {
		b, err := newDirectionalButton(img, direc)
		if err != nil {
			return err
		}
		dp.buttons[direc] = b
	}

	return nil
}
```

引数でゲームパッドの土台とする画像`pad`と各方向キーにしたい画像`button`を渡すと、いい感じの`*DirectionalPad`を返すメソッドです。土台にする画像サイズと方向キー画像サイズをあらかじめ調整しておけば、下図のようにゲームパッドの見た目を調整できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/166225/7259dc20-4a89-b9a8-1b7e-dca71644d44f.png)

## 外部からもらった座標を元に、ゲームパッドをいい感じに配置する

[**SetLocation**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/directional_pad.go#L49-L63)

```golang:directional_pad.go
// SetLocation sets the location to draw this directional pad.
func (dp *DirectionalPad) SetLocation(x, y int) {
	dp.op.GeoM.Translate(float64(x), float64(y))

	wp, _ := dp.baseImg.Size()
	halfWp := int(wp / 2)
	wb, _ := dp.buttons[Left].Size()
	halfWb := int(wb / 2)
	outerMargin := int(0.25 * float64(halfWp))
	innerMargin := int(0.08 * float64(halfWp))

	dp.buttons[Left].SetLocation(x+outerMargin, y+halfWp-halfWb)
	dp.buttons[Upper].SetLocation(x+halfWp-halfWb, y+outerMargin)
	dp.buttons[Right].SetLocation(x+halfWp+innerMargin, y+halfWp-halfWb)
	dp.buttons[Lower].SetLocation(x+halfWp-halfWb, y+halfWp+innerMargin)
}
```

マージンを設定しながら四つ葉のクローバーのように方向キーを配置します。

## `ebiten`から毎フレーム呼ばれる`Update`メソッドで内部ステータスの更新をする

[**Update**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/directional_pad.go#L66-L71)

```golang:directional_pad.go
// Update updates the internal status of this struct.
func (dp *DirectionalPad) Update() error {
	dp.updateDirection()
	dp.updateButtons()

	return nil
}
```

`updateDirection`でユーザーが押した方向を判定して、`updateButtons`で方向キーにその情報を伝えてます。順に見ていきます。

### ユーザーのタッチ位置から各方向キーの押下状態を更新する

[**updateDirection**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/directional_pad.go#L73-L95)

`updateDirection`メソッドでの処理を見てみましょう。

```golang:directional_pad.go
func (dp *DirectionalPad) updateDirection() {
	IDs := ebiten.TouchIDs()

	// There are no touches
	if len(IDs) == 0 {
		dp.selectedDirection = None
		return
	}
...
```

まず`ebiten.TouchIDs()`でユーザーのタッチ情報を得るための`ID`リストを取得します。ユーザーのタッチがない場合には`nil`が返ってきますので、その場合には`len(IDs)`が0になって「ユーザーはタップしてない、選択してる方向は無し」として処理を終えます。（なんで `IDs == nil` で判定しなかったんや俺・・(´・ω・｀)

```golang:directional_pad.go
func (dp *DirectionalPad) updateDirection() {
...
	directions := []Direction{}
	// Find the newly touched direction key.
	sort.Slice(IDs, func(i, j int) bool {
		return IDs[i] < IDs[j]
	})
	for index := range IDs {
		for key := range dp.buttons {
			if isTouched(IDs[index], dp.buttons[key].GetRectangle()) {
				directions = append(directions, key)
			}
		}
	}
	dp.defineDirection(directions)
}
```

`sort.Slice`を使ってタッチの`IDs`を小さい数字順に並べて「先にタッチされたものから順に評価」できるようにしています。`isTouched`でタッチした座標が各ボタンの表示領域に含まれているかを評価して、含まれていればその方向を「ユーザーが今のフレームで押している方向」として`directions`配列に追加しています。

この`directions`を後述の`defineDirection`に渡して一意の方向にします。

#### 各方向キーの押下状態から総合的に判断してユーザー入力方向を判定する

[**defineDirection**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/directional_pad.go#L97-L103)

```golang:directional_pad.go
func (dp *DirectionalPad) defineDirection(directions []Direction) {
	current := None
	for index := range directions {
		current = getMergedDirection(current, directions[index])
	}
	dp.selectedDirection = current
}
```

`directions`にはより優先度が高いユーザー入力から順に保存されているので、これを順番に取り出して評価していきます。

[**getMergedDirection**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/direction.go#L40-L77)

```golang:direction.go
func getMergedDirection(previous, current Direction) Direction {
	switch previous {
	case Left:
		if current == Upper {
			return UpperLeft
		} else if current == Lower {
			return LowerLeft
		} else {
			return previous
		}
	case Upper:
		if current == Left {
			return UpperLeft
		} else if current == Right {
			return UpperRight
		} else {
			return previous
		}
	case Right:
		if current == Upper {
			return UpperRight
		} else if current == Lower {
			return LowerRight
		} else {
			return previous
		}
	case Lower:
		if current == Right {
			return LowerRight
		} else if current == Left {
			return LowerLeft
		} else {
			return previous
		}
	default:
		return current
	}
}
```

評価方法は至って簡単、以下のとおりです。案外これでいい感じになります。

* より優先度の高い入力`previous`に入ってる
* 現在評価対象となっている方向`current`が、`previous`に隣接するなら合成する
    * `previous`が**Lower**で`current`が**Right**なら、**LowerRight**を返す
* `previous`が合成済みの**UpperLeft**などであれば何もしない

### 押される方向キー`directionalButton`に「君、押されてるで」と教える

[**updateButtons**](https://github.com/kemokemo/ebiten-sketchbook/blob/bbe37fee24a98014fa6e4739b992fa99b824f42c/virtual-gamepad/internal/pad/directional_pad.go#L105-L128)

```golang:directional_pad.go
func (dp *DirectionalPad) updateButtons() {
	for key := range dp.buttons {
		dp.buttons[key].SelectButton(false)
	}
	if dp.selectedDirection == None {
		return
	}

	if dp.selectedDirection == UpperLeft {
		dp.buttons[Upper].SelectButton(true)
		dp.buttons[Left].SelectButton(true)
	} else if dp.selectedDirection == UpperRight {
		dp.buttons[Upper].SelectButton(true)
		dp.buttons[Right].SelectButton(true)
	} else if dp.selectedDirection == LowerLeft {
		dp.buttons[Lower].SelectButton(true)
		dp.buttons[Left].SelectButton(true)
	} else if dp.selectedDirection == LowerRight {
		dp.buttons[Lower].SelectButton(true)
		dp.buttons[Right].SelectButton(true)
	} else {
		dp.buttons[dp.selectedDirection].SelectButton(true)
	}
}
```

決定したユーザー入力方向`selectedDirection`に従って、各方向キーである`directionalButton`の選択状態を更新しています。

「君、押されてるで」と教えたら色を変えて描画する仕組みを`directionalButton`にいれてありますので、ユーザーの入力方向としてゲーム側が認識した方向キーのみ色を変えて「入力受け取ったで！」とアピールできます。ユーザビリティ的にも「押してる感」が出て良い感じです :smiley: 

## 方向キーの当たり判定範囲をいい感じに調整する

さて、ここまでの実装でかなり良い感じの十字キーができあがりました。操作感の仕上げのために、各方向キーの当たり判定を調整します。

ここからは理屈ではなくて実際に動くものを触りながら合わせこんでいったもの、言うなれば **kemokemo-fix** ですので、方向キーにつかう画像の形状やご自身の好みで皆様なりに変更していただいた方が良いかもしれません。

という訳で、くだんの **kemokemo-fix** はこんな感じに調整しました。

![rectangle_for_hit.png](https://qiita-image-store.s3.amazonaws.com/0/166225/a96d0970-ca1f-8802-48e1-323c446aa848.png)

* 方向キーの短辺方向: ボタン幅と同じだけ左右に範囲を広げる
* 方向キーの長辺方向: ボタン幅と同じだけ外側に範囲を広げる

# まとめ

この記事では`十字キー`に特化してご説明いたしました。
次回はアクションゲームのジャンプやシューティングゲームのショットなどで使うような`トリガーキー`についてご説明いたします。

`VirtualGamePad`サイコーʕ◔ϖ◔ʔ