PowerApps

PowerAppsでゲームを作ってみたらいろんな知見を得られた


この記事の対象者



  • PowerAppsをある程度触ったことがある方

  • 一からアプリを作りたいと思っているが何から始めてよいか迷っている方


おことわり

PowerAppsでゲームを作ることはExcelでゲームを作ることに似ていて、本来の目的とは若干異なるかもしれません。

なぜPowerAppsでゲームを作ってしまったかといえば、勉強がてら作りたいものがゲームだったからだけです。

各種不適切な使い方が多々あるかもしれませんが、まだまだ勉強中の部分もありますので生暖かい気持ちで見ていただければと思います。


こんなゲームができました。

もうすぐクリスマスですね。

サンタさんと一緒に空から降ってくるプレゼントを集めましょう。

悪魔にプレゼントを取られないよう注意してね。

YouTUbeで見る

PowerApps でゲームを作ってみた


以下、解説

ある程度大事な部分だけピックアップして説明します。

また、個人的な覚書を補足として付け加えています。


画面の設定

トップ画面(Top)、ステージ画面(Stage)、結果画面(Result)の3種類作成しました。

image.png


初期設定をOnVisibleで設定する

初期値を予めどこかで宣言する必要がありましたのでScreenのOnvisibleを使いました。

OnVisibleは画面が切り替わったときに発動するプロパティになりますので、TopからStageへ遷移してきたときに実行されるよう、Stage.Onvisibleに設定しました。

今回は以下のような変数を設けました。


Stage.OnVisible

UpdateContext(

{
SantaPosX: 650, // サンタのX座標
SantaSpeed:8, // サンタの移動スピード
presentInterval: 5000, // プレゼントを落下させる間隔
score: 0, // スコア
tremor: 10, // 悪魔とぶつかったときに震える幅
countdownImage:number_3, // カウントダウンの初期画像
countdownNum:3, // カウントダウンの初期数値
countdownSeFLG:false, // カウントダウンの音を出すフラグ
DemonSeFLG:false, // 悪魔とぶつかったときに音を出すフラグ
presentGetSeFLG:false, // プレゼントを取ったときに音を出すフラグ
timeOverSeFLG:false, // 時間切れの時に音を出すフラグ
gameStartFLG: false, // ゲームを開始するときのフラグ
damageFLG: false, // ダメージを受けたときのフラグ
overFLG: false, // 時間切れの時のフラグ
Const: { // 固定値
TimeLimit: 60000, // 制限時間
StagePosX: 300, // ステージのX座標
StageWidth: 800, // ステージの幅
StageHeight: 600, // ステージの高さ
DemonID:4, // 悪魔キャラのID、プレゼントを同時3つまで表示する制限としたため、4つ目を悪魔キャラのIDとした。
PresentIntervalMin:500, // プレゼントの落下間隔の最小値
PresentIntervalSubtraction:500, // プレゼントの落下間隔を少しずつ縮めるための値
PresentFallingSpeed:15 // プレゼントの落下スピード
}
}
);
ClearCollect(
presentImages,
{
id: 1,
image: christmas_cake
},
{
id: 2,
image: christmas_mark3_candy
},
{
id: 3,
image: christmas_mark7_bell
},
{
id: 4,
image: christmas_present
},
{
id: 5,
image: ballon_flower
}
);
ClearCollect(
presents,
{
id: 1,
visible: false,
posY: 0,
posX: 0,
image: ""
},
{
id: 2,
visible: false,
posY: 0,
posX: 0,
image: ""
},
{
id: 3,
visible: false,
posY: 0,
posX: 0,
image: ""
},
{
id: 4,
visible: false,
posY: 0,
posX: 0,
image: character_akuma
}
);


変数に関する覚書

UpdateContextは変数に値を代入する命令です。PowerAppsではhoge=1のような書き方で変数を更新できません。毎回UpdateContextを書く必要があります。ちょっと面倒ですね。

また、Setという命令も変数に代入する命令です。UpdateContextとの違いはVBAでいうところのPrivateとPublicになります。

UpdateContextで指定するとPrivate扱いとなり、宣言したScreen内でしか参照できなくなります。

Setで指定すればどのScreenからでも参照できます。

書き方も若干違いますので覚えるまで迷いますね。


UpdateContextとSetの書き方

UpdateContext({hoge:1000}) // 中括弧でくくること。結構忘れがち。変数と値の間にはコロンを使う。

UpdateContext({hoge:1000,fuga:2000}) // 複数の変数を変更したい場合はカンマで並べる
Set(hoge,1000) // 中括弧要らない、変数と値の間にはカンマを使う。複数変更できない。

Constのように変数にレコードを指定することもできます。

値を取り出すときはConst.DemonIDのように指定します。

また値を更新するときはUpdateContext({Const:{DemonID:1}})のように指定します。

※この書き方をするとUpdateContextの時にサジェストしてくれないので今の段階ではお勧めしません。値を変更しないConstのような使い方には便利かもしれません。

ClearCollectでデータソースが作れます。

今回はプレゼントの画像のデータソースと、プレゼントの情報を管理するデータソースを作成しました。

↓[ホーム]-[コレクション]を確認するとこんな感じになってます。※画像はあらかじめ[メディア]で登録済みです。

image.png

image.png


書式設定に関する覚書

画面上部の関数の入力欄を下に引っ張ると「書式設定」が表示されます。

これを押すと整形できます。フォーマット解除すると元に戻ります。見やすくなりますね。

ただ、If文とかはちょっとわかりづらいです。

image.png

またカーソルで上下に移動するとサジェスト欄にカーソルが移動してしまって思い通りに操作できないときがあります。

このあたりの使い勝手は今後改善されていくんでしょうね。


サンタを動かすコントロールについて

コントロールのスライダー1個と、画像1個を組み合わせます。

スライダー(SantaSlider)のValueを星マーク(Star)のX座標に指定します。するとスライダーの変更とともに星が移動するようになります。

またスライダーは見えなくてもよいので色を透明にしておきます。

この時の注意点としては、スライダーは星より上になるように再配置することです。

星の下に配置するとスライダーを触れなくなります。

image.png

今回はSliderSantaのMaxとMinを10と0に設定し、11段階にしました。

StarもSliderSantaに合わせて11段階で動くようにX座標を以下のように指定します。


Star.X

SantaSlider.Value * (Const.StageWidth/(SantaSlider.Max-SantaSlider.Min + 1)) + Const.StagePosX



サンタの移動について

スライダーの位置によってサンタを左右に動かします。

スライダーを右へ移動させればサンタは右へ、左へ移動させれば左へ移動し続けます。

またスライダーの値によって移動の強弱もつくようにしました。


タイマーコントロールを使う

何かのタイミングで処理を実行したいことは山ほどあります。先ほど紹介したOnVisibleもそのうちの1つですね。

PowerAppsに用意されているプロパティでよく使うのは、ボタンを押したときのOnSelectやスライダーを変更したときのOnChangeです。どちらもユーザが何かアクションを起こさないと発動しないプロパティになります。

しかし、例えばサンタとプレゼントのぶつかりを判定する処理については、ユーザのアクションに関係なく、常に監視するように動いてもらわないといけません。

今回のサンタも、スライダーを動かさないときでも移動してほしいので、OnChangeは合いません。

そこでタイマーコントロールを使います。

タイマーコントロールについているプロパティで利用できそうなのは以下の3つです。

プロパティ
説明

OnSelect
タイマーをクリックした時の処理

OnTimerStart
タイマーが実行を開始した時の処理

OnTimerEnd
タイマーが実行を完了した時の処理

さらにタイマーには実行する時間の長さを指定するDurationと、繰り返しを行うRepeatがあります。

察しの良い方ならわかると思いますが、Durationを小さくし、Repeatを有効にすると短いサイクルでOnTimerStartやOntimerEndがユーザのアクションなしに実行され続けるようになります。

image.png

サンタの動きはスライダーのValueを加算し続けるようにしたいのでタイマー(SantaMotionEvent)を1つ用意し、OnTimerStartに以下のような処理を入れます。


SantaMotionEvent.OnTimerStart

UpdateContext(

{
SantaPosX: Max(
Const.StagePosX, // 画面左端の制限
Min(
Const.StagePosX + Const.StageWidth-Santa.Width, // 画面右端の制限
SantaPosX + (SantaSlider.Value-(SantaSlider.Max-SantaSlider.Min)/2) * SantaSpeed // 移動のメインはここ
)
)
}
)

Durationは10(ミリ秒)に設定しておけば十分でした。


サンタと悪魔がぶつかったときの動作

震えさせて固まるようにしたかったので、こちらもタイマー(DamageTimer)を用意しました。

タイマーのStartプロパティにdamageFLGをセットします。

damageFLGが無効から有効に切り替わったときにタイマーが動きます

サンタと悪魔がぶつったときにdamageFLGを有効にすればタイマーが動き始める仕掛けになります。

image.png

OnTimerEndでdamageFLGを無効にしています。

またタイマーが動き始めて2秒後に止まる算段です。

damageFLGが有効の間、サンタの行動を停止させたいので先ほどのSantaMotionEvent.OnTimerStartにIFを加えて以下のように更新します。


SantaMotionEvent.OnTimerStart

If(

damageFLG,
UpdateContext({tremor: tremor * -1});
UpdateContext({SantaPosX: SantaPosX + tremor}),
UpdateContext(
{
SantaPosX: Max(
Const.StagePosX,
Min(
Const.StagePosX + Const.StageWidth-Santa.Width,
SantaPosX + (SantaSlider.Value-(SantaSlider.Max-SantaSlider.Min)/2) * SantaSpeed
)
)
}
)
)

これでサンタは震えます。


タイマーの発動条件の覚書

タイマーのStartはfalseからtrueに切り替わったときに発動します

trueからtrueに切り替えても発動しません。

UpdateContextでtrueにしても前の状態がtrueのままだったりすることよくあります。

タイマーが発動しない原因はだいたいこれです。

使い終わったらすぐにfalseにしましょう。

今回はOnTimerEndでfalseにしています。


プレゼントを落下させる動作


コレクションを利用

プレゼントの情報はコレクション(presents)で管理することにしました。

各項目は以下のようになります。

プロパティ
説明

id
ユニークなID

image
プレゼントの画像

posX
プレゼントのX座標

posY
プレゼントのY座標

visible
プレゼントが画面に表示されているか

コレクションを更新すればプレゼントの位置や画像も更新されていくわけです。

今回、画面上に同時に表示されるプレゼントを最大4個としましたので、コレクションのデータも4つになっています。

コレクションの値はLookUpを使って取れます。画像のプロパティに以下のように設定しました。


Present1

Image = LookUp(presents,id=1,image)

X = LookUp(presents,id=1,posX)
Y = LookUp(presents,id=1,posY)
Visible = First(Filter(presents,id=1)).visible

※Present2 ~ Present4も同様

※Visibleだけ取り出し方が違いますが、これは勉強のため敢えて違う方法で取り出しただけです。LookUpで問題ありません。

image.png


画像の覚書

最初、画面に同時に表示するプレゼントを6個くらいにしていたのですが、メモリを大量に使うようでブラウザが固まってしまう課題に陥りました。一旦更新すれば直るのですがゲームができなくなってしまったので最終的に4個にしました。


プレゼントの動きもタイマーで管理

プレゼントのY座標を変更するタイマー(PresentFallingTimer)と、プレゼントを落とす間隔を管理するタイマー(PresentIntervalTimer)を用意しました。

PresentFallingTimerで定期的にコレクションのposYを更新します。更新にはUpdateIfが使えます。

画面に表示されているデータだけY座標を更新します。

また、一番下まで落下したら非表示にします。


PresentFallingTimer

OnTimerStart = UpdateIf(presents,visible,{posY:posY+Const.PresentFallingSpeed})

OnTimerEnd = UpdateIf(presents,posY>Const.StageHeight,{visible:false})

プレゼントを取得したときにPresentIntervalTimerのDurationの値を徐々に小さくしていき(後述)、プレゼントを落とす間隔を速くします。

ゲームの後半にはプレゼントがどんどん落ちてくるようになります。


PresentIntervalTimer.OnTimerStart

UpdateContext(

{ // 非表示のプレゼントのidを探します。
targetID: LookUp(
presents,
Not(visible),
id
),
// presentImageのデータソース(全5種類)から画像をランダムに取得するため、imageのidを予め決めます。
randomImageID: RoundUp(
Rand() * 5,
0
)
}
);
UpdateContext(
{ // プレゼントのidが4だった場合は悪魔の画像、それ以外はプレゼントの画像をセットします。
presentImage: If(
targetID = Const.DemonID,
character_akuma,
LookUp(
presentImages,
id = randomImageID,
image
)
)
}
);
// presentsコレクションを更新して画面にプレゼントを表示します。
UpdateIf(
presents,
id = targetID,
{
visible: true,
posX: Const.StagePosX + Rand() * (Const.StageWidth-Present1.Width),
posY: 0,
image: presentImage
}
)


プレゼントや悪魔とのぶつかり判定

ここもタイマー(PresentGetEvent)を用意します。

サンタとぶつかっているプレゼントもしくは悪魔がないかpresentsコレクションの中から探し出します。

コレクションから条件を指定して探すためにFilterを使い、1つ目だけが欲しかったのでFirstを使いました。

当たり判定については適宜。


PresentGetEvent.OnTimerStart

// 判定

UpdateContext(
{
getPresentID: First(
Filter(
presents,
visible,
posY >= Santa.Y-100 && posY <= Santa.Y-100 + 50 && posX > Santa.X-Santa.Width && posX < Santa.X + Santa.Width
)
).id
}
);

If(
Not(IsBlank(getPresentID)), // ぶつかるものがあった場合
If(
getPresentID = Const.DemonID, // 悪魔なら1ポイントマイナス、damageFLG有効
Reset(DemonSeAudio);
UpdateContext(
{
damageFLG: true,
addPoint: -1,
DemonSeFLG: true
}
),
Reset(PresentGetSeAudio); // それ以外は1ポイントプラス
UpdateContext(
{
addPoint: 1,
presentGetSeFLG: true
}
)
);
UpdateContext({score: score + addPoint}); // スコア加算
UpdateContext( // 落下の間隔を速くする
{
presentInterval: Max(
Const.PresentIntervalMin,
presentInterval-Const.PresentIntervalSubtraction
)
}
);
UpdateIf( // ぶつかったプレゼントを消す
presents,
id = getPresentID,
{
visible: false,
posY: 0
}
)
)



処理の覚書

上記の処理でもわかるのですが、いろいろな処理が混ざってますね。ぶつかり判定・スコア計算・スコア集計・落下間隔変更・画像の消去・・・。

それぞれを別のメソッドとして書きたいのですが、そのような書き方が出来るか分かりませんでした。もう少し調査しないといけません。

そもそもPowerAppsはローコーディングで作れることを売りにしているのでこのような課題は優先度が低いのかも知れません。


スコアの表示

スコアによってVisibleを変更しているだけですね。

例えば、3個目のプレゼントのVisibleは以下のようになります。


Score3.Visible

If(Mod(score,5)>=3,true,false)


image.png

点数表示も同様にスコアによって画像を切り替えています。


Score5_1.Image

Switch(RoundDown(score/10,0), 1,number_1,2,number_2,3,number_3,4,number_4,5,number_5) //十の位の画像



BGM,SEの追加

オーディオを使えば音楽を挿入できます。

Startプロパティが有効になれば音が出せます。音を鳴らしたいタイミングでフラグを切り替えればよいだけです。

ただし、タイマーのStartプロパティと異なるのは有効の間、音楽が鳴り続けることです。

タイマーのStartは無効から有効に切り替わったときに発動しました。

オーディオのStartは有効の間、発動します。

同じプロパティ名でも仕様が違うため注意が必要です。

また、音楽を途中で止めて再度最初から流したいときはオーディオをResetする必要があります。

同じ効果音を続けて鳴らしたい場合に使ったりします。

以下は悪魔とぶつかったときの効果音の処理です。


DemonSeAudio

Start = DemonSeFLG && musicFLG

OnEnd = UpdateContext({DemonSeFLG:false});Reset(DemonSeAudio)

image.png


ということで

ゲームを作ったことで様々な知見を得られました。

外部データとの接続までは手を伸ばさなかったので、次回はFlowを使った何かを作ろうと思います。