この記事は Ebiten Advent Calendar 2018 5日目の記事です。
Advent Calendar初参加でドッキドキですが、張り切っていきたいと思います!!ʕ◔ϖ◔ʔ
ゲームパッドが好き
こんにちは!ファミコン世代の@kemokemoと申します!
最近息子たちもゲームパッドに慣れてきて、我が家の「ニンテンドークラシックミニ」と「ニンテンドークラシックミニ スーパーファミコン」の稼働率はかなり高めです。
突然ですが、私、ゲームパッドの操作性でゲームをするのが好きなんです。
スマホやタブレット用のゲームでも、ゲームパッドっぽいのが画面上に表示されて方向キー
やトリガーボタン
(押したら技とか出るやつ)をグリグリ、バシバシ操作して遊ぶようなものが結構ありますよね?ああいう感じです。
この記事について
上記で挙げたような画面上に表示された擬似的なゲームパッド
のことを、自分は便宜上VirtualGamePad
と呼んでおりまして、少し前にebitenを使って実装してみました。
https://github.com/kemokemo/ebiten-sketchbook/tree/master/virtual-gamepad
この記事では特に十字キー
にスポットを当ててお話したいと思います。
最後までお付き合いいただけますと幸いです!ʕ◔ϖ◔ʔ
前説
「Go言語もebiten
ライブラリも知ってるよ!」という方は読み飛ばして実現イメージの確認へと進んでください。
Go言語はゲームだって作れます
日々、多くのオープンソースや企業のプロダクトで採用されているGo言語
、その魅力については敢えて語るまでもないかと思います。簡潔に分かりやすく書けてパフォーマンスも最高、そんなGo言語
でゲームだって作れます。そう、@hajimehoshi さんが作っておられる ebitenライブラリを使えばね
準備
この記事では、ebiten
を使ったゲーム制作の始め方的な内容は割愛いたします。
その代わりと言ってはなんですが、参考情報を以下に書きます。
開発環境
開発環境の整え方について公式WikiのPlatformsにプラットフォーム毎の内容がありますので、そちらをご覧になってください。Windows、Ubuntu Desktop、macOSなど、どこでもサクッと始められます。実際にやってみたことある私が保証しますʕ◔ϖ◔ʔ
チュートリアル
「ebiten
に興味出てきた、簡単な例を実際に動かしてみたい」という方には公式WikiのTutorialsがオススメです。「チュートリアルも終わってもっといろいろ動かしてみたいぞ」という場合には公式リポジトリのexamplesフォルダにあるたくさんのサンプルが最高の教材になると思います。
実現イメージの確認
改めまして、今回ご紹介するVirtualGamePad
を使ったサンプルの画面は以下です。
動かしてみた動画はYouTubeにアップしたので こちら からご覧いただけます!
主な特徴は以下のような感じです。
- 画面上の
十字キー
を指でグリグリ押したら、キャラクターがグリグリ動く -
Aボタン
を指で押して離したら、キャラクターがモードチェンジ(色が変わって、ショットの種類も変わる)する -
Bボタン
を指で押している間、ショットが出続ける
こうやって改めて見ると、そこはかとなくゲームウォッチっぽいですね(;´∀`)
十字キー
の実現方法について説明しますよ!
このように画面上に十字キーを表示して 単純にタッチ位置と方向キー画像の当たり判定だけ を行うと、以下のような問題が起きました。
-
上と下同時押し といった、現実世界のゲームパッドでは発生しないような入力ができてしまう
- ユーザーの入力方向をどっちやと判定すればよいのか・・
- 押してるつもりなんだけど反応しない、反応がシビアすぎ
改めて考えてみると、ファミコンのゲームパッドとかって上下や左右は同時に押せないようになってるんですよね。以下はWikipediaの十字キーの記事からの引用です。
十字の中心には支点があり、カバーがシーソーのように動くことで、上と下または左と右のスイッチが同時に押せないようになっている。十字キーを斜めに押すことで上と右、下と左のように隣り合う2つのスイッチを同時に押せるようになっており、これにより8方向の入力が可能になっている。
また、現実の十字キーは方向キーの真ん中に指が当たっていなくても 方向キーの端っこに指がかかっているだけで入力状態になります 。上記の説明にあるような機構のため、方向キーの端っこを押しただけでもキーパッド全体が傾いて入力として受け付けてくれるからです。
これらの特徴を実現することで、より現実のゲームパッドに近い操作感が得られそうです。
考え方と作り方
上記に挙げたような現実のゲームパッドの機構を考慮しながら、よりリアリティが感じられるVirtualGamePad
の作成に取り掛かりましょう。
まず上記で挙げた問題点を解決すべく、以下のような対策案を考えました。
- ユーザー入力と複数の方向キーの当たり判定結果から、総合的に「入力方向」を確定する仕組みを作る
- 動かせるモックをいじりながら、タッチ入力に対する方向キーの当たり判定範囲をいい感じに広げる
この考えを実現しつつ、以下のような作り方を実践しました。
- 上下左右に4つ配置した方向キーを管理する
DirectionalPad
を作る - 外部からもらった座標を元に、ゲームパッドをいい感じに配置する
-
ebiten
から毎フレーム呼ばれるUpdate
メソッドで以下の処理を行う- ユーザーのタッチ位置から各方向キーの押下状態を更新する
- 各方向キーの押下状態から総合的に判断してユーザー入力方向を判定する
- 押される方向キー
directionalButton
に「君、押されてるで」と教える
- ユーザーのタッチ位置から各方向キーの押下状態を更新する
- 方向キーの当たり判定範囲をいい感じに調整する
上下左右に4つ配置した方向キーを管理するDirectionalPad
を作る
// 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
を返すメソッドです。土台にする画像サイズと方向キー画像サイズをあらかじめ調整しておけば、下図のようにゲームパッドの見た目を調整できます。
外部からもらった座標を元に、ゲームパッドをいい感じに配置する
// 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 updates the internal status of this struct.
func (dp *DirectionalPad) Update() error {
dp.updateDirection()
dp.updateButtons()
return nil
}
updateDirection
でユーザーが押した方向を判定して、updateButtons
で方向キーにその情報を伝えてます。順に見ていきます。
ユーザーのタッチ位置から各方向キーの押下状態を更新する
updateDirection
メソッドでの処理を見てみましょう。
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
で判定しなかったんや俺・・(´・ω・`)
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
に渡して一意の方向にします。
各方向キーの押下状態から総合的に判断してユーザー入力方向を判定する
func (dp *DirectionalPad) defineDirection(directions []Direction) {
current := None
for index := range directions {
current = getMergedDirection(current, directions[index])
}
dp.selectedDirection = current
}
directions
にはより優先度が高いユーザー入力から順に保存されているので、これを順番に取り出して評価していきます。
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
に「君、押されてるで」と教える
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
にいれてありますので、ユーザーの入力方向としてゲーム側が認識した方向キーのみ色を変えて「入力受け取ったで!」とアピールできます。ユーザビリティ的にも「押してる感」が出て良い感じです
方向キーの当たり判定範囲をいい感じに調整する
さて、ここまでの実装でかなり良い感じの十字キーができあがりました。操作感の仕上げのために、各方向キーの当たり判定を調整します。
ここからは理屈ではなくて実際に動くものを触りながら合わせこんでいったもの、言うなれば kemokemo-fix ですので、方向キーにつかう画像の形状やご自身の好みで皆様なりに変更していただいた方が良いかもしれません。
という訳で、くだんの kemokemo-fix はこんな感じに調整しました。
- 方向キーの短辺方向: ボタン幅と同じだけ左右に範囲を広げる
- 方向キーの長辺方向: ボタン幅と同じだけ外側に範囲を広げる
まとめ
この記事では十字キー
に特化してご説明いたしました。
次回はアクションゲームのジャンプやシューティングゲームのショットなどで使うようなトリガーキー
についてご説明いたします。
VirtualGamePad
サイコーʕ◔ϖ◔ʔ