キーボード、ゲームパッドコントローラーで操作可能なScrollView (KeyControllableScroll)


キーボードや、ゲームパッドコントローラーで操作可能な `ScrollView` をMITのオープンソースライブラリとして作ったのでご紹介。

" Key Controllable Scroll " なので以下 `KCS` と呼びます。

使い方の面で少し面倒な部分もありますが、スクロールの挙動を色々カスタムできる上、パフォーマンスもかなり良いと思います。

[OSSのリンクはこちら](https://github.com/yahhoi136/KeyControllableScroll-Master/tree/ver.1.0.1)
~~[Read Me in English is Here](https://github.com/yahhoi136/KeyControllableScroll-Master/blob/main/README.md)~~ // Sorry, not finished yet 

*なぜかSafariでこのページ開くとGif挙動がおかしくなるらしいので、Chrome等で開くことをお勧めします。



# 概要

### 操作
KCSでは「スクロールの操作」と「フォーカスの操作」がそれぞれ可能です。

##### スクロールのキー操作
縦スクロールの時は上下、横スクロールの時は左右に動かせます。
フォーカス (黄色ハイライト) は画面内に収まるように自動で移動します。
![ScrollSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/acd7b2c6-d9de-fa84-5e8b-d4ebe3e148f7.gif)


##### フォーカスのキー操作
上下左右のセルにフォーカスを動かせます。
フォーカスが画面内に収まるようにスクロールは自動で移動します。
![FocusSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/e9d2dc10-c644-ef8c-ccfd-499881a5a7c5.gif)



### 負荷

##### 軽量化
* スクロールの移動によって画面外に出たセルは、再利用しているため軽量化されています。
* 初期化時にセルの任意のコンポーネントをキャッシュできるため、動作中に`GetComponent()`の必要がありません。
* セルの再利用とキャッシュには配列 `Array` を使っているためメモリ負荷も軽いです。
* より詳しい負荷については、後述の [**負荷について (詳細)**](https://qiita.com/yahhoi136/items/7ebd986bdde3f0daa694#%E8%B2%A0%E8%8D%B7%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6-%E8%A9%B3%E7%B4%B0) にて説明しています。

↓ セルの再利用 ↓
![MaskSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/608bbce8-35a8-937c-1f7f-4f227b5447e2.gif)







# 使い方 (KCSの作成と追加)

## 使用上の注意
* １つの KCS 内では同一サイズのセルしか使えません。
* セル Prefab の種類ごとに KCS を作成する必要があります。*1
* セル Prefab内の、オブジェクトの親子関係が変化したとき (追加/削除/移動) には KCS を作成し直す必要があります。*2

*これらは「使い方」の使用例でも説明します。


## 1. セル Prefab の作成

#### 手順
* セルのPrefabを作ります。
* この時、**Rootオブジェクトには必ず Anchors Max (x, y) = (0, 1)、 Anchors Min (x, y) = (0, 1)、 Pivot (x, y) = (0, 1)、の `RectTransform` をアタッチしてください**。
* セルの内容が定まっていない時は、適当に Rootオブジェクトと RectTransform だけでもいいですが、**セルPrefab の内容が変化 (子オブジェクトの追加/削除/移動)すると、この「使い方」の手順をやり直す必要がある** ので、先にある程度決めておくと楽です。
* フォーカスは セルPrefab の Rootオブジェクト が対象になるので、**Rootオブジェクト に Button などをアタッチするとフォーカスを視認できます。** Sample では Button をアタッチして Selected Color を黄色にしています。
* 実際にはセルの内容を持ったオブジェクトであれば良いので、必ずしも Prefab化したオブジェクトである必要はありません。


#### SampleのセルPrefabの内容はこんな感じ
`SampleCell` が Rootオブジェクト、`ImageObject` と `TextObject` が同じ階層の子オブジェクトです。
`-`以下はそのオブジェクトにアタッチされているコンポーネントです。
![SampleCell.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/04d29636-9ed6-0b51-4186-c858bebb13a6.png)



## 2. KCS Script.cs の作成
 [OSSをGitHubからダウンロード](https://github.com/yahhoi136 "GitHub Source Code") していると、ツールバーから、Window > UI Toolkit > KCS Script Generator を開けます。
![Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/db7ace47-8dd1-74ad-5b12-b9bb12f70db6.png)

開いた KCS Script Generator ウィンドウで、先ほど生成したセルPrefabを `Cell Prefab:` にアサインすると、こんな感じに折り畳みやトグルをチェック出来るようになります。
![KCSGeneratorWindow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/53cb11a1-c0ee-853c-835f-98d6cd1f3bf2.png)

それぞれのパラメーターを設定します。
 * `KCS Type:`
KCS のスクロール方向を、横 (Horizontal) か縦 (Vertical) か選べます。Sampleでは縦方向です。

 * `Cell Prefab:`
この KCS に使うセルを選択します。これが None だと以下の項目は表示されません。

 * `DataSource Target Components:`
初期化時にキャッシュしたい任意のコンポーネントを指定できます。 **セルを描写する時に使いたい セルPrefab のコンポーネントにチェックを入れておきましょう** (例えば、表示するセルごとに画像を変えたいなら Image 、ボタンクリック操作を変えたいなら Button などなど)。ここに表示されるオブジェクトやコンポーネントの順番は セルPrefab の `Hierachy`で表示される通りなので、見比べながら確認すると便利です。

 * `KCS Name:`
KCS Script の名前を決めれます。

* `Generate KCS Script`
KCS Script を保存します。Dafault Path では `KeyControllableScroll > NewKCSs` のディレクトリに、Select Path では任意の場所に保存できます。




## 3. KCS オブジェクトの作成

 `Assets > Create > UI > KCS` から、横方向スクロールの時は `Horizontal KCS Framework` を、縦方向スクロールの時は `Vertical KCS Framework` を作成します。
![KCSFramework.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/64725e9a-7cba-7c0a-2b5d-74bdca12d3b6.png)


KCS Framework を作成した後は、KCS の Rootオブジェクト に先ほど作成した KCS Script をアタッチします。 `Add Component` ボタンから `UI > KCS` でも追加できます。
![KCSaddComponent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/5925855c-8323-8f56-d7f5-34ef9906b488.png)





# 使い方 (KCSのカスタム)


## 1. インスペクターでカスタム

次にインスペクターを見ていきます。


### Cell Recycle Param

KCS の基本的なパラメーターです。
![Inspector1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/91f4fca5-a219-36da-c3a7-4c3849dc3c79.png)


* `Smooth Mode:`
オフにすると、画面外で待機させるセルの数を減らします。負荷を軽くできますが、フォーカスのキーを高速に連続入力した時の遅れが大きくなります (下図➀)。**負荷を減らしたい時や、フォーカスのキーを連続入力した際に見栄えがゆっくりになっても構わない時はオフを推奨します。**

* `Cell Prefab:`
この KCS Script を作ったときに使った セルPrefab を設定します。**設定は必須です。**

* `Viewport Width / Height:`
横方向スクロールのときはVeiwport の幅を、縦方向スクロールのときはVeiwport の高さを設定します。デフォルトだと Viewport は KCS の Rootオブジェクトにフィットしているので、Rootオブジェクトの値をそのまま設定します。**設定は必須です。**

* `All Contents Num:`
セルの総数を設定します。**設定は必須です。**


#### 図➀ - SmoothMode オン (左) と オフ (右)
下図では同じ入力頻度でフォーカスキーを高速連続入力。
オン (左) の方が画面外で待機するセルの数が多く、フォーカスのキーを連続入力した時の遅れが小さい。
![SmoothMode.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/989e30e6-51ce-b170-7f7b-4378b4ce0ece.gif)




### Grid Layout Group Param

Grid Layout Group コンポーネントと同じノリでレイアウト設定ができるパラメーターです。これでセルの大きさや、セル同士のスペースを設定して下さい。ただし、KCS の仕様で値が制限されていたり (Limited)、固定されていたり (Fixed) します。
![Inspector2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/8a558b93-40d0-2dd0-e6a0-6b3fe7864db2.png)


* `Padding:`
 CellList の上下左右端から、セルの集団までの余白を設定できます。

* `Cell Size:`
セルのサイズを設定できます。

* `Spacing:`
セル同士間の余白を設定できます。

* `Start Coner:` / `Start Axis:`（Fixed）
横方向スクロールでは、セルは左上からスタートして上詰めに配置されていきます。縦方向スクロールでは、セルは左上からスタートして左詰めに配置されていきます (下図➁)。

* `Child Alignment:`（Fixed）
セルの集団は CellList の左上に配置されます。

* `Constraint:`
横方向スクロールでは行の数、縦方向スクロールでは列の数を設定できます。


#### 図➁ - セルの配置順
横スクロールは左上からスタートして上詰めに配置
![HoliAlignmentOrder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/16084519-b00e-86ee-6b90-1422e510cc50.png)

縦スクロールは左上からスタートして左詰めに配置
![VerAlignmentOrder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/7c47df4f-3633-f42a-cbad-2196d6f1f563.png)





### Scroll Key Control

スクロールのキー操作用パラメーターです。
![Inspector3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/e3cf32c8-335a-c0b3-7bad-5e1e73a47dfb.png)


* `Scroll Key Control:`
スクロールのキー操作が有効になります。**スクリプト上でスクロールキーを割り当てるインターフェースの実装が必要です ([後述](https://qiita.com/yahhoi136/items/7ebd986bdde3f0daa694#input-%E6%BA%96%E5%BF%85%E9%A0%88))。**

* `Scroll Speed:`
スクロールの速度を設定できます。



### Focus Key Control

フォーカスのキー操作用パラメーターです。
![Inspector4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/ea537b1a-4f3f-a64b-6ed0-3fdb0b755e9d.png)


* `Focus Key Control:`
フォーカスのキー操作が有効になります。**スクリプト上でフォーカスキーを割り当てるインターフェースの実装が必要です ([後述](https://qiita.com/yahhoi136/items/7ebd986bdde3f0daa694#input-%E6%BA%96%E5%BF%85%E9%A0%88))。**

* `Focus Fixed Row:`
スクロール方向に垂直な、 Viewport 中央の軸を 0 として、**Viewport 内でフォーカスが移動できる範囲を行数で設定できます (下図➂)**。スクロール方向のセルの大きさ ( 横方向では Width、縦方向では Height ) が Viewport に対して小さくなるほど設定できる範囲が増え、逆に十分大きいと 0 固定になります。

* `Scroll Chase Speed:`
フォーカスが画面内に収まるようにスクロールが自動移動する際の速度を設定できます (下図➃)。

* `Key Hold Repeat:`
キーの長押しで、フォーカスが連続移動するように設定できます。

* `Repeat Interval:`
フォーカスの連続移動のインターバルを設定できます。

* `Repeat Start Delay:`
キーの長押し時に、フォーカスが連続移動を始めるまでのディレイを設定できます。

* `Delay Decreasing:`
フォーカスが連続移動を始めるまでのディレイを徐々に減らして、移動を徐々に加速させます (下図➄)。

* `Decreasing Time:`
ディレイを完全に無くすまでの時間を設定できます。


#### 図➂ - Focus Fixed Row の設定
スクロール方向に垂直な、Viewport中央の軸が 0 となる。Sample では -1 ～ 1 の範囲で設定できる。
![FocusFixedRow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/de214607-1017-9153-4b43-ff64aaaf6dca.png)

Up Focus Fixed Row と Down Focus Fixed Row を 0 に設定すると、画面内におけるフォーカスの移動範囲は０行目だけになる。
![FocusFixedRowGif0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/52b0a4cd-ec36-23ad-0b3a-493b515b3f55.gif)

一方でそれぞれを -1 と 1 に設定すると、画面内におけるフォーカスの移動範囲は -1 ～ 1 行目までになる。
![FocusFixedRowGif1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/4cc88811-785d-d5e2-d4f7-7d65166ef731.gif)

Viewportに対するセルの大きさが小さいほど設定できる範囲が増え (左)、逆にセルの大きさが十分大きいと 0 固定になります (右)。
![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/ddfd3eca-758e-08ff-74a5-9dfbc230e75b.png)




#### 図➃ - Scroll Chase Speed の設定
フォーカスをキー操作した時に、スクロールがフォーカスを追従する速度。左が 0.1 右が 1 。
![ScrollChaseSpeed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/b189a875-b57f-7b1c-a9c3-b2f952f36bda.gif)



#### 図➄ - Delay Decreasing の設定
キーの長押し時、オフだとディレイは初めだけ生じる。オンだとディレイが徐々に減少し、移動が加速する。 
![DelayDecreasing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/70610fa7-eb28-99c2-3b57-25f4442d64ac.gif)



### Public Methods & Fields

上記以外の、スクリプト上で使える KCS の Publicメソッド や Publicフィールド をお試しで使えます。ランタイム中しか有効になりません。
![Inspector5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988003/8c9ba2a8-5920-723c-ecc3-d5c9e12fa40a.png)


* `Scroll Key Suspended:`
スクロールのキー操作を一時停止します。

* `Focus Key Suspended:`
フォーカスのキー操作を一時停止します。

* `Focused Cell No:`
現在フォーカスされているセルの番号が確認できます。Get のみで Set できません。

* `Jump To:`
後ろで指定した番号のセルまで移動してフォーカスします。

* `Scroll Init:`
KCS を初期化します。




## 2. インターフェースでカスタム

**KCS の初期化は `Start()` で行われているため、インターフェースのセットは `Start()` より前で行う必要があります。**
Sampleでは `Awake()` で行っています。

### DataSource (必須)

セルを描写する時の処理を実装します。**これを実装しないと KCS は動きません。**
引数には KCS Script を作成時に選択したコンポーネントがキャッシュされています。

```C#:DataSource
I"KCS名"DataSource
public void SetCellData(int index, Image image, Button button, Image image_0, Text text_1) {}
```


### Input (準必須)

スクロールまたはフォーカスをキー操作する時の、キーの割り当てを実装します。
`Scroll KeyControll` または `Focus KeyControll` を有効にすると、それぞれの実装が必要になります。
**長押しで連続入力が可能な入力 ( `Input.GetKey()` など )** を、それぞれのキーにタプルとして retrun してください。


```C#:Input
I"KCS名"ScrollKeyInput
public (bool ScrollUpOrLeftKey, bool ScrollDownOrRightKey) GetScrollKeys() {}

I"KCS名"FocusKeyInput
public (bool UpKey, bool DownKey, bool LeftKey, bool RightKey) GetFocusKeys() {}
```


### KeyMoved (任意)

スクロールまたはフォーカスのキー操作が行われるたびに呼ばれるインターフェースです。
移動時の効果音などを実装したい時に利用できます。

```C#:KeyMoved
I"KCS名"ScrollKeyMoved
public void WhenScrollMoved() {}

I"KCS名"FocusKeyMoved
public void WhenFocusMoved() {}
```



## 3. スクリプトでカスタム

#### Public フィールド
インスペクターのパラメーターとして表示されているフィールドは、全て Publicフィールドとして Get や Set できます。

```C#:Public フィールド
// 現在フォーカスされているセル番号の取得
int cellNo = "KCSのインスタンス".FocusedCellNo

// スクリプトから AllContentsNum (セルの総数) の設定
"KCSのインスタンス".AllContentsNum = 100 ;
```

ただし、**ランタイム中にインスペクターで無効になっているフィールドは、動作中に Set すると KCS の挙動がバグるので、Set する場合は初期化の前に行う必要があります。** デフォルトでは初期化Init( ) は `Start()` で行われます。

ランタイム中に無効になるパラメーター (右: 灰色部分)
![RunTImePublic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/1b343de4-a904-b177-73c5-289f934dc38c.png)



#### Public メソッド
インスペクターでボタンとして表示されている関数も、全て Publicメソッドとして利用できます。

```C#:Public メソッド
// KCS の初期化
"KCSのインスタンス".Init();

// 指定セル番号まで移動
"KCSのインスタンス".JumpTo( 68 );
```







# 負荷について (詳細)

### 実測値（セル描写実装なし）
Sample の セルPrefab を使い、セルの描写時に何の処理もしない KCS のみを配置したプロジェクト の PlayerLoop 負荷は、以下の通りになりました。
*Unity Profiler の Deep Profile で計測
| 状態 | GC Alloc. | Time ms |
|:-:|:-:|:-:|
| Init( ) 発火 | 2.6 KB | 6 ms 前後 |
| スクロール中 | 51 B | 1.5 ms 前後 |
| スクロール中以外 | 51 B | 1 ms 前後 |




Init( ) 発火
![SampleInit()時負荷.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/5b1d1b24-ac86-bf8c-4a0b-7e37971877ec.png)


スクロール中
![Sampleスクロール中負荷.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/9cfda9bc-7200-8a14-b0e7-fbf83e7c933a.png)

スクロール中以外
![Sample平常時負荷.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074835/34894588-6599-8cf7-8e07-f991f37ee68a.png)



### 利用時の最適化
上記のように、セルの描写時に何の処理もしない KCS の負荷は、初期化時ではそれなりに重く、初期化時以外はかなり軽くなっています (有名な [LoopScrollRect](https://github.com/qiankanglai/LoopScrollRect) と比べても軽い)。そのため、**KCS の利用時の負荷は 初期化Init( ) と、セル描写時の処理SetCellData( ) の重さに依存します**。

##### 初期化処理 Init( )
* できるだけ [`Init( )`](https://qiita.com/yahhoi136/items/7ebd986bdde3f0daa694#public-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)を発火しない。初期位置に戻りたいときは [`JumpTo(int)`](https://qiita.com/yahhoi136/items/7ebd986bdde3f0daa694#public-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)を使う。
* セルPrefab 内の子オブジェクト数を減らす。( `Instantiate()` が内在するため ) 
* [`SmoothMode`](https://qiita.com/yahhoi136/items/7ebd986bdde3f0daa694#cell-recycle-param) をオフにする。
* Viewport に対してセルやスペースを大きくして、画面に一度に表示されるセルの数を減らす。


##### セル描写処理 SetCellData( )
* String の多用など、重たい処理を避ける。
* [`SmoothMode`](https://qiita.com/yahhoi136/items/7ebd986bdde3f0daa694#cell-recycle-param) をオフにする。
* Viewport に対してセルやスペースを大きくして、画面に一度に表示されるセルの数を減らす。






# トラブルシューティング

#### インスペクターで Cell Recycle Param を埋めてください
KCS Script のインスペクターで、Cell Recycle Param が設定されていない時に発生します。

#### DataSourceにインターフェースをセットしてください
IDataSource インターフェースが実装されていない時に発生します。


#### ～ にインターフェースをセットするか、～ を無効にしてください
スクロールやフォーカスのキー操作が有効になっているが、キーの割り当てが行われていない時に発生します。
インターフェースでキーの割り当てを実装するか、インスペクターでキー操作を無効にしてください。

#### セルとスペースのサイズが大きすぎます
セルの大きさが Viewport の大きさよりも大きい時に発生します。
少なくとも１つのセルが完全に表示できるように、セルの大きさとスペースの大きさを小さくするか、Viewportを大きくして下さい。

#### 型や名前空間が見つからない: Type or NameSpace missing 
セル描写時にキャッシュしたコンポーネントを使用するのに、必要な型の参照がインポートされていない時に発生します。
エラー箇所に必要な参照を `using` でインポートしてください。


#### それ以外

KCS 以外の部分にエラーの可能性があるので、一度 KCS を取り除き、作り直してみて再度エラーが生じるか確認してください。依然


# 後の機能追加予定

* 無限スクロールに対応したい。
* 一つの KCS 内で異なる大きさのセルを置けるようになりたい。
* 逆方向のスクロール (右から左 or 下から上)を作りたい。
* セルの配置を左上以外からスタートできるようにしたい。


# Thank you for reading !

作るまでが面倒で申し訳ないです ; ; が、作り終えた後は結構カスタムも効く上パフォーマンスもかなり良いと思います。

(というよりカスタムできる要素を増やしすぎた感はある、、)

ちゃんとプログラミングを始めてまだ半年ちょいなので、あまり良くないコードとかが割とありそうです、、。

色々と参考にさせていただきたいので、意見やコメント等がありましたら是非よろしくお願いします _ ( . _ . ) _