PowerAppsを使ってNon-Codeでゲームを作ってみた

# 概要

この記事は[Low-Code/No-Code Advent Calendar 2018](https://qiita.com/advent-calendar/2018/lowcodenocode) 16日目の記事です。

Microsoft の PowerApps を使ってゲームを作ってみました。
その時の知見をまとめたものになります。

# PowerAppsとは

PowerAppsとは Excel のような関数と PowerPoint のような操作だけで、コーディングすることなくアプリケーションが開発できるサービスです。
通常は業務関係のアプリを作ることの多い PowerApps ですが、今回はこれを利用してゲームを作成しました。
利用には Office 365ライセンスまたは PowerApps プランの加入が必須となります。

## 注意点
2019年2月より PowerApps と Flow の料金体系が変化します。
ざっくり説明すると、
カスタムコネクタ、やオンプレとのデータ接続や作成をしたい場合はいままで Officeアカウントでできてたけどこれからはそれぞれプラン１以上になってね！ということです。
詳しくはこちら
https://community.dynamics.com/b/dynamicsblog-ja-jp/archive/2018/12/11/updates-to-microsoft-flow-and-powerapps-for-office-365

なお、今回のアプリに関してはどちらも利用していないので、もし同様のものを作る場合でも Officeアカウントで十分です。

# 完成物
制作時間 5 時間
テスト、素材集め（[いらすとや](https://www.irasutoya.com/) さんからお借りしています） 0.5時間
ゲーム内容は　ゲーム＆ウォッチのヘルメットとほぼ同じで落ちてくる障害物を交わしながら、隣の家へ５回たどり着くという内容です。

## イメージ

スタート画面です　歯車を押すことでサイドメニューが飛び出して各種設定が行えます。
![001.JPG](https://qiita-image-store.s3.amazonaws.com/0/183354/7d02e521-ab08-9728-a880-31093fbed2bb.jpeg)

チュートリアル画面です。スクリーンのテンプレートを若干改造しただけでほぼそのまま流用しています。
![002.JPG](https://qiita-image-store.s3.amazonaws.com/0/183354/48039f8e-7fc9-abfa-6d1d-68e58fc5e4a7.jpeg)

プレイ画面その１です。隣の家の窓（雨戸）が閉じているため、この場合家に入ることができません。
![003.JPG](https://qiita-image-store.s3.amazonaws.com/0/183354/e7938f35-8088-2858-7f23-1ae843acc67a.jpeg)

プレイ画面その２です。今度は開いているので入れます。
![004.JPG](https://qiita-image-store.s3.amazonaws.com/0/183354/19cfaa15-73f0-fef3-cf9c-2914b864f51b.jpeg)

ゲームオーバー画面です。タイムオーバーもしくは４回障害物に触れるとゲームオーバーです。コンテニューは何度でも行えます。
![005.JPG](https://qiita-image-store.s3.amazonaws.com/0/183354/d982018f-2594-303f-1ab5-2206640f9f5a.jpeg)

クリア画面です。スコアとして左側にかかった時間(コンテニューするとリセットされる)とコンテニュー回数を表示しています。
![006.JPG](https://qiita-image-store.s3.amazonaws.com/0/183354/ffe2737c-6d29-803a-cead-ea782dc6d063.jpeg)

なお今回は特に外部と通信を行っていない為、このゲームを拡張するとしたら、ランキング一覧などを作って競う形にしても面白そうです。

## ファイル
ちなみにファイルは.msappなので、インポートではなくファイルを開くから開いてください。
https://github.com/github129/Game-Watch-Helmet-2018


# 知見
## 連続的な動作にはタイマーを利用する

基本的に PowerApps では連続した動作（例えば障害物の落下）などの関数は存在しません。ですので、そういった動作をする場合は タイマーコントロール を利用する必要があります。

ちなみに今回タイマーで動いているのは以下の部分です。

- 設定メニューの開閉
- 障害物の落下
- 家に入った、ゲームオーバー、ゲームクリアといった各種判定
- 制限時間

## コレクションの値がテーブル（配列）になってしまった場合は ForAll関数を利用すると良い

よくAPIなどで値をコレクションに代入すると

![1.png](https://qiita-image-store.s3.amazonaws.com/0/183354/b5f63a93-130f-ffa6-f5f2-83f06afd61b7.png)

こういった状態になることがあります。
このままの使用は難しいので（ギャラリーを使えばできないこともない）、別のコレクションを新たに作り、そこに配列の情報をForAll関数を使い代入するようにします。

```
ClearCollect(tempCollection,Defaults(datasource));
Clear(tempCollection);

// For each of the records retrieved from GraphAPI, prepare the record to be written back to your datasource by lining up the right columns
ForAll(responsevariable,
    Collect(tempCollection,
        {
            column1: [corresponding field from response variable]
            column2: [corresponding field from response variable]
        }
    )
);

```

https://powerusers.microsoft.com/t5/General-Discussion/An-array-formed-with-a-subarray-value/m-p/83737
https://powerusers.microsoft.com/t5/General-Discussion/Method-for-collect-all-objects-in-array/m-p/172017

このあたりが参考になります。

## できるだけ変数を利用する

変数を利用しないと値の変更や動作が思い通りにいかないケースが多々出てきます。
値は基本的に UpdateContext関数や Set関数を利用して、変更のしやすい形を作りましょう。


## 変数の更新は必ず関数を利用すること

プログラムをかじっていると例えば

```
int a;
a += 1;
```

と PowerApps でもついやりたくなります。
が、 PowerApps はこのように変数に対して値を代入するだけでは変数の値は更新されません。(私はよく間違えます)
必ず UpdeteContext 関数等で更新するようにしましょう。 

## コントロールの名前は注意してつける

PowerApps ではコントローラー名を変更することができますが、アプリ内では常に一意に保たなければなりません。（スクリーン外でも）
なぜかというと、PowerApps ではフォーム名の参照を修飾しないため（Accessでいうフォーム名.コントロール名）為、すべてのコントロールは一意でなければなりません。

ここで問題となるのは命名規則です。
PowerApps コミュニティーの方でも議題には上がっていますが、私の場合は
コントロール名＿機能名＿スクリーン名
としています。

```
btn_formSubmit_Home
```

のような感じです。

## 存在しない形は組み合わせることでそれらしく作る

今回でいうコントローラーの十字（今回は左右）のキーの部分がまさにそうです。本来このような形のコントロールは存在しませんが、四角と六角形を組み合わせることで作成しています。

## 実機でのデバックは必須

基本的に PowerApps はマルチプラットフォームで動作しますが、やはりデバイスごとの OS のバージョンやメモリなどで思った通りに動作しないことがあります。そのため、必ず実機でのデバックを行いましょう。

## グループをうまく利用すると開発が楽

各種コントロールは　Ctrlキーで複数選択し、右クリックを押し、「グループ」を選択することでグループ化することができます。まとめて表示非表示を切り替えたいといった場合に便利です。

## グローバル変数で debug を作ると開発が捗る

開発時にラベルやタイマーを表示状態にし、公開するタイミングで、消すもしく Visible を false にしている場合は、グローバル変数で 

```
Set(debug,false)
```

を作り、それを各コントロールの Visible に設定することで、追加開発や、デバックなどでコントロールを消したりする手間が省けます。

## TimePicker コントロール

PowerAppsには TimePicker は存在しません。（Date Pickerはあります。）
その為、ドロップボックスコントロール等を利用して自力で作る必要があります。
私の場合は以下を使用しています。

```
ClearCollect(timePicker, [ "0:00","0:05","0:10","0:15","0:20","0:25","0:30","0:35","0:40","0:45","0:50","0:55", "1:00","1:05","1:10","1:15","1:20","1:25","1:30","1:35","1:40","1:45","1:50","1:55", "2:00","2:05","2:10","2:15","2:20","2:25","2:30","2:35","2:40","2:45","2:50","2:55", "3:00","3:05","3:10","3:15","3:20","3:25","3:30","3:35","3:40","3:45","3:50","3:55", "4:00","4:05","4:10","4:15","4:20","4:25","4:30","4:35","4:40","4:45","4:50","4:55", "5:00","5:05","5:10","5:15","5:20","5:25","5:30","5:35","5:40","5:45","5:50","5:55", "6:00","6:05","6:10","6:15","6:20","6:25","6:30","6:35","6:40","6:45","6:50","6:55", "7:00","7:05","7:10","7:15","7:20","7:25","7:30","7:35","7:40","7:45","7:50","7:55", "8:00","8:05","8:10","8:15","8:20","8:25","8:30","8:35","8:40","8:45","8:50","8:55", "9:00","9:05","9:10","9:15","9:20","9:25","9:30","9:35","9:40","9:45","9:50","9:55", "10:00","10:05","10:10","10:15","10:20","10:25","10:30","10:35","10:40","10:45","10:50","10:55", "11:00","11:05","11:10","11:15","11:20","11:25","11:30","11:35","11:40","11:45","11:50","11:55", "12:00","12:05","12:10","12:15","12:20","12:25","12:30","12:35","12:40","12:45","12:50","12:55", "13:00","13:05","13:10","13:15","13:20","13:25","13:30","13:35","13:40","13:45","13:50","13:55", "14:00","14:05","14:10","14:15","14:20","14:25","14:30","14:35","14:40","14:45","14:50","14:55", "15:00","15:05","15:10","15:15","15:20","15:25","15:30","15:35","15:40","15:45","15:50","15:55", "16:00","16:05","16:10","16:15","16:20","16:25","16:30","16:35","16:40","16:45","16:50","16:55", "17:00","17:05","17:10","17:15","17:20","17:25","17:30","17:35","17:40","17:45","17:50","17:55", "18:00","18:05","18:10","18:15","18:20","18:25","18:30","18:35","18:40","18:45","18:50","18:55", "19:00","19:05","19:10","19:15","19:20","19:25","19:30","19:35","19:40","19:45","19:50","19:55", "20:00","20:05","20:10","20:15","20:20","20:25","20:30","20:35","20:40","20:45","20:50","20:55", "21:00","21:05","21:10","21:15","21:20","21:25","21:30","21:35","21:40","21:45","21:50","21:55", "22:00","22:05","22:10","22:15","22:20","22:25","22:30","22:35","22:40","22:45","22:50","22:55", "23:00","23:05","23:10","23:15","23:20","23:25","23:30","23:35","23:40","23:45","23:50","23:55", ])
```

5分刻みの TimePikcer になっています。
時分をバラバラに作ってもよかったのですが、２回選択しなければならないのがめんどくさそうだったのでまとめてみました。

## ローカルストレージを上手に利用する

今回は利用していませんが、PowerAppsでは通常アプリを閉じると消えてしまうコレクションをローカルストレージ（ローカルキャッシュ）に保存する SaveData 関数が存在します。
個人ごとのアプリ内設定や、オフライン対応などもこの関数を利用することで対応が可能になっています。ただし注意点としては、web版では現時点（2018/12/10）では利用できない点です。

## CDS for Apps は利用者も PowerApps プランへの加入が必須

CDS(Common Data Service)でアプリを作るにはプラン2、利用者はプラン1,の加入が必須です。
ちなみにプランへ加入していなくてもアプリ自体の共有は可能ですが、アプリを開いたときにライセンスをチェックされ開けません。

## IsBlank() と Blank() は同じ結果を返しません

```
IsBlank("")
```

と

```
"" = Blank()
```

は結果が違います。
どうやら、Blank()は正確にはオブジェクト型となっており、Text型の「""」とオブジェクト型で比較をしてしまっている為、falseと判定されているようです。早いところ統一してほしいですね。

# パフォーマンス改善

## 1.同期的な処理ができるように心がける

Onvisible や OnStart のタイミングで変数やコレクションを作成しますが、普通に書いてしまうと処理が遅く成るります。 Concurrent 関数を利用し、同期的に処理を行うことで処理時間を大幅に削減することができます。ただし Concurrent 関数は関係性を見ない点に注意が必要です。

## 2.データソースを直接参照する方法はできる限り避ける

PowerApps でギャラリーなどを作る場合直接データソースを参照することが多いですが、その場合表示やフィルター等のたびにデータソースを参照する為、ローカル上にデータをキャッシュしそれを参照するようにするとパフォーマンスが改善されます。

## 3.User()関数は１度しか使わないようにする

ラベルコントロールなどに 

```
User().FullName
```

等とすると、こちらも 2 と同様無駄な処理が走ります。
1度取得すればいい情報は OnStart のタイミングで変数に代入してしまいましょう。

## 4.無駄な通信を減らす。

例えば Office365 ユーザーをコレクションに入れる際に

```
ClearCollect(
        ContactDetails,
        AddColumns(
            Contacts,
            "Phone",
            Office365Users.UserProfile(POC).TelephoneNumber,
            "Title",
            Office365Users.UserProfile(POC).JobTitle,
            "Full Name",
            Office365Users.UserProfile(POC).GivenName & " " & Office365Users.UserProfile(POC).Surname
        )
    )
```

等と行うと、都合４度通信していることになります。
上記の場合ですと、Office365に通信するのは１度にし、

```
ClearCollect(
    Contacts1,
    AddColumns(
        Contacts,
        "Profile", Office365Users.UserProfile(POC)
    )
);
ClearCollect(
    ContactDetails,
    DropColumns(
        AddColumns(
            Contacts1,
            "Phone", Profile.TelephoneNumber,
            "Title", Profile.JobTitle,
            "Full Name", Profile.GivenName & " " & Profile.Surname
        ),
        "Profile"
    )
)
```

とすることで約４倍速にすることができます。

## 5.Default() で通信を行わない

こちらも4と理由は同様で、コントロールごとに無駄な通信が発生します。
OnStartは１度しか実行されないので、そちらでデータをグローバル変数に設定し、それを参照するようにしましょう。


## 6.低速なデータソースを避ける

既に使用している場合は難しいですが OneDrive や Excel ではなく CDS や SQLDB等を使用するようにするとパフォーマンスが改善されます。

# まとめ

PowerApps でゲームを作ると、様々な知見が得られて楽しかったです。
あまり気に留めていなかった、パフォーマンス周りのテクニックも身につくし、作っていて楽しいのでお勧めです。~~（業務アプリばかり作っててもあまり面白くない...）~~
PowerApps コミュニティでも有志による様々なゲームやアプリが公開されているので、興味がある方は是非ご覧ください。

明日の記事は[nori790822](https://qiita.com/nori790822)さんになります。