コルネの進捗や備忘録が記されたなにか

進捗や成果物や備忘録てきななにかを雑に更新していきます。

Power Appsでゲームを作成してみよう! ~テクニック編~


スポンサードリンク

はじめに

07/15(土) 13:30~18:00 にPower Appsゲーム作成コンテストを開催します!

jppgb.connpass.com

以下のようなスケジュールで開催予定で、現在(2023/05/13)は「エントリー & 作成期間」となっています。

エントリーを行われる方は、上記connpassのイベントページより参加申し込みをお願いします。

今回は試験的な開催のため賞品は用意できていませんが、以下のような賞も用意しております。

  • 大賞
  • 技術賞
  • デザイン賞
  • アイディア賞
  • オリジナリティ賞
  • エンターテインメント賞

皆様の気軽な参加お待ちしております!
きっちりしたゲーム以外にも、ネタ全振りなゲームもお待ちしております!!

さて、今回はゲーム作成コンテストを開催するにあたり、より多くの人にPower Appsでのゲーム作成を楽しんでもらうために、ゲーム作成に使えるテクニックをいくつか紹介していこうと思います。

ループ処理

タイマーによるループ処理

Power Appsでのループ処理というと、恐らく最もメジャーな方法です。

タイマーコントロールを繰り返し実施させて、ループを実現する方法ですね。
処理はタイマーコントロールOnTimerStartOnTimerEnd に記載します。

タイマーを繰り返し実行させるので Repeattrue に設定します。

Timer.Repeat

true

続いてループの実行間隔を指定します。
Duration に設定した時間([ms])、タイマーコントロールは実行されることになるので、ここのプロパティに設定した時間間隔に従ってループ処理が実行されます。

例えば、1[ms]間隔でループ処理を実行するようにしたい場合は以下のように設定します。

Timer.Duration

1

最後にループ中に実行したい処理を OnTimerStart またはOnTimerEnd のいずれかに記載します。

Timer.OnTimerStart or Timer.OnTimerEnd

UpdateContext({num: num + 1})

この設定により、タイマーコントロールを選択するとループ処理が実行され、1[ms]おきに num という変数に値が1ずつ加算されていきます。
タイマーコントロールを再度選択するとループが止まります。

ボタンを押したときや何らかのアクションによってループを開始したい場合はタイマーを開始するための変数を用意し、タイマーの Start にその変数を設定する必要があります。

例えばボタンを押したときにタイマー(ループ)を開始させたい場合は以下のように設定します。

Button.OnSelect

UpdateContext({isTimerStart: true});

Timer.Start

isTimerStart

タイマー(ループ)を止めたい場合は先ほどの変数を false に設定してあげます。

Button.OnSelect

UpdateContext({isTimerStart: false});
If(
    !isTimerStart,
    Reset(Timer1)
)

Reset() 処理を行っているのは、次のループでも同じ間隔でループを実行させるためです。

たとえば Duration を 1000 に設定していて、ループが 500[ms] 経過した際に中断された場合、次のループ開始は最初だけ 500[ms] でループが実行されます。
これを避けるために、ループを止める際にタイマーを一緒にリセットしています。

ループのスタートとストップを同時にボタンで操作したい際は以下のようになりますね。

Button.OnSelect

UpdateContext({isTimerStart: !isTimerStart});
If(
    !isTimerStart,
    Reset(Timer)
)

以上がタイマーコントロールを利用したループの設定方法です。

このタイマーコントロールを利用したループは比較的メジャーなループ実装方法で、設定やロジックも簡単なものではありますが、いくつか考慮が必要なことがあります。

  • ループの最小間隔は1[ms]
  • ネットワークやアプリ上に配置したコントロール数によっては設定した間隔以上のループ間隔となる

2つ目の考慮事項はタイマーコントロールに限った話でもないのですが、タイマーによるループは(体感)アプリが重くなる率が高いような気がします。(計測したわけでもなく、あくまで体感です。)
タイマーを複数同時に実行させたときなんかが如実に感じますかね。

Power Appsでは多くのコントロールを配置したり、コレクションなどで多くのデータを保持するような設計にするとアプリが重くなってしまいます。

それによりタイマーコントロールまで重くなってしまうと、例えば1[ms]間隔でループが実行されるように設定したのに、実際は10[ms]間隔でループが動いてしまっているー。
ということも起きてしまいます。

従って、あまりここで設定した間隔にしがってループが実行されることを想定した設計にはしないことをおすすめします。

スライダーによるループ処理

Power Appsでのループ処理のやり方としては、スライダーを利用したループ処理もあります。

こちらでたく丸さんが紹介してくださっているやり方ですね。

qiita.com

私がPower Appsでループ処理を実行する際はこのやり方をよく利用します。

このやり方は文字通りスライダーを利用してループを実行する方法です。

簡単に理論を説明すると、

  1. スライダーの Default変数-A を設定
  2. スライダーの OnChange変数-A の値を変更
    • これによりスライダーの値が変更されるので、再度 OnChange が実行される

と、このような形式でスライダーが OnChange にてスライダー自身に設定された値を変更することで再度 OnChange が呼ばれてループ処理が行われる。
といった仕組みです。

察しの良い方はお気づきかもしれませんが、こちらループを止める処理をいれないと大変なことになるので気を付けてください。

スライダーの OnChange はタイマーのループとは違い画面を編集中でも実行されます。
よって、記載内容によっては永遠処理が実行されて最悪アプリが落ちます。

この処理を利用する際は緊急停止ボタンをつけ忘れないようにしてくださいね。

さて、肝心の設定方法について説明します。

まずはスライダーに設定する値と、ループの実行を判断する変数を用意します。

UpdateContext({sliderVal: 0});
UpdateContext({isSliderStart: false});

次にスライダーに変数を設定します。

Slider.Default

sliderVale

スライダーに設定した変数を OnChange で変更してあげましょう。
このとき、ループ実行状態の時だけ値を変更するようにします。

Slider.OnChange

If(
    isSliderStart,
    /*
        ここに処理を記載する。
    */
    UpdateContext({sliderVal: sliderVal + 1})
)

コメントでも記載している通り、"ここに処理を記載する。"とコメントを入れている箇所にループ時に実行したい処理をいれます。

ループのStart及びStopは以下のような処理で行います。

Button.OnSelect

UpdateContext({isSliderStart: !isSliderStart});
If(
    isSliderStart,
    Reset(Slider)
)

今回は Reset() 関数を実行することでループを開始しています。
Reset() 関数を実行すると OnChange も実行されるからですね。

スライダーによるループはタイマーとは違いループの実行間隔もないのでループを止めた際に特段何もする必要はないですね。

こちらのスライダーによりループ処理のメリットとしては

  • タイマーコントロールより短い間隔でループ処理を実行可能
  • タイマーコントロールによるループを行うより負荷が少ない(要検証)

という点が挙げられます。

ただし冒頭でも説明した通り、記載を誤ってしまうと大変なことになってしまうので気を付けましょう。

N × Mの盤面を作成する

ギャラリーの折り返しを活用する

こんな感じのボードゲームとかで利用される盤面の作成方法です。

これはギャラリーの折り返し( WrapCount )を利用して作成することができます。

ギャラリーは「空の垂直ギャラリー」を利用しています。

例えば画像のように10 × 10の盤面を作成したいのであれば、まずギャラリーの Items にて各マスの要素が存在するテーブルを設定します。

Gallery.Items

Sequence(10 * 10)

正方形としたい場合はギャラリーの幅と高さを合わせてあげればいいですね。

Gallery.Height

Self.Width

今回X成分(横方向)の折り返しを10に設定したいので折り返しの数を10に設定します。

Gallery.WrapCount

10

Y成分(縦方向)には10行設定したいので、以下のようにサイズを指定します。

Gallery.TemplateSize

Self.Width / 10

計算が面倒なのでテンプレートのパディングは0に設定しておくことをおすすめします。

Gallery.TemplatePadding

0

後はマスを構成するコントロールをギャラリー内に設定するだけですね。

例えばラベルを設定する場合は以下のようになります。

Label.X

(Parent.TemplateWidth - Self.Width) / 2

Label.Y

(Parent.TemplateHeight - Self.Height) / 2

Label.Width

Parent.TemplateWidth - 5

Label.Height

Self.Width

ラベルが引っ付いてしまわないように少し小さめにサイズを設定して、中央に描画されるように設定しています。

この方法だと、 WrapCount に設定可能な最大の数値が10のため、10列以上の盤面を作成できないというのがデメリットとなります。

ギャラリーのTemplateSize = 0を活用する

先ほどの制限を解消する方法として TemplateSize を0に設定してコントロールを配置する方法があります。

この方法では、ギャラリーの Items にてマスの座標系の値を定義してコントロールに設定するということを行っているので、NやMの値に特に制限はありません。
強いてあげるのであれば、Power Appsのコントロール数の制限とかですかね。

TemplateSize を0に設定するので以下のプロパティたちにはそれぞれ以下のように設定してください。

Gallery.WrapCount

1

Gallery.TemplateSize

0

Gallery.TemplatePadding

0

設定してみて気づいた方もおられるかもしれませんが、 TemplateSize = 0 に設定してもプロパティペインでは1に設定されてしまいます。

ここのずれが後々面倒になってくるので、 TemplateSize は0ではなく1になっている。というということを念頭に置いて次の説明を見ていってください。

さて、冒頭でも説明した通りこの方法を利用する場合は、 Items で各マスの座標系を定義してあげます。

例えば12 × 12の盤面で、幅と高さ45のマスを定義する際は、XとYは以下のような式で導き出すことができますね。

Gallery.Items

With(
    {
        N: 12,
        M: 12,
        width: 45,
        height: 45
    },
    ForAll(
        Sequence(N * M),
        {
            Value: ThisRecord.Value,
            X: width * Mod(ThisRecord.Value - 1, N),
            Y: height * RoundDown((ThisRecord.Value - 1) / N, 0),
            Width: width,
            Height :height
        }
    )
)

ここの値をもとにコントロールに値を設定していくのですが、そのままの数値を設定してしまうと以下のようにずれてしまいます。

これは TemplateSize が0ではなく1になってしまっているのが原因です。
各要素が1ずつずれてきてしまっているわけですね。

したがって、Yにはこれを補正するための式を設定してあげます。
他の座標系のプロパティはそのままの値を設定してOKです。

X

ThisItem.X

Y

ThisItem.Y - (ThisItem.Value - 1)

Width

ThisItem.Width

Height

ThisItem.Height

これで、大きい盤面も作成することができます。

SVGで作成する

最後にSVGで盤面を描画する方法です。

この方法であれば、盤面の描画に画像コントロール1つで済むためアプリの負荷が少なく済みます。
ただし、どのマスを選択したのか?を判断できない点に気を付けてください。

ただ単にデザインとして盤面を描画したいときに利用することになります。

このやり方は説明がめんどくさい割と上級者向けなので、細かい解説はなしでサンプルコードだけ置いておきます。
以下のようなものが作成できます。

Image.Image

With(
    {
        N: 12,
        M: 12,
        width: 45,
        height: 45
    },
    "data:image/svg+xml,"& 
    EncodeUrl(
        "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>" &
            // 縦線
            Concat(
                ForAll(
                    Sequence(N - 1),
                    "<line
                        x1='" & ThisRecord.Value * width & "' 
                        y1='0' 
                        x2='" & ThisRecord.Value * width & "' 
                        y2='" & Self.Height &"' 
                        stroke='#0f0f0f' 
                        stroke-width='2'
                    />"
                ),
                ThisRecord.Value
            ) &
            // 横線
            Concat(
                ForAll(
                    Sequence(M - 1),
                    "<line
                        x1='0' 
                        y1='" & ThisRecord.Value * height & "' 
                        x2='" & Self.Width & "' 
                        y2='" & ThisRecord.Value * height & "' 
                        stroke='#0f0f0f'  
                        stroke-width='2'
                    />"
                ),
                ThisRecord.Value
            )
        & "</svg>"
    )
)

Image.Width

45 * 12

Image.Height

45 * 12

選択された座標を取得する

こちらで以前紹介したやり方ですね。

koruneko.hatenablog.com

ギャラリーに評価コントロールを配置して、選択された評価コントロールでY成分を、選択された評価コントロールの評価値でX成分を取得しています。

思ったより説明ちゃんと書いてあったし、更新する内容もなさそうなのでやり方は上記記事をご確認ください。

強いて補足するなら、評価コントロールは透明に設定しましょう。ぐらいですかね。
非表示だと選択できなくなるのでダメです。

文字を1文字ずつ表示させる

こちらも以前紹介した内容ですね。

koruneko.hatenablog.com

ノベルゲーやセリフ、文章を表示させる際に利用できるテクニックです。
こちらも改めて記載しなおす程の内容でもないので割愛。

余談ですが、上記記事を投稿した時期は他にもPower Appsでアニメーションを付ける方法を紹介していたのでよかったらみてみてくださいね。

当たり判定を作成する

当たり判定は私がカスタム関数を作成して公開していますので是非こちらを利用していただければと思います!

github.com

利用できる判定は

  • 四角形 × 四角形
  • 円 × 円
  • 四角形 × 円

の3つですね。

利用方法については(英語ですが)上記GitHubで紹介している他、以下記事でロジックの解説と利用方法の説明も行っています。

koruneko.hatenablog.com

円の当たり判定はちょっと計算めんどくさいですが、よかったらみてみてくださいね。

特定の条件になったかどうかを判断する

当たり判定やゲームのクリア条件、もしくはゲームオーバーの条件を達成したことを判断する場合は切り替えコントロール(トグルコントロール)を利用します。

トグルコントロールDefault に条件を記載し、トグルが true になったらその条件を達成した。
と判断できますね。

例えば col というコレクションのOKフィールドで true となっているレコードの数が5より大きければゲームクリア。
と判断したい場合は以下のように設定すればいいですね。

Toggle.Default

CountIf(col, OK) > 5

Toggle.OnCheck

// クリアしたときに行う処理

自キャラを操作する

ボタン操作によってPlayerが操作可能なキャラクターなどを上下左右に動かす方法です。

簡単な操作であれば、ボタンなどが押されたときにキャラクターに設定している座標の変数を変更させればいいですね。

OnSelect

UpdateContext({playerY: playerY + 5});

ただこれだと、長押しに対応できないです。
操作するのに毎回毎回選択しないといけないのって操作性てきにいまいちですよね。

ボタンコントロールには Pressed というプロパティが存在しています。
これはボタンが押され続けている間は true を返してくれます。

これを活用して、ボタンが押されている間もキャラクターが動くようにします。

まずは上下左右ボタンとしてボタンを4個用意します。

今回の例では

上ボタン = UpBtn
下ボタン = DownBtn
右ボタン = RightBtn
左ボタン = LeftBtn

として用意しました。

これらのボタンのプロパティ変更は不要です。

ボタンが押されているとき繰り返し処理を行うのでループ機能が必要です。

今回はスライダーを使ってループを行わせます。

IniBtn.OnSelect

UpdateContext({sliderVal: 0});
UpdateContext({isSliderStart: false});
// Playerの座標
UpdateContext({playerX: 600, playerY: 200})

Slider.Default

sliderVal

Slider.OnChange

// 上
If(
    UpBtn.Pressed && isSliderStart,
    UpdateContext({playerY: playerY - 5});
    UpdateContext({sliderVal: sliderVal + 1})
);
// 下
If(
    DownBtn.Pressed && isSliderStart,
    UpdateContext({playerY: playerY + 5});
    UpdateContext({sliderVal: sliderVal + 1})
);
// 右
If(
    RightBtn.Pressed && isSliderStart,
    UpdateContext({playerX: playerX + 5});
    UpdateContext({sliderVal: sliderVal + 1})
);
// 左
If(
    LeftBtn.Pressed && isSliderStart,
    UpdateContext({playerX: playerX - 5});
    UpdateContext({sliderVal: sliderVal + 1})
);

ループ実行状態かつそれぞれのボタンが押されているときだけ処理を実行して、各々変数を更新しています。

さて、ループ実行状態(isSliderStart = true)とする処理ですが、これはトグルコントロールを利用します。

ボタンのいずれかが選択状態であればトグルをチェック状態にしてループを開始し、チェック状態が外れたら、ループを止めるような設定にします。

Toggle.Default

UpBtn.Pressed || DownBtn.Pressed || RightBtn.Pressed || LeftBtn.Pressed

Toggle.OnCheck

UpdateContext({isSliderStart: true});
Reset(Slider)

Toggle.OnUnCheck

UpdateContext({isSliderStart: false})

これでボタンが押されている間キャラクターを動かし続けることができます。
応用としてボタンを押し続けている間処理を繰り返し実行させる。ということもできますね。

各ボタンの OnSelect になにも処理を記載していないのは、これは OnSelect が呼ばれるタイミングがボタンを離したときだからですね。
これでは長押しのタイミングでループを発火させることができません。

横スクロール機能を作成する

ループ機能との組み合わせですね。

まずはループ機能を作成します。
ここではスライダーを利用します。

InitialBtn.OnSelect (初期化用ボタン)

UpdateContext({sliderVal: 0});
UpdateContext({scrollVal: 0});
UpdateContext({isSliderStart: false});

LoopBtn.OnSelect

UpdateContext({isSliderStart: !isSliderStart});
If(
    isSliderStart,
    Reset(Slider)
)

scrollVal という変数を作成しています。
これが今どれだけスクロールさせているか?を表すための変数です。

Slider.Default

sliderVal

Slider.OnChange

If(
    isSliderStart,
    UpdateContext({scrollVal: scrollVal + 10});
    UpdateContext({sliderVal: sliderVal + 1})
)

ループの度に scrollVal を10ずつ加算しています。
ここの加算値が大きければ大きいほど一度に動く量が増えます。

この値を使って、コントロールを右から左にスクロールさせてみましょう!

例えばコントロールが画面の左端まで移動したら右端からまたスクロールさせるようにするには以下のように設定します。

X

Mod(100 - scrollVal, Parent.Width + Self.Width) - Self.Width

ギャラリーやSVGに描画している場合はX成分の値を変化させてあげればいいですね。

おわりに

これらの技術などを駆使して私が過去に作成したゲームで作成方法を公開しているのは以下です。
実際にゲームとしてできているところを見たい!という方は参考にしていただければ。
(ボンバーマンのやつはここでは割愛してます。)

koruneko.hatenablog.com

koruneko.hatenablog.com

koruneko.hatenablog.com

koruneko.hatenablog.com

koruneko.hatenablog.com

それでは皆様のご参加お待ちしております!

jppgb.connpass.com


スポンサードリンク