Power Apps でゲームを作る~ユーザーの操作を受け付ける~ #Power Platformリレー
2021-11-04
azblob://2022/11/11/eyecatch/2021-11-04-powerapps-games-04-000.jpeg

こんにちは!Power Apps のキャンバスアプリの可能性を伝える男、サトハルです。

「全然ローコードじゃないやんけ!」と突っ込まれても聞く耳は持ちません。

Power Apps の可能性を探求するべくブロック崩しを作ってきた本シリーズです。

ここまで「ボールのアニメーション」、「ブロックの配置」、「ボールとブロックの衝突」と順番に作ってきました。

これまでの記事を未読の方は順番通りに読んで頂くことをお勧めします。

今まで作ったものではユーザーからゲーム画面に干渉できず、ただ跳ね返るボールを眺めるだけの虚無です。

そこで、今回はユーザーから操作できるバーを追加し、ブロック崩しの体裁を整えたいと思います。

表記方法について

今回も例によって一つのコンポーネントについてたくさんの プロパティを編集する場合があります。

その時に本記事では「プロパティ名:値, プロパティ 名:値 ・・・」という形式で表記します。

具体的には以下のようになります。

Text:"ブロック崩し",
Width:1048,
Height:233,
X:156,
Y:132,
Align:Center

また、一つのフィールドに大量にロジックを書く場合があります。

その時には以下のように表記します。

Clear(Blocks);
ForAll([1,2,3,4,5] As I_x,
    ForAll([1,2,3] As I_y,
        Collect(Blocks,{
            life:RoundUp(Rand()*3,0) ,
            num:I_y.Value + (I_x.Value-1)*3,
            x:Block_width*(I_x.Value-1)+Block_padding*I_x.Value,
            y:Block_height*(I_y.Value-1)+Block_padding*I_y.Value
            })
    )
);
Navigate(ゲーム画面);

ユーザーが操作するバーを表示する

ユーザー操作に関連するコンポーネントは以下のようになります。

一つ一つ見ていきましょう。

操作エリアコンテナー

要素をまとめるだけでなく、座標系を簡単にするために用意しました。

垂直や水平ではなく普通のコンテナーです。

Width:Parent.Width,
Height:45,
X:0,
Y:650

操作用スライダー

ユーザーの操作をPowerAppsに伝える方法として、今回はスライダーを利用することにしました。

透明にして上からブロックを表示することでそれっぽさを出しています。

Default:(ゲームエリアコンテナー.Width- Self.HandleSize)/2,
BorderColor:RGBA(0, 0, 0, 0) //一番右が0ならなんでもいい,
HandleFill:RGBA(255, 255, 255, 0) //一番右が0ならなんでもいい,
HandleSize:操作用バー四角形.Width,
Max:ゲームエリアコンテナー.Width- Self.HandleSize,
Visible:true,
Width:Parent.Width,
Height:Parent.Height

操作用バー四角形

スライダーと連動して動く四角形です。

こいつの横幅ここで直接変更できるようにしてあります。

X:操作用スライダー.Value,
Width:170 // なんでもいい

左(右)矢印アイコン

装飾なのでなくても良いです。

後述するロジックが伝わりにくいと思ったのでつけてみました。

当然こいつらもスライダーと連動して動きます。

X:操作用バー四角形.X
X:操作用バー四角形.X + 操作用バー四角形.Width - Self.Width

操作エリアコンテナーの中のコンポーネントの表示順は上記図の通りする必要があるので注意してください。

もし順番を入れ替えたい場合にはツリービューから動かしたいコンポーネントの「・・・」⇒「再配列」とクリックした後必要なものを選択してください。

ユーザーの操作するバーとボールの接触ロジックを追加する

毎度おなじみロジック用タイマーに加筆して、ボールと操作バーが接触した時の動作を設定します。

単純にY方向で反転するだけだとユーザーの介入できる余地が少ないので、バーのどの部分に当てたかによってX方向の移動量を変化させるとします。

それに伴って、ゲーム中共通のパラメータが新たに3つ必要になったのでOnStartで設定しておきます。

Set(Block_width,63);
Set(Block_height,25);
Set(Block_padding,8);
//以下が新規追加
Set(Ball_vx0,10);//x方向の速さの基準値
Set(Ball_vx_max,25);//x方向の速さの最大値
Set(Ball_bar_acceleration, 3);//バーの位置と速度変化に対しての影響係数(この値が大きい方が大きく影響する)
~前略~
//  操作バー
If(
    ball_posy + ボール丸.Height > 操作エリアコンテナー.Y,
    If(
        (ball_posx <= 操作用バー四角形.X + 操作用バー四角形.Width) && (ball_posx + ボール丸.Width >= 操作用バー四角形.X) && (ball_posy + ボール丸.Height >= 操作用バー四角形.Y + 操作エリアコンテナー.Y) && (ball_posy <= 操作用バー四角形.Y + 操作エリアコンテナー.Y),
        UpdateContext(
            {
                ball_vy: -Abs(ball_vy),
                ball_vx: Min(
                    Max(
                        ball_vx - Ball_vx0 * Ball_bar_acceleration *(((操作用バー四角形.X + 操作用バー四角形.Width / 2) - (ball_posx + ボール丸.Width / 2)) / 操作用バー四角形.Width),
                        -Ball_vx_max
                    ),
                    Ball_vx_max
                )
            }
        );
    )
);
~後略~

ゲームオーバーの判定と処理を追加する

ボールが到達するとアウトになる範囲にとりあえず四角形を置きます。

Y:操作エリアコンテナー.Height+操作エリアコンテナー.Y
Width:Parent.Width
Height:Parent.Height - Self.Y
Fill:RGBA(238, 204, 204, 1) //好みの色で

ロジック用タイマーに加筆して、ボールがアウトの範囲に入ったらゲームオーバー画面に飛ばすようにします。

~前略~
// アウト判定
If(ball_posy > アウトゾーン四角形.Y,
Navigate(ゲームオーバー画面スクリーン,ScreenTransition.Fade,{LastScore:score})
);

この時、コンテキスト変数である socre を遷移先のページに受け渡しています。

ゲームオーバー画面は非常にシンプルで画面の構成とコンポーネントは以下のようになります。

特筆すべきは最終スコアくらいでしょうか。

先ほど受け渡したコンテキスト変数を用いて以下のようになります。

"Score : " & LastScore

まとめ

「Power Apps でゲームを作る」シリーズ、いかがでしたでしょうか。

ここまで一か月間連載してきましたが、ひとまずはここで終了となります。

お世辞にもクオリティが高いゲームとは言えませんが、Power Apps でもゲームが作れるという可能性は示せたのではないでしょうか。

アニメーションが少ないゲーム(オセロとか)ならもっと簡単に作れた気がします。

逆に言えば Power Apps は不得意分野でもこれだけのものができるというわけですね。

本記事を通して Power Apps の可能性を少しでも感じて頂けたら幸いです。

シリーズリンク

1,Power Apps でゲームを作る~アニメーション編~ #Power Platformリレー

2,Power Apps でゲームを作る~ブロックを自在に並べる~ #Power Platformリレー

3,Power Apps でゲームを作る~ボールとブロックの衝突判定~

4,Power Apps でゲームを作る~ユーザーの操作を受け付ける~ #Power Platformリレー (←今ココ!)

おまけ (OnTimerEnd プロパティの全文 )

//移動する
UpdateContext(
    {
        ball_posx: ball_posx + ball_vx,
        ball_posy: ball_posy + ball_vy
    }
);
//壁判定
If(
    ball_posx < 0,
    UpdateContext(
        {
            ball_vx: -ball_vx,
            ball_posx: 0
        }
    ),
    If(
        ball_posx + ボール丸.Width >= ゲームエリアコンテナー.Width,
        UpdateContext(
            {
                ball_vx: -ball_vx,
                ball_posx: ゲームエリアコンテナー.Width - ボール丸.Width
            }
        )
    )
);
If(
    ball_posy < 0,
    UpdateContext(
        {
            ball_vy: -ball_vy,
            ball_posy: 0
        }
    ),
    If(
        ball_posy + ボール丸.Height >= ゲームエリアコンテナー.Height,
        UpdateContext(
            {
                ball_vy: -ball_vy,
                ball_posy: ゲームエリアコンテナー.Height - ボール丸.Height
            }
        )
    )
);
//ブロック判定
If(
    ball_posy < 200,
    //上下判定
    If(
        //上向きに移動かつ下の端に接触
        ball_vy < 0,
        ClearCollect(
            Y_Hit_Blocks,
            Filter(
                Blocks,
                ThisRecord.life > 0,
                ThisRecord.y + Block_height < ball_posy + ボール丸.Height,
                ThisRecord.y + Block_height > ball_posy,
                ThisRecord.x + Block_width > ball_posx,
                ThisRecord.x < ball_posx + ボール丸.Width
            )
        ),
        //下向きに移動かつ上の端に接触
        ClearCollect(
            Y_Hit_Blocks,
            Filter(
                Blocks,
                ThisRecord.life > 0,
                ThisRecord.y < ball_posy + ボール丸.Height,
                ThisRecord.y > ball_posy,
                ThisRecord.x + Block_width > ball_posx,
                ThisRecord.x < ball_posx + ボール丸.Width
            )
        )
    );
    //左右判定
If(
        ball_vx > 0,
        //右向きに移動かつ左端に接触
        ClearCollect(
            X_Hit_Blocks,
            Filter(
                Blocks,
                ThisRecord.x < ball_posx + ボール丸.Width,
                ThisRecord.x > ball_posx,
                ThisRecord.y < ball_posy + ボール丸.Height,
                ThisRecord.y + Block_height > ball_posy,
                ThisRecord.life > 0
            )
        ),
        //左向きに移動かつ右端に接触
        ClearCollect(
            X_Hit_Blocks,
            Filter(
                Blocks,
                ThisRecord.x + Block_width < ball_posx + ボール丸.Width,
                ThisRecord.x + Block_width > ball_posx,
                ThisRecord.y < ball_posy + ボール丸.Height,
                ThisRecord.y + Block_height > ball_posy,
                ThisRecord.life > 0
            )
        )
    );
    //角にあたった場合の処理
Clear(XY_Hit_Blocks);
    //角にあたったブロックを抽出
ForAll(
        X_Hit_Blocks As X_block,
        ForAll(
            Filter(
                Y_Hit_Blocks,
                ThisRecord.num = X_block.num
            ) As XY_Block,
            Collect(
                XY_Hit_Blocks,
                XY_Block
            )
        )
    );
    If(
        CountRows(XY_Hit_Blocks) > 0,
        //右上向き
        If(
            ball_vx > 0 && ball_vy < 0,
            ForAll(
                XY_Hit_Blocks,
                If(
                    (ball_posx + ボール丸.Width - ThisRecord.x) - (ThisRecord.y + Block_height -ball_posy) >= 0,
                    Remove(
                        X_Hit_Blocks,
                        ThisRecord
                    ),
                    If(
                        (ball_posx + ボール丸.Width - ThisRecord.x) - (ThisRecord.y + Block_height -ball_posy) < 0,
                        Remove(
                            Y_Hit_Blocks,
                            ThisRecord
                        )
                    )
                )
            ),
            //左上向き
            If(
                ball_vx < 0 && ball_vy < 0,
                ForAll(
                    XY_Hit_Blocks,
                    If(
                        (ThisRecord.x + Block_width - ball_posx) - (ThisRecord.y + Block_height -ball_posy) >= 0,
                        Remove(
                            X_Hit_Blocks,
                            ThisRecord
                        ),
                        If(
                            (ThisRecord.x + Block_width - ball_posx) - (ThisRecord.y + Block_height -ball_posy) < 0,
                            Remove(
                                Y_Hit_Blocks,
                                ThisRecord
                            )
                        )
                    )
                ),
                //左下向き
                If(
                    ball_vx < 0 && ball_vy > 0,
                    ForAll(
                        XY_Hit_Blocks,
                        If(
                            (ThisRecord.x + Block_width - ball_posx) - (ball_posy + ボール丸.Height -ThisRecord.y) >= 0,
                            Remove(
                                X_Hit_Blocks,
                                ThisRecord
                            ),
                            If(
                                (ThisRecord.x + Block_width - ball_posx) - (ball_posy + ボール丸.Height -ThisRecord.y) < 0,
                                Remove(
                                    Y_Hit_Blocks,
                                    ThisRecord
                                )
                            )
                        )
                    ),
                    //右下向き
                    If(
                        ball_vx > 0 && ball_vy > 0,
                        ForAll(
                            XY_Hit_Blocks,
                            If(
                                (ball_posx + ボール丸.Width - ThisRecord.x) - (ball_posy + ボール丸.Height -ThisRecord.y) >= 0,
                                Remove(
                                    X_Hit_Blocks,
                                    ThisRecord
                                ),
                                If(
                                    (ball_posx + ボール丸.Width - ThisRecord.x) - (ball_posy + ボール丸.Height -ThisRecord.y) < 0,
                                    Remove(
                                        Y_Hit_Blocks,
                                        ThisRecord
                                    )
                                )
                            )
                        )
                    )
                )
            )
        )
    );
    //衝突結果の反映
If(
        CountRows(Y_Hit_Blocks) > 0,
        ForAll(
            Y_Hit_Blocks,
            Patch(
                Blocks,
                ThisRecord,
                {life: ThisRecord.life - 1}
            );
        );
        UpdateContext(
            {
                ball_vy: -ball_vy,
                score: score + (100 * CountRows(Y_Hit_Blocks))
            }
        );
    );
    If(
        CountRows(X_Hit_Blocks) > 0,
        ForAll(
            X_Hit_Blocks,
            Patch(
                Blocks,
                ThisRecord,
                {life: ThisRecord.life - 1}
            );
        );
        UpdateContext(
            {
                ball_vx: -ball_vx,
                score: score + (150 * CountRows(X_Hit_Blocks))
            }
        );
    )
);
//  操作バー
If(
    ball_posy + ボール丸.Height > 操作エリアコンテナー.Y,
    If(
        (ball_posx <= 操作用バー四角形.X + 操作用バー四角形.Width) && (ball_posx + ボール丸.Width >= 操作用バー四角形.X) && (ball_posy + ボール丸.Height >= 操作用バー四角形.Y + 操作エリアコンテナー.Y) && (ball_posy <= 操作用バー四角形.Y + 操作エリアコンテナー.Y),
        UpdateContext(
            {
                ball_vy: -Abs(ball_vy),
                ball_vx: Min(
                    Max(
                        ball_vx - Ball_vx0 * Ball_bar_acceleration *(((操作用バー四角形.X + 操作用バー四角形.Width / 2) - (ball_posx + ボール丸.Width / 2)) / 操作用バー四角形.Width),
                        -Ball_vx_max
                    ),
                    Ball_vx_max
                )
            }
        );
    )
);

// アウト判定
If(ball_posy > アウトゾーン四角形.Y,
Navigate(ゲームオーバー画面スクリーン,ScreenTransition.Fade,{LastScore:score})
);