UnityでAndroidの戻るキー対応をなるべく簡単にする提案

# 意外と面倒くさいAndroid戻るキー対応
ね。面倒くさいですよね。（iOSには無いわけですし・・・）
さしあたり一番簡単なのは「画面上の『ネガティブ的』なボタンと同等の機能をつける」事です。

ダイアログを開いたのであれば、戻るキーが「閉じるボタン」や「戻るボタン」相当になればよいですし。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/45880e23-77e6-3d54-dc98-6fcd194c2c75.png)

画面遷移したのであれば、戻るキーが「Backボタン」（前の画面に戻る）相当になればよいわけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/cf2ac102-d7c6-b109-08a1-591f2a288ecb.png)


そう書くと簡単のように聞こえますが、
画面遷移した次の画面で、ダイアログを表示した場合は？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/2211af5a-9912-a20b-c533-6a684bb5c853.png)

**戻るキーを押したら、ダイアログも閉じてしまう＋画面も前の画面に戻ってしまう**

では困るわけです。

これをまともに対応しようとすると、

- 優先順位スタックマネージャ的なクラスを作成
- 画面遷移したら、「戻るボタン」の処理(Actionとか?)を（上記）スタックマネージャにPush
- ダイアログを開いたら「ダイアログ閉じるボタン」の処理(Actionとか?)をスタックマネージャにPush
- 戻るキーを押したら、スタックマネージャにスタックされている処理の一番上(Peek)を処理
- ダイアログを閉じたら「ダイアログ閉じるボタン」の処理をスタックマネージャからRemove
- もう一度戻るキーを押したら・・・・

といった、管理が必要になります。　**はい面倒臭いですね！**

# もっとシンプルに考える
そもそも、上記例の「画面遷移した次の画面で、ダイアログを表示した場合」って、普通はダイアログがモーダル的に表示されていて、後ろの「戻るボタン」は押せないようにしているのがほとんどのはず。（わざわざ後ろのボタンのintaractiveをfalseにしているのか、「タッチガード」的な全画面Panelを一枚噛ませてタッチイベントを遮断しているかのどちらかがほとんどでしょう）

問題なのは、

**単純に戻るキーとボタンが押されたときの処理を関連付けてしまうと、uguiのイベントとは関係無しに処理が呼ばれてしまう**

ことです。

なので、徹底的にuguiのイベントを倣い、**戻るキーの押下を指定ボタンへのマウスクリックへとすり替えてあげれ**ば解決です。

#作ってみた

```cs:KeyBind.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(Button))]
public class KeyBind : MonoBehaviour
{
    [SerializeField]
    private Button _targetButton;

    public KeyCode _bindKey;

    private void Reset()
    {
        _targetButton = GetComponent<Button>();
    }

    private static List<RaycastResult> raycastResultList = new List<RaycastResult>();
    private PointerEventData _pointerEventData;

    private void Update()
    {
        //指定したキーの押下
        if (Input.GetKeyDown(_bindKey))
        {
            _pointerEventData = new PointerEventData(EventSystem.current)
            {
                button = PointerEventData.InputButton.Left,
                position = _targetButton.transform.position //指定したボタンの位置にマウスがある体
            };
            EventSystem.current.RaycastAll(_pointerEventData , raycastResultList);
            var validGameObject = raycastResultList.Select(result => result.gameObject).FirstOrDefault(gameObject => gameObject != null);//一番最初にぶつかっている有効なGameObject取得
            raycastResultList.Clear();
            if (validGameObject == null)
            {
                return;
            }
            var currentPointerDownHandlerObject = ExecuteEvents.GetEventHandler<IPointerDownHandler>(validGameObject); //ボタン位置にあるGameObjectからIPointerDownHandlerを保持しているGameObjectを取得
            if (currentPointerDownHandlerObject != _targetButton.gameObject){
                return;    //ボタン位置から得られたGameObjectとボタンのGameObjectが異なる＝別のもので遮られている ので処理しない
            }

            _pointerEventData.pointerPress = currentPointerDownHandlerObject;
            ExecuteEvents.Execute(currentPointerDownHandlerObject, _pointerEventData, ExecuteEvents.pointerDownHandler);
        }

        //指定したキーの押上
        if (_pointerEventData != null && _pointerEventData.pointerPress != null && Input.GetKeyUp(_bindKey))
        {
            ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerUpHandler);
            ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerClickHandler);
            _pointerEventData = null;
        }
    }
}
```

（よくわからんなりに調べて作ったので、大分力業ですが・・・）

# 使い方
このScriptを`Button`コンポーネントが乗っている`GameObject`に追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/90a0c2e1-f183-b8b5-b421-601edc06e1cf.png)

`Target Button` は勝手に同`GameObject`の`Button`がセットされます。
そして
`Bind Key` には**割り当てたいハードキー** を指定します(`KeyCode` の一覧が候補で出ます）
Androidの戻るキーは **KeyCode.Escape** で割り当たります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37184/db83dc2a-c87a-8bdc-4de2-af4daac71d1a.png)

なんと、これだけで、ボタンのタップとAndroidの戻るキーが同等になります！　シンプル！！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Androidの戻るボタン対応<br>1回目は、普通にマウスでダイアログの「戻るボタン」と画面の「バックボタン」をクリック、2回目はAndroidの戻るボタンによる処理。<br>”クリックできるボタンのみ反応&quot;するので、お手軽Android戻るキー対応としてはありなんじゃないかと。<br>解説はこちら<a href="https://t.co/je9qlPXJ7V">https://t.co/je9qlPXJ7V</a> <a href="https://t.co/DfkPELJYqp">pic.twitter.com/DfkPELJYqp</a></p>&mdash; すずきかつーき (@divideby_zero) <a href="https://twitter.com/divideby_zero/status/1229595648472698887?ref_src=twsrc%5Etfw">February 18, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#注意
ボタンを疑似的にクリックした相当なので、（利点でも欠点でもあるんですが）ボタンのTransitionがそのまま効きます。
↑の動画をよく見ると分かるんですが、戻るキーを押した時でもボタンの色が変化しています（戻るキーを押しっぱなしにすると、ボタンも押されっぱなしになる）
それが嫌！　という場合には使えないです。　悪しからず・・・。

#補足
今回、 **Androidの戻るキー対応** と銘打っては居ますが、既に書いた通りボタンには KeyCodeで割り当てるキーを指定することができます。
もう一つの使い道として、**入力処理の一元化**があります。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">初めてUnity(2019.3)使うので、練習としてテトリス？っていうのを作ってみました。<br>聞くところによると「Tスピン」というのが強いらしいので見様見真似で実装してみましたが、回転力を強くしすぎてしまったようです。<a href="https://twitter.com/hashtag/madewithunity?src=hash&amp;ref_src=twsrc%5Etfw">#madewithunity</a><a href="https://t.co/7XjEMvHJLx">https://t.co/7XjEMvHJLx</a> <a href="https://t.co/us0kXPFT4G">pic.twitter.com/us0kXPFT4G</a></p>&mdash; すずきかつーき (@divideby_zero) <a href="https://twitter.com/divideby_zero/status/1227990878305677312?ref_src=twsrc%5Etfw">February 13, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こちらの動画のゲームで今回のKeyBind.csが実際に使われており、前半はマウスでボタンをクリックして操作ですが、後半はそれぞれのボタンに割り当てられたキーボードで操作しています。

このように「複数方法の入力処理を制御」するには

- ベタでボタンがクリックされた場合の処理とキーボード入力の処理の場合を分けて書いてしまうスタイル
- `IInput` のような入力処理を抽象化したインタフェースを切り、`IInput` を実装した `KeyboardInput` と `ButtonInput` のようなクラスをそれぞれ実装するスタイル
- 神Inputクラスに想定される全インプット処理分の条件分岐をぶち込んでいくGODスタイル

などなど。方法はありますがそれなりに面倒で。

対して、↑の動画のゲームでは入力制御は「ボタン処理」のみ対象に記述しています。
そして、KeyBind.csはあくまでもボタンの疑似クリック処理なので、キーボード操作を増やしても入力制御処理は何も手を入れずに済んでいます。　つまり、ボタンによる入力処理で「処理の一元化」がされている状態です。

もちろん、これは画面上にバーチャルパッド的なものをuguiで置いているから出来るだけなので適用範囲はそう広くは無いですが、使える人も少なくないのではないでしょうか。