2048Power Appsゲームアプリ

【JPPGB #9】Power Appsで2048を作ってみた

2048

国際線の機内で暇つぶしに2048をやっていたら、これPower Appsで作れそうじゃね?と思ったので作ってみました。

この記事はJPPGB #9での登壇内容を含んでいます。登壇資料↓

こちら(2048 - Power Platform Community)からダウンロードして遊べます。

機能

普通の2048のように、画面をフリックまたはスクロールするとパネルが移動します。

トグルでフリックとスクロールを切り替えられるようにしており、横長の場合はスクロール、縦長の場合はフリックがデフォルトです。

スクロールでの横入力が可能なマウスは限られているので、スライダーで横移動ができるようにしています。

ロジック解説

ボード(GalleryBoard)

以下の数式でボードを定義しています。

ClearCollect(
    ColBoard,
    AddColumns(
        Sequence(16),
        "Col",
        Mod(ThisRecord.Value - 1, 4) + 1,
        "Row",
        RoundUp(ThisRecord.Value / 4, 0),
        "Val",
        0
    )
);

グラフにするとこんな感じです。

線形増加するValueの値に応じて鋸型に増減するColと、4ごとにステップ増加するRowのテーブルを作成することで座標を設定します。

Col, Row

操作ボード(GalleryRow & GalleryCol)

GalleryBoardの上にGalleryRowを配置することで、操作はGalleryRowとその中のGalleryCol、ゲームのボードはGalleryBoardというように分けることができます。

GalleryBoardの上に透明のGalleryRowが置かれている

GalleryRowの中にはGalleryColを配置しており、それぞれ縦と横のスクロールやフリックを検知します。

コンポーネントのロジック

ランダム配置(ComponentBoard.RandPut)

リセット時と操作後に実行される2または4をランダムで配置するロジックです。

With(
    {
        Rand:
            FirstN(Shuffle(
                Filter(
                    ColBoard,
                    Val = 0
                )
            ), IsReset + 1).Value
    },
    UpdateIf(
        ColBoard,
        true,
        {
            Val:
                If(
                    Value in Rand && !Only2,
                    RandBetween(1, 2) * 2, 
                    Value in Rand,
                    2, 
                    IsReset,
                    0,
                    ThisRecord.Val
                )
        }
    )
);

RandPutにはOnly2とIsResetの二つのパラメーターがあり、Only2がtrueの場合は2のみが配置され、IsResetがtrueの場合はリセット時の動作(2または4が2つ配置され、それ以外のマスは0になる)をします。

上下左右に動かす(Up, Down, Left, Right)

ForAll(
    Sequence(4) As ColNum,
    With(
        {ThisCol:Filter(ColBoard, Col = ColNum.Value, Val + 0 <> 0)},
        If(
            //ThisColが空白の場合は処理しない
            IsEmpty(ThisCol),
            false,
            //1,2と3,4行目がそれぞれ同じ数
            And(
                IfError(Index(ThisCol, 1).Val = Index(ThisCol, 2).Val, false),
                IfError(Index(ThisCol, 3).Val = Index(ThisCol, 4).Val, false)
            ),
            Collect(ColScore, {Score:Index(ThisCol, 1).Val * 2 + Index(ThisCol, 3).Val * 2});
            UpdateIf(
                ColBoard,
                Col = ColNum.Value && Row = 1,
                {
                    Val:IfError(Index(ThisCol, 1).Val * 2, 0)
                },
                Col = ColNum.Value && Row = 2,
                {
                    Val:IfError(Index(ThisCol, 3).Val * 2, 0)
                },
                Col = ColNum.Value && Row in [3, 4],
                {
                    Val:0
                }
            ),

            //1と2行目が同じ
            IfError(Index(ThisCol, 1).Val = Index(ThisCol, 2).Val, false),
            Collect(ColScore, {Score:Index(ThisCol, 1).Val * 2});
            UpdateIf(
                ColBoard,
                Col = ColNum.Value && Row = 1,
                {
                    Val:Index(ThisCol, 1).Val * 2
                },
                Col = ColNum.Value,
                {
                    Val:IfError(Index(ThisCol, Row + 1).Val, 0)
                }
            ),

            //2と3行目が同じ
            IfError(Index(ThisCol, 2).Val = Index(ThisCol, 3).Val, false),
            Collect(ColScore, {Score:Index(ThisCol, 2).Val * 2});
            UpdateIf(
                ColBoard,
                Col = ColNum.Value && Row = 1,
                {
                    Val:Index(ThisCol, 1).Val
                },
                Col = ColNum.Value && Row = 2,
                {
                    Val:Index(ThisCol, 2).Val * 2
                },
                Col = ColNum.Value && Row in [3, 4],
                {
                    Val:IfError(Index(ThisCol, Row + 1).Val, 0)
                }
            ),

            //3と4行目が同じ
            IfError(Index(ThisCol, 3).Val = Index(ThisCol, 4).Val, false),
            Collect(ColScore, {Score:Index(ThisCol, 3).Val * 2});
            UpdateIf(
                ColBoard,
                Col = ColNum.Value && Row in [1, 2],
                {
                    Val:Index(ThisCol, Row).Val
                },
                Col = ColNum.Value && Row = 3,
                {
                    Val:IfError(Index(ThisCol, 3).Val * 2, 0)
                },
                Col = ColNum.Value && Row = 4,
                {Val:0}
            ),

            //それ以外
            ForAll(
                Sequence(4) As RowNum,
                UpdateIf(
                    ColBoard,
                    Row = RowNum.Value && Col = ColNum.Value,
                    {
                        Val:IfError(Index(ThisCol, RowNum.Value).Val, 0)
                    }
                )
            )
        )
    )
)

もう少しスマートに書けた気はしますが、この長いコードが上に操作した時のロジックです。

途中にスコア計算のロジック(ColScoreに行を追加している箇所)もはさんでいます。

他の方向はアプリの中身を確認してみて下さい。

トグルによる移動検知・動作の数式の実行

トグルを用いて、以下の順番で移動検知・動作の数式を実行しています。

  1. ギャラリーが移動(デフォルトの位置ではなくなる)​
  2. トグルがオンになる​
  3. OnChangeプロパティの数式が実行される​
  4. OnChangeプロパティの最後のReset(ギャラリー)でデフォルトの位置に戻る​

上下移動検知(ToggleRow)

ToggleRow.Defaultは_Def <> GalleryRow.VisibleIndexになっており、GalleryRowがデフォルトの位置から移動した際にトグルがオンになります。

トグルがオンになったときに発動するOnCheckプロパティに以下のコードを書いておくことで、移動した方向によって前述の上下移動のロジックが実行されます。

ClearCollect(ColPreBoard, ColBoard);
If(
    Toggle2.Value,
    If(
        GalleryRow.VisibleIndex > _Def,
        ComponentBoard_1.Down(),
        ComponentBoard_1.Up()
    ),
    If(
        GalleryRow.VisibleIndex < _Def,
        ComponentBoard_1.Down(),
        ComponentBoard_1.Up()
    )
);
ComponentBoard_1.RandPut(false, false);
Reset(GalleryRow);

上下移動後にReset(GalleryRow);を実行することでギャラリーがデフォルトの位置に戻ります。

左右移動検知(ToggleCol)

ToggleRowとほぼ同じロジックです。

ClearCollect(ColPreBoard, ColBoard);
If(
    GalleryCol.VisibleIndex < _Def,
    ComponentBoard_1.Right(),
    ComponentBoard_1.Left()
);
ComponentBoard_1.RandPut(false, false);

この上下左右のスクロールをトグルで検知してロジックを実行するというTipsは、アクションゲームなどの他のゲーム作成時にも使えると思います。(少し不安定な部分もありますが)

こちらの記事で詳しく説明しています

トグルがオフに戻らないバグ?への対応

ギャラリーをリセットしてもトグルがオフにならないバグがたまに発生するため、上下と左右で異なるアプローチで対応しました。

上下:ポップアップ

上下はゲームオーバーと共通化して、ポップアップをクリックする方式にしています。

If(
    //GalleryRowのリセット
    ToggleRow.Value,
    Reset(GalleryRow),
    
    //ゲームオーバーリセット
    ComponentBoard_1.RandPut(true, true);ComponentBoard_1.RandPut(true, true);
    Reset(ToggleRow);
    Clear(ColScore)
)

左右:タイマー

左右のギャラリー(GalleryCol)はギャラリー(GalleryRow)に格納されているためReset関数によるリセットが不可能です。

そのため、タイマーによるリセットの方式にしました。

デバッグにチェックを入れると確認できます

横移動は動作が不安定な時があるので、スライダーやフリック操作で操作した方が確実かもしれません。

マスの色変化

数によって文字色と背景色を変化させています。

//文字色(Color)
With(
    Match(Text(1000000 - ThisItem.Val, "000000"), "(?<R>\d{2})(?<G>\d{2})(?<B>\d{2})"),
    If(
        (((255 - R * 255 / 100) + (255 - G * 255 / 100) + (255 - B * 255 / 100)) / 3) < 127.5,
        Color.Black, Color.White
    )
)

背景色(Fill)
ColorValue(
    "#" &
    With(
        Match(Text(1000000 - ThisItem.Val, "000000"), "(?<R>\d{2})(?<G>\d{2})(?<B>\d{2})"),
        If(Len(Dec2Hex(R * 2.56)) = 1, 0 & Dec2Hex(R * 2.56), Dec2Hex(R * 2.56)) &
        If(Len(Dec2Hex(G * 2.56)) = 1, 0 & Dec2Hex(G * 2.56), Dec2Hex(G * 2.56)) &
        If(Len(Dec2Hex(B * 2.56)) = 1, 0 & Dec2Hex(B * 2.56), Dec2Hex(B * 2.56))
    )
)

以下は2のn乗(1≦n≦50)の色分けです。まだ私は8192に到達していないので、デバッグ不十分です。

色変換はこちらを参考

コメント

タイトルとURLをコピーしました