- はじめに
- レコーディング動画
- ブロック崩しゲームを作成する
- おわりに
はじめに
この記事はJPPGB #3でのハンズオン資料です。
ハンズオンではPower Appsでブロック崩しを作成します。
レコーディング動画
当日行われたハンズオンの模様はこちらにアップロードされています。
* 一部編集はしていますが、ほぼノーカットです。時間も長いので適宜倍速などで再生していただければ。
ブロック崩しゲームを作成する
初期化用ボタンを作成する
まず最初に変数やコレクションなんかの初期化を行うためのボタンを用意します。
画面内にボタンを追加して、"InitialBtn"と名前を変更してください。
わかりやすくボタンのテキストは「ini」とか「初期化」とかつけておくとわかりやすいですね。
この初期化用ボタンはScreen.OnVisible
で実行したいような処理を記載するためのもの。と思ってください。
このような初期化ボタンを作成するメリットとしては大きく2つあります。
- 開発中に
Screen.OnVisible
を実行させるために一々画面遷移を行わずに済む - 処理内で同様の初期化処理を行いたいときに
Select(InitialBtn)
みたいな記載で済む
開発をちょっと楽にするテクニックみたいなものですね。
この"InitialBtn"はOnVisible
で実行させたいので、OnVisible
には以下の式を設定しておいてください。
Screen.OnVisible
Select(InitialBtn)
バーを作成する
こんな感じのプレイヤーが操作してボールを落とさないようにするためのバーをまずは作成します。
バーの要素は1つに纏めたいので、コンテナを用意してその中にスライダーと図形の四角形を追加してください。
コントロール名は画像のものと揃えておくと、後々記載する式をコピペで利用できるので揃えておくことをおすすめします。
バーの動作イメージとしては、プレイヤーはスライダーを操作することによってバーを動かせるような形にします。
バーの見た目はスライダーのレールみたいな丸いやつじゃなく、ちゃんと四角形のバーにしたいので、そのために図形の四角形を用います。
プレイヤーは(透明にする予定なので最終的にはみえなくなりますが)実際に触って操作することになるのはスライダーなので、必ずスライダーが四角形よりも上にくるように配置に気を付けてくださいね。
バーの位置を決める
バーをどこに配置するのかまずは設定します。
このバーはプレイ領域を画面いっぱいにするのであれば、
可動域 = 画面幅
となりますね。
また、このバーは画面の下の方に設定することになるのでそれに合わせて、画面下のほうに配置されるよう式を設定します。
高さはバーに用いたい高さを適当に設定します。
上記を踏まえ式にすると以下のようになりますね。
BarCtn.X
0
BarCtn.Y
Parent.Height - Self.Height - 50
BarCtn.Width
Parent.Width
BarCtn.Height
20
コンテナ内に配置したコントロールたちもコンテナのサイズに合わせて調整してあげましょう。
Slider.X
0
Slider.Y
0
Slider.Width
Parent.Width
Slider.Height
Parent.Height
RectangleBar.X
0
RectangleBar.Y
Slider.Y
RectangleBar.Width
150
RectangleBar.Height
Slider.Height
四角形のX座標はあとで設定します。
幅は適当な幅を設定しておいてください。
スライダーの操作によってバーの位置が変化するように設定する
スライダーの値が、バー(四角形)のX座標の値となるように設定します。
スライダーの最大値は現在デフォルトの100に設定されているかと思いますが、画面幅の値を最大値にしたいと思います。
こうすることで
スライダーの値 = スライダーのハンドルのX位置
となりますね。
ここでワンポイント。
スライダーの最大値は上記の文章通り、Parent.Width
でもいいのですが、これでは仕様変更によってスライダーの幅を変更したときに、修正箇所が増えてしまいます。
確かに、スライダーの最大値 = 画面幅に要件としてはしたいのですが、突き詰めるとスライダーの最大値はスライダーの右端のX座標としたいですし、最小値は左端のX座標となるように設定したいですよね。
これを考慮して式を設定してあげると、以下のようになりますね。
Slider.Max
Self.Width + Self.X
Slider.Min
Self.X
デフォルト値(既定)はゲーム開始時のバーの初期位置になります。
お好きな値を設定してあげてください。
例えば中心にしたいのであれば以下のようになりますね。
Slider.Default
(Self.Max - Self.Min) / 2
続いてスライダーの値を四角形のX座標に設定するのですが、このとき意識しておかなくてはならない点として、単純にスライダーの値をバーのX座標に設定してしまいますと、バーが中心からずれてしまうので、バーの中心がスライダーのハンドルの位置にくるように設定してあげる必要があります。
これを考慮してX座標を設定すると以下のようになるのですが、
RectangleBar.X
Slider.Value - Self.Width / 2
これだとこんな感じで画面端に来た時にバーが見切れちゃいます。
このずれを補正するために、スライダーの最大値や可動域を考慮して式を設定してあげる必要があります。
RectangleBar.X
Slider.Value / Slider.Max * (Parent.Width - Self.Width)
これによって、スライダーの値が最小もしくは最大にいくにしたがってX位置が補正されますね!
スライダーをみえなくする
スライダーはゲームをプレイする上でプレイヤーには表示させなくてもよいコントロールなので透明にします。
非表示ではなく透明ですね。非表示にしちゃうとスライダーが触れなくなっちゃいます。
透明にする箇所は以下です。
- RailFill
- ValueFill
- HandleFill
- BorderColor
透明にするには上記4か所の設定値を
RGBA(0, 0, 0, 0)
に変更します。
もしくはカラーパレットを開いて赤枠の箇所を選択します。
透明にするだけでなく、スライダーの値の表示も邪魔なのでこれも非表示にしちゃいましょう。
Slide.ShowValue
false
これでバーの完成です!
余談ですが、このバーを応用することで自作のスクロールバーを作成することもできちゃうのです。
興味があればまたみてみてください。
ボールを表示させて画面端で跳ね返るようにする
続いてブロックを崩すためのボールの作成です。
ボールを用意する
ボールには図形の円を利用します。
名前はそのまま"Ball"にでもしておきます。
このボールは正円としたいので、幅と高さは揃えておきます。
サイズはお好きなサイズにしてください。
Ball.Width
Self.Height
Ball.Height
30
ボールのXやYの初期位置は初期化ボタンで宣言しましょう。
バーの右端あたりにくるように設定しようと思います。
1点注意するとすれば、バーはコンテナの中にあるので、コンテナの座標も加味しないと、画面上でのバーの位置は算出できないということですね。
InitialBtn.OnSelect
// ボールの座標の初期化 UpdateContext({ball_X: BarCtn.X + RectangleBar.X + RectangleBar.Width - Ball.Width}); UpdateContext({ball_Y: BarCtn.Y + RectangleBar.Y - Ball.Height});
ボールのX、Yにはこの初期化した変数を割り当てておきます。
Ball.X
ball_X
Ball.Y
ball_Y
ここまでの設定で今画面としては、こんな感じになっているかと思います。
ボールを動かす
ボールを動かすために先ほど初期化したball_X
とball_Y
の値をループ処理によって変化させます。
ループ処理はタイマーを使ってもいいのですが、ループ間隔をより短くしたい(ループを早くしたい)のでスライダーを利用してのループ処理を行います。
スライダーによるループ処理はこちらの記事を参考にしてください。
ループを行うためのスライダーと、ループ開始用のボタン、ループの設定をミスしたときに緊急停止するようのボタンを追加します。
次にスライダーでのループ処理を行うにあたり必要な変数を用意します。
初期化用ボタンに以下を追加します。
InitialBtn.OnSelect
// ループ設定の初期化 UpdateContext({loopVal: 0}); UpdateContext({isLoop: false});
スライダーを使ってのループは簡単に説明すると、スライダーのOnChange
にてスライダーの値に設定した変数を更新することで、スライダーの値が変更されるので、またスライダーのOnChange
が呼ばれて・・・といった感じで行うループ処理です。
ただこのままだと無限ループに陥ってしまうので、ループを止めるようの処理が必ず必要になってくるので気を付けましょう。
忘れるとアプリ落ちちゃいます。
上記を踏まえたうえで式を設定していくと、各プロパティは以下のようになります。
LoopControlBtn.OnSelect
UpdateContext({isLoop: !isLoop}); If( isLoop, Reset(LoopSlider) )
LoopControlBtn.Text (* これは任意)
If( isLoop, "Stop", "Start" )
GameStartBtn.OnSelect
UpdateContext({isLoop: true}); Reset(LoopSlider);
LoopSlider.Default
loopVal
LoopSlider.OnChange
If( isLoop, /* ここに処理を記載! */ // ループ継続 UpdateContext({loopVal: loopVal + 1}); )
ここまでの設定が完了しましたら、アプリの保存を行ってからループ開始用ボタンを押してループを開始してみてください。
上手く設定できていれば、スライダーの値がだんだん増えていくはずです。
ループを止める際は、ループ制御用のボタンを押してループを止めてあげます。
これでループが止まらなかった場合は、設定どこかミスしているので設定を見直してください。
* ループが止まらず、アプリが重くなってしまった場合はスライダーを消すかアプリを開きなおして下さい。
さて、これでループを行うための準備ができましたので早速ボールを動かしていきます。
といってもここまで設定できていれば難しいことはなく、ループ内でボールの座標に設定している変数を加算/減算していくだけです。
ここでどれだけ加算/減算するかによって、ボールのスピードは決まってきます。
初期化用ボタンに以下式を追加して、ボールの速度用変数を宣言しましょう。
InitialBtn.OnSelect
// ボールの速度の初期化 UpdateContext({ball_X_Speed: 3, ball_Y_Speed: 3});
ループ処理内でボールの速度に従って、ボールの座標を変化させます。
LoopSlider.OnChange
If( isLoop, // ボールを動かす UpdateContext({ball_X: ball_X + ball_X_Speed}); UpdateContext({ball_Y: ball_Y - ball_Y_Speed}); // ループ継続 UpdateContext({loopVal: loopVal + 1}); )
これで、ボールを動かすことができました!
画面端にいったらボールが跳ね返るようにする
ボールを動かせるようにはできましたが、このままだとボールは画面を超えてもただひたすら右上方向に動いて行ってしまいます。
ボールは壁(画面端)にぶつかったときに跳ね返るようにしたいので、その処理をループ内で追加します。
反射の処理は簡単に実装します。
原理としては、左右の壁にぶつかった際はボールのX方向の速度の正負を反転させて、上の壁にぶつかった際はボールのY方向の速度の正負を反転させます。
下の壁にあたっときはゲームオーバーなので、ループを止める(looVal
の加算をしない)処理が必要ですね。
これを式にすると以下のようになりますね。
LoopSlider.OnChange
If( ball_Y < Parent.Height && isLoop, // ボールを動かす UpdateContext({ball_X: ball_X + ball_X_Speed}); UpdateContext({ball_Y: ball_Y - ball_Y_Speed}); // 壁に当たった際の処理 If( ball_X + Ball.Width >= Parent.Width, UpdateContext({ball_X: Parent.Width - Ball.Width}); UpdateContext({ball_X_Speed: ball_X_Speed * -1}) ); If( ball_X <= 0, UpdateContext({ball_X_Speed: ball_X_Speed * -1}) ); If( ball_Y <= 0, UpdateContext({ball_Y_Speed: ball_Y_Speed * -1}) ); // ループ継続 UpdateContext({loopVal: loopVal + 1}); )
これでボールが壁にぶつかったら反射できるようになりましたー
ゲーム開始前にバーを動かすとボールもそれに追従して動いてくれるようにする
今の処理のままですと、ゲーム開始前にバーを動かすとボールが置き去りになってしまいます。
これをちゃんとバーを動かすことでボールが追従されるように設定していきます。
バーを動かすことでボールの座標を変えたいので、バー用のスライダーのOnChange
にボールの座標用変数を更新する処理を追加します。
ただここで素直に式を追加してしまうと、ループ中にボールを動かしている処理にも影響を及ぼしてしまいます。
なのでここでボールの座標を更新するのは、ループ処理が行われていないときだけに限定しましょう。
Slider.OnChange
If( !isLoop, UpdateContext({ball_X: BarCtn.X + RectangleBar.X + RectangleBar.Width - Ball.Width}); UpdateContext({ball_Y: BarCtn.Y + RectangleBar.Y - Ball.Height}); )
さて、ここまで設定してもらいましたがこの処理のままでは動きが若干不自然化と思います。
バーを動かし終わったらボールの座標が変わるようになっていますね。
これはスライダーのOnChange
は、ユーザがスライダーを離したときに実行されるからですね。
よって、ボールの座標はループ前であればバーの座標を、ループ中であればボールの座標を設定した変数をそれぞれ利用するようにします。
Ball.X
If(isLoop, ball_X, BarCtn.X + RectangleBar.X + RectangleBar.Width - Ball.Width)
Ball.Y
If(isLoop, ball_Y, BarCtn.Y + RectangleBar.Y - Ball.Height)
これでボールの初期位置をバーを動かすことによって決めることができるようになりましたね。
ボールがバーにぶつかったら跳ね返るようにする
ボールとバーがぶつかったら跳ね返るようにするには、当然ではありますがボールとバーがぶつかったことを判定する必要があります。
ということで当たり判定を実装する必要があるのですが、これは私が以前作成して公開している当たり判定を行うカスタム関数(コンポーネント)を利用してください!
上記GitHubのリポジトリに"solution"内にあるmsappファイルを自身の環境で開いて保存してください。
利用方法はREADME.mdで記載していますが、難しいことはありません。
当たり判定を行いたいそれぞれのオブジェクトの座標やサイズを関数に渡すだけです。
これで接触していればtrue
、接触していなければfalse
を返します。
このカスタム関数では、
- 四角形 × 四角形
- 円 × 円
- 四角形 × 円
の当たり判定をサポートしています。
こちらのカスタム関数の中身を知りたい方は、こちらで解説を行っていますので暇なときにみてみてください。
コンポーネントを環境内に追加したら、アプリにコンポーネントを追加しましょう。
コンポーネントをインポートすると、ライブラリ コンポーネントという項目が追加されますので、ここから画面にコンポーネントを追加します。
このコンポーネントはユーザには見えなくていいので非表示にしておきましょう。
さて、この関数を利用するとボールとバーが当たったときにtrue
を返すようにできるので、ループ内の条件処理に追加してもいいのですが、当たった瞬間に動作させたいとなるともっと適切なコントロールがあります。
そう!トグル(切り替え)ですね。
トグルのDefaultに関数にて当たり判定の関数を設定し、OnCheckにて当たったときの処理を記載します。
当たり判定の関数はトグルとセットで使うことが主になってくると思います。
トグル(切り替え)を追加して、名前を"BallBarCollision"に変更してください。
プロパティでは以下を設定します。
BallBarCollision.Default
CollisionUtils_1.SquareCircle( BarCtn.X + RectangleBar.X, BarCtn.Y + RectangleBar.Y, RectangleBar.Width, RectangleBar.Height, ball_X + Ball.Width / 2, ball_Y + Ball.Height / 2, Ball.Width / 2 )
BallBarCollision.OnCheck
UpdateContext({ball_Y: BarCtn.Y + RectangleBar.Y - Ball.Height}); UpdateContext({ball_Y_Speed: ball_Y_Speed * -1});
BallBarCollision.Visible
false
これで、ボールとバーがぶつかったときにボールが跳ね返るようになりました!
小休止
ここまで開発していく中で、再生(F5)でアプリの動作を確認してみたらボールのスピードが遅いのでスピードの設定値を変えよう。と思われるかもしれませんが、ちょっと待ってください。
一度アプリを公開して公開されたアプリでも動作を試してみてください。
恐らく、アプリ編集中の再生でアプリの動作を確認していたときとでボールの速度が違ってくると思います。
編集中のアプリの再生での動作と、公開されたアプリでの動作って微妙に内部処理がちがってくるんですかねー?
正確な情報はMSの開発部門に潜入でもしないとわからなさそうですね。
ブロックを用意する
ブロックの情報の定義
まずはブロックたちの座標やサイズが定義されたコレクションを用意しましょう。
初期化用ボタンにブロックの情報の初期化処理を追加します。
InitialBtn.OnSelect
// ブロックの生成 ClearCollect( block, With( { n: 10, m: 5 }, ForAll( Sequence(n * m), { x: (Parent.Width / 2 - (n / 2) * 110) + Mod(ThisRecord.Value, n) * 110, y: 100 + RoundDown((ThisRecord.Value - 1) / n, 0) * 30, width: 100, height: 20 } ) ) )
今回はブロックの情報を1つ1つ定義していくのがめんどくさかったので、n * mのブロックを等間隔で並べるようにしています。
nが横方向でmが縦方向ですね。
横方向はブロックを並べたときに画面の中心にくるようにし、縦方向は高さ100から順に下方向に対して並べています。
ブロックの描画
続いてブロックの描画ですが、これにはSVGを利用しようと思います。
ギャラリーを利用するのでもいいのですが、ギャラリーですと描画するオブジェクト(ブロック)の数が増えてくるとどうしてもアプリが重くなってしまいます。
なのでこういったオブジェクトを描画させたい際はSVGを利用することを私は推奨します。
SVGですとコントロールとしては画像コントロール1つで済みますしね。
SVGを描画するための画像コントロールを追加して、"BlockImg"とします。
この画像コントロールは画面いっぱいに表示させてあげてください。
ただそうしちゃうと、バーやボタンが操作できなくなっちゃうので、画像コントロールは最背面に配置しておいてください。
この画像コントロールにSVGを描画していくわけですが、Power AppsでのSVGについて簡単に触れておきます。
Power AppsでSVGを描画する際は画像コントロールのImageプロパティにSVGをData URI形式で記載してあげます。
例えば四角形をSVGで描画したい場合は以下になりますね。
Image.Image
"data:image/svg+xml,"& EncodeUrl( "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'> <rect x='100' y='100' width='100' height='100' /> </svg>" )
詳しく知りたい方はこちらのドキュメントを読んでみるといいかもです。
ただし、Power Appsでは取り扱えない表現もあるのでその点はご注意ください。(多分これがダメとか明確に記載されたドキュメントはないはず)
さて、上記のSVGを用いて先ほど定義したコレクションの情報を用いて四角形を描画していこうと思います。
コレクションでは座標やサイズを定義していたので、これをもとに四角形描画の要素を作成していきます。
四角形の描画は以下の要素で行われています。
<rect x='100' y='100' width='100' height='100' />
これを組み立ててやればいいわけですね。
Power Appsでコレクション(テーブル)の情報を1レコードずつ処理する関数といえば、みんな大好きForAll関数があります。
ForAll関数だけではテキストを返すことができない(テーブル型が返ってくる)ので、Concat関数を使って、テーブルの値を文字列に変換してあげます。
BlockImg.Image
"data:image/svg+xml,"& EncodeUrl( "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>" & Concat( ForAll( block, "<rect x='" & ThisRecord.x & "' y='" & ThisRecord.y & "' width='" & ThisRecord.width & "' height='" & ThisRecord.height & "' />" ), Value ) & "</svg>" )
これでブロックの描画ができました!
あとはブロックとボールの当たり判定の処理です。あと一息!
ブロックとボールの当たり判定と処理を実装する
当たり判定
ブロックとボールの当たり判定だけでいうと、バーとボールの当たり判定のやり方の応用ですね。
トグルを用意して、BallBlockCollision
にリネームしてください。
今回判定を行いたいブロックは複数あるので、それぞれに対して判定を行う必要があります。
そこで活躍するのがまたまたForAll関数です。
ForAll関数を用いて、各レコードに対して当たり判定を行います。
ボールがいずれかのブロックに当たったのならば、ForAllの結果のどれかがtrue
になっているはずなので、CountIf関数でtrueがあるかどうかの判断をしてあげます。
BallBlockCollision.Default
CountIf( ForAll( block, CollisionUtils_1.SquareCircle( ThisRecord.x, ThisRecord.y, ThisRecord.width, ThisRecord.height, ball_X + Ball.Width / 2, ball_Y + Ball.Height / 2, Ball.Width / 2 ) ), Value = true ) > 0
これでボールとブロックのいずれかが当たったかの判定が行えます。
ボールとぶつかったブロックを消す
ボールとぶつかったブロックをどのブロックがぶつかったのか特定したうえでブロックの削除を行います。
ForAll関数では、ThisRecord演算子を利用することで対象のレコードを返すので、当たり判定の結果そのレコード内のブロックがtrue
(ボールとブロックが当たっている)であれば対象のレコードを返すようにします。
これとRemove関数を組み合わせることで、ブロックの削除が行えますね。
BallBlockCollision.OnCheck
Remove( block, ForAll( block, If( CollisionUtils_1.SquareCircle( ThisRecord.x, ThisRecord.y, ThisRecord.width, ThisRecord.height, ball_X + Ball.Width / 2, ball_Y + Ball.Height / 2, Ball.Width / 2 ), ThisRecord ) ) )
ボールとブロックがぶつかったときに跳ね返るようにする
今のままでは、ボールとブロックがぶつかってもそのままボールが進み続けてしまうので、ボールとブロックがぶつかったときにボールが跳ね返るようにしたいと思います。
このとき注意したいのは、ボールがブロックの上下左右どこに当たったか?を意識する必要があるということです。
例えばボールがブロックの上に当たったときと、ブロックの左に当たったときとではボールが跳ね返る方向は異なりますよね?
なので、ボールがブロックの上下左右どこに当たったかでそれぞれ処理を分ける必要があるわけです。
上下左右どこにあたったかは?はブロックの上下左右に薄い四角形がそれぞれ張り付いていて、そいつとあたったかどうかで判断することが可能です。
気を付ける点としては、右と下の四角形は元の四角形のXまたはYがずれているのでその補正が必要という点ですかね。
BallBlockCollision.OnCheck
// Top If( CountIf( ForAll( block, CollisionUtils_1.SquareCircle( ThisRecord.x, ThisRecord.y, ThisRecord.width, 1, ball_X + Ball.Width / 2, ball_Y + Ball.Height / 2, Ball.Width / 2 ) ), Value = true ) > 0, UpdateContext({ball_Y_Speed: ball_Y_Speed * -1}); ); // Bottom If( CountIf( ForAll( block, CollisionUtils_1.SquareCircle( ThisRecord.x, ThisRecord.y + ThisRecord.height - 1, ThisRecord.width, 1, ball_X + Ball.Width / 2, ball_Y + Ball.Height / 2, Ball.Width / 2 ) ), Value = true ) > 0, UpdateContext({ball_Y_Speed: ball_Y_Speed * -1}); ); // Left If( CountIf( ForAll( block, CollisionUtils_1.SquareCircle( ThisRecord.x, ThisRecord.y, 1, ThisRecord.height, ball_X + Ball.Width / 2, ball_Y + Ball.Height / 2, Ball.Width / 2 ) ), Value = true ) > 0, UpdateContext({ball_X_Speed: ball_X_Speed * -1}); ); // Right If( CountIf( ForAll( block, CollisionUtils_1.SquareCircle( ThisRecord.x + ThisRecord.width - 1, ThisRecord.y, 1, ThisRecord.height, ball_X + Ball.Width / 2, ball_Y + Ball.Height / 2, Ball.Width / 2 ) ), Value = true ) > 0, UpdateContext({ball_X_Speed: ball_X_Speed * -1}); );
これでブロック崩しの完成です!
お疲れさまでした!!!
おわりに
これでブロック崩しの原型が作成できましたね。
これにさらにスコア表示や残基、ステージやアイテムの実装なんかするとよりゲーム性が増すと思います。
このハンズオン/ブログをきっかけにゲーム作成された方いらっしゃいましたら、是非SNSやブログ等で報告いただけると、嬉しいなーと思います。