251018JPPGB#2

561 Views

October 18, 25

スライド概要

下記イベント登壇資料です
https://jppgb.connpass.com/event/363360/

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

(ダウンロード不可)

関連スライド

各ページのテキスト
1.

がんばれハム子 製作秘話 2025年10月18日 JPPGB ゲーム作成コンテスト #2

2.

自己紹介 プライベート(2児の父) POWER CLUB 部長 みのる (@meccha__eeyan) / X Qiita PowerAppsタグ 年間4位 資格取得 ⚫ 基本情報技術者試験 (2025年1月) ⚫ MCP PL900,100,300,500,200,600 ⚫ APPLIED SKILLS 会社(製造業の品質保証部) Power Platformの社内コミュニティ(約160名)運営 ⚫ Power Apps ⚫ Power AppsとDataverse ⚫ Power Automate ⚫ Copilot Studio 非IT部門で初の環境管理者権限 Dataverseアプリリリース Power Platformで初の外部伴走支援導入 部内DX推進チーム創設 2025年10月18日 JPPGB POWER CLUB - connpass ゲーム作成コンテスト #2

3.

作品紹介 • ハム子はおなかを空かしてます。 • でもねこがハム子をねらっています・・・ • つかまらずにたくさんエサを集めてね! (のこり時間にも気をつけよう) ハム子の操作はデバイスの傾きを利用しているため、スマホや タブレット等加速度センサ搭載デバイス限定アプリです。 2025年10月18日 JPPGB ゲーム作成コンテスト #2

4.

プレイの様子 2025年10月18日 JPPGB ゲーム作成コンテスト #2

5.

ハム子紹介 種類: ジャンガリアンハムスター 色: ブルーサファイア 性別: 雌 誕生日: 2023年11月29日(良い肉の日) 豆腐チップスが 大好き 2025年10月18日 JPPGB ゲーム作成コンテスト #2

6.

着想背景 前回(24年6月22日)作品 • 前回は風の影響を考慮した弾道計算 • そういえば加速度センサの値とれたよな・・・ • おまけ用とかでよくある バランス迷路おもちゃ 苦労ポイント③ 弾道計算 放物運動(初速と角度から計算) - 高精度計算サイト (casio.jp) りょ! • 我が家のペットのハム子のえさ集めにしよ 砲弾の弾着シミュレーション - 防衛省・自衛隊 無理! 風の影響はvxだけにして、風との速 度差に比例すると仮定しよ。 • それだけだと面白くないから敵用意 :風の速度 :風の抵抗係数 2025年10月18日 JPPGB ゲーム作成コンテスト #2

7.

加速度(Acceleration)信号の検知 Power Apps での Acceleration、App、Compass、Connection、および Location 信号 - Power Platform | Microsoft Learn + + - + - X 0 0 0 0 9.81 -9.81 Y 0 0 9.81 -9.81 0 0 z 9.81 -9.81 0 0 0 0 Acceleration.X とか書くだけで加速度数値化 ⇒ 姿勢検知 2025年10月18日 JPPGB ゲーム作成コンテスト #2

8.

正負がややこしい・・・ 加速度の正負 + + - + Power Appsの 座標の正負 x + + - y ハム子を左にうごかしたい ⇒ スマホの右側を持ち上げる ⇒ 加速度+値 ⇒ X座標-方向 ハム子を下にうごかしたい ⇒ スマホの上側を持ち上げる ⇒ 加速度+値 ⇒ Y座標+方向 加速度の正負と座標の正負の対応がXとYでことなるのでややこしい・・・ 2025年10月18日 JPPGB ゲーム作成コンテスト #2

9.
[beta]
ゲーム実装解説
THIS IS Low-Code! Do more with less!
メインスクリーン: コード355行(自力コーディングは20%くらい)
変数設定(ボタン)
演算処理(タイマー)

グラフィック
(ハム子/猫)
グラフィック
(フィールド/アイテム)

2025年10月18日 JPPGB

Screens:
ScreenMain:
Properties:
Fill: =App.Theme.Colors.Lighter80
OnVisible: |=Select(ButtonParamReset);
Select(ButtonItemSet);
Reset(AudioGameMusic);
Set(_PlayMusic, false)
Children:
- ContainerGameField:
Control: [email protected]
Variant: ManualLayout
Properties:
BorderColor: =RGBA(255, 191, 0, 1)
BorderStyle: =BorderStyle.None
BorderThickness: =7
DropShadow: =DropShadow.None
Height: =1040
RadiusBottomLeft: =0
RadiusBottomRight: =0
RadiusTopLeft: =0
RadiusTopRight: =0
Width: =Parent.Width
Children:
- ImageField:
Control: [email protected]
Properties:
Height: =Parent.Height
Image: =Field_pixel_art_finer
ImagePosition: =ImagePosition.Fill
Transparency: =0.3
Width: =Parent.Width
- GalleryItemSet:
Control: [email protected]
Variant:
BrowseLayout_Vertical_OneTextOneImageVariant_ver5.0
Properties:
BorderColor: =App.Theme.Colors.Lighter10
BorderStyle: =BorderStyle.None
Height: =Parent.Height
Items: =Sequence(Parent.Height/_ObjSize)
TemplateSize: =_ObjSize
Width: =Parent.Width
Children:
- Gallery2:
Control: [email protected]
Variant:
BrowseLayout_Horizontal_TwoTextOneImageVariant_ver5.0
Properties:
Height: =Parent.TemplateHeight
Items: =Sequence(Parent.Width/_ObjSize)
TemplateSize: =_ObjSize
Width: =Parent.Width
Children:
- Image4:
Control: [email protected]
Properties:
Height: =Parent.Height
Image:
=Switch(LookUp(ColItemSet,XGrid=ThisItem.Value &&
YGrid=Value(TextYGrid.Text)).RandVal,1,item01,2,item02,3,item03)
OnSelect: =Select(Parent)
PaddingBottom: =Self.PaddingTop
PaddingLeft: =Self.PaddingTop
PaddingRight: =Self.PaddingTop
PaddingTop: =_ObjSize*0.3
Visible:
=!IsBlank(LookUp(ColItemSet,XGrid=ThisItem.Value &&
YGrid=Value(TextYGrid.Text)))
Width: =Parent.TemplateWidth
- TextYGrid:
Control: [email protected]
Properties:
Text: =ThisItem.Value
Visible: =false
- HtmlHam:
Control: [email protected]
Properties:

Font: =Font.'Open Sans'
- ButtonBack:
_y:_y+_vy*_dt,
_duration:60000
Height: =_ObjSize*Sqrt(2)
Control: [email protected]
//座標&向き計算(猫)
});
HtmlText: |Properties:
_catX:_catX+_catV*Cos(Radians(_catDirect))*_dt,
Set(_IteNum,20),
=$"<img
Appearance: ='ButtonCanvas.Appearance'.Primary
_catY:_catY-_catV*Sin(Radians(_catDirect))*_dt,
"easy",
src={Switch(_HamMode,0,_ImgHamStop,1,_ImgHamWalk1,
FontSize: =30
_catDirect:Mod(Degrees(Atan2((_x-_catX)+1e-7,-(_yUpdateContext({
2,_ImgHamWalk2)}
Height: =70
_catY))), 360)
_catV:20,
style=""
Icon: ="ArrowExit"
});
_duration:180000
width:{_ObjSize}px;
Layout: ='ButtonCanvas.Layout'.TextOnly
//向き計算(ハム子)
});
text-align:center;
OnSelect: |Set(_Rotate,Mod(Degrees(Atan2(_vx+1e-7,_vy))+90,360));
Set(_IteNum,10),
transform:rotate({_Rotate}deg);
=Navigate(ScreenStart,ScreenTransition.CoverRight);
//えさ捕食
"oni",
margin-top:{(Self.Width-_ObjSize)/2}px;
Set(_HamMode,1);Select(ButtonStartReset)
UpdateContext({_hamX:_x+_ObjSize/2,_hamY:_y+_ObjSize/2})
UpdateContext({
margin-left:{(Self.Width-_ObjSize)/2}px;
Text: ="もどる"
;
_catV:200,
""
Width: =137
RemoveIf(ColItemSet,XGrid*_ObjSize>_hamX && (XGrid_duration:60000
>"
X: =502
1)*_ObjSize<_hamX && YGrid*_ObjSize>_hamY && (YGrid});
PaddingBottom: =0
Y: =1053
1)*_ObjSize<_hamY);
Set(_IteNum,40)
PaddingLeft: =0
- LabelTimer:
With({_num:CountRows(ColItemSet)},If(_num<_remainItems,
);
PaddingRight: =0
Control: [email protected]
UpdateContext({_remainItems:_num})));
Text: ="ParamReset"
PaddingTop: =0
Properties:
//猫接触
Y: =140
Width: =_ObjSize*Sqrt(2)
Color: =RGBA(224, 86, 6, 1)
If(
- ButtonItemSet:
X: =_x-(Self.Width-_ObjSize)/2
Font: ="HGS創英角
体"
Or(
Control: [email protected]
Y: =_y-(Self.Height-_ObjSize)/2
Height: =96
_catX+_ObjSize>_hamX && _catX<_hamX &&
Properties:
- HtmlCat:
Size: =35
_catY+_ObjSize>_hamY && _catY<_hamY,
OnSelect: |
Control: [email protected]
Text: =$"のこり{Text(_duration/1000-TimerMain.Value/1000,"0")}
TimerMain.Value=TimerMain.Duration
=With(
Properties:
秒"
),
{
Font: =Font.'Open Sans'
Width: =316
Navigate(ScreenGameOver);Select(ButtonStartReset),
Nx: Max(1, Int(ContainerGameField.Width / _ObjSize)),
Height: =_ObjSize*Sqrt(2)
Y: =1040
CountRows(ColItemSet)=0,
Ny: Max(1, Int(ContainerGameField.Height / _ObjSize)),
HtmlText: |- ContainerGameEngine:
Navigate(ScreenGameClear,ScreenTransition.Fade,{_mode:
Ntake: Min(_IteNum, Max(1,
=$"<img
Control: [email protected]
_mode});Select(ButtonStartReset)
(Int(ContainerGameField.Width / _ObjSize) - 1) *
src={_ImageCatHand}
Variant: ManualLayout
)
(Int(ContainerGameField.Height / _ObjSize) - 1)))
style=""
Properties:
Repeat: =true
},
Height:{_ObjSize}px;
Fill: =RGBA(255, 255, 255, 0.3)
Reset: =!_Start
UpdateContext({_remainItems:_IteNum});
text-align:center;
Height: =247
Start: =_Start
ClearCollect(
transform:rotate({-_catDirect+90}deg);
Visible: =CheckboxSetting.Value
Text: ="TimerCalc"
ColItemSet,
margin-top:{(Self.Width-_ObjSize)/2}px;
Width: =385
Width: =147
// 全セルを列挙 → シャッフル → 先頭Ntake件だけ
margin-left:{(Self.Width-_ObjSize)/2}px;
Y: =780
X: =231
AddColumns(
""
Children:
Y: =81
FirstN(
>"
- TimerHam:
- TimerMain:
Shuffle(
PaddingBottom: =0
Control: [email protected]
Control: [email protected]
RemoveIf(
PaddingLeft: =0
Properties:
Properties:
AddColumns(
PaddingRight: =0
AutoPause: =false
AutoPause: =false
Sequence(Nx * Ny) As rec,
PaddingTop: =0
Duration: =3000/(Sqrt(_vx*_vx+_vy*_vy)/10+1)
Duration: =_duration
XGrid, 1 + RoundDown((rec.Value - 1) / Ny,0),
Width: =_ObjSize*Sqrt(2)
Height: =38
Height: =38
YGrid, 1 + Mod(rec.Value - 1, Ny)
X: =_catX-(Self.Width-_ObjSize)/2
OnTimerEnd:
OnTimerStart: =
),XGrid=1&&YGrid=1)
Y: =_catY-(Self.Height-_ObjSize)/2
=Switch(_HamMode,0,Set(_HamMode,1),1,Set(_HamMode,2),2,Set(_Ha
Reset: =!_Start
),
- CheckboxSetting:
mMode,1))
Start: =_Start
Ntake
Control: Classic/[email protected]
Repeat: =true
Text: ="TimerMain"
),
Properties:
Reset: =!_Start
Width: =146
// 画像選択用ランダム属性(1~3)
BorderStyle: =BorderStyle.None
Start: =_Start
X: =231
RandVal, 1 + Mod( RoundDown(Rand() * 1000000,0), 3 )
CheckboxSize: =35
Text: ="TimerHam"
Y: =119
)
Height: =45
Width: =146
- ButtonParamReset:
)
Size: =13
X: =231
Control: [email protected]
)
Text: ="設定"
Y: =43
Properties:
Text: ="えさ"
Width: =124
- TimerCalc:
Height: =31
Y: =191
X: =-Self.Width
Control: [email protected]
OnSelect: |
- AudioGameMusic:
Y: =65
Properties:
=Set(_AccMul,50);
Control: [email protected]
- ButtonStartReset:
AutoPause: =false
Set(_Rotate,0);
Properties:
Control: [email protected]
Duration: =10
Set(_HamMode,0);
Loop: =true
Properties:
Height: =38
UpdateContext({
Media: ='がんばれハム子 プレイ中'
FontSize: =30
OnTimerEnd: |_x:_InitX,
Reset: =!_PlayMusic
Height: =70
=With(
_y:_InitY,
Start: =_PlayMusic
OnSelect: |{_fieldW:ContainerGameField.Width,_fieldH:ContainerGam
_vx:0,
Width: =385
=If(
eField.Height},
_vy:0,
X: =-Self.Width
_HamMode=0,
//壁面接触時の処理(ハム子)
_dt:0,
//_HamModeが0の時⇒スタート処理
If(_x+_ObjSize>_fieldW,UpdateContext({_vx:_t:0,
Set(_HamMode,1);
_vx*0.5,_x:_fieldW-_ObjSize-1}));
_catX:ContainerGameField.Width-_ObjSize-1,
Set(_Start,true);
If(_x<0,UpdateContext({_vx:-_vx*0.5,_x:1}));
_catY:ContainerGameField.Height-_ObjSize-1,
Set(_PlayMusic, false);
If(_y+_ObjSize>_fieldH,UpdateContext({_vy:_catDirect:135
Set(_PlayMusic, true),
_vy*0.5,_y:_fieldH-_ObjSize-1}));
});
//_HamModeが0以外の時⇒リセット処理
If(_y<0,UpdateContext({_vy:-_vy*0.5,_y:1}));
Switch(
Select(ButtonParamReset);
);
_mode,
Set(_Start,false);
UpdateContext({_dt:(TimerMain.Value-_t)/1000});
"normal",
Set(_HamMode,0);
If(Mod(TimerMain.Value,1000)<20,UpdateContext({_debug:_
UpdateContext({
Select(ButtonItemSet);
dt}));
_catV:50,
Set(_PlayMusic, false);
UpdateContext({
_duration:120000
)
_t:TimerMain.Value,
});
Text: =If(_HamMode=0,"スタート","リセット")
//速度、座標計算(ハム子)
Set(_IteNum,20),
Width: =171
_vx:_vx-Acceleration.X*_dt*_AccMul,
"hard",
X: =316
_vy:_vy+Acceleration.Y*_dt*_AccMul,
UpdateContext({
Y: =1053
_x:_x+_vx*_dt,
_catV:80,

ゲーム作成コンテスト #2

10.
[beta]
変数設定(ボタン) アイテム配置
ハム子が回収するエサをランダムに配置(ハム子初期位置左上を除く)
With(
{
Nx: 横方向の分割数
Nx: Max(1, Int(ContainerGameField.Width / _ObjSize)),
Ny: 縦方向の分割数
Ny: Max(1, Int(ContainerGameField.Height / _ObjSize)),
Ntake: Min(_IteNum, Max(1, (Int(ContainerGameField.Width / _ObjSize) - 1)
* (Int(ContainerGameField.Height / _ObjSize) - 1)))
Ntake: アイテム数
},
UpdateContext({_remainItems:_IteNum});
ClearCollect(
ColItemSet,
// 全セルを列挙 → シャッフル → 先頭Ntake件だけ
AddColumns(
FirstN(
Shuffle(
RemoveIf(
① Nx × Ny 行のテーブル用意
AddColumns(
Sequence(Nx * Ny) As rec,
XGrid, 1 + RoundDown((rec.Value - 1) / Ny,0),
YGrid, 1 + Mod(rec.Value - 1, Ny)
),XGrid=1&&YGrid=1)
② XGrid列に1~Nx、YGrid列に1~Nyの採番
),
③ ハム子初期位置(1,1)を除外
Ntake
④ シャッフルしたものから先頭Ntake件のみ抽出
),
// 画像選択用ランダム属性(1~3)
RandVal, 1 + Mod( RoundDown(Rand() * 1000000,0), 3 )
)
⑤ エサ画像バリエーション用の1~3ランダム数
)
)

ちょっと無駄あり・・・

2025年10月18日 JPPGB

ゲーム作成コンテスト #2

11.

というわけでリファクタリング ポイント • RemoveIfの無駄削除 • Grid列採番の無駄削除 2025年10月18日 JPPGB ゲーム作成コンテスト #2

12.

演算処理(タイマー)① ハム子のアニメ HTMLテキストコントロールを使用 速度に応じて切り替え速度を変える - TimerHam: Duration: =3000/(Sqrt(_vx*_vx+_vy*_vy)/10+1) OnTimerEnd: =Switch(_HamMode, 0,Set(_HamMode,1), 1,Set(_HamMode,2), 2,Set(_HamMode,1) ) 2025年10月18日 JPPGB ゲーム作成コンテスト #2

13.

演算処理(タイマー)② ハム子&猫の移動計算 速度一定で方向決定 ⇒ 単調すぎてゲーム性 坂道を転がるボールのように、角度や時間に応じて速度が変わるように リアルタイム計測用のメインタイマー(プレイ制限時間) 短時間ループでゲーム演算処理 10msec設定⇒理論上100fpsだが実測12fps程度 _t:前回ループ時のメインタイマー値 _AccMul:加速補正定数 ハム子はユーザー操作で移動、猫は演算ループごとにハム子へ軌道修正 2025年10月18日 JPPGB ゲーム作成コンテスト #2

14.

演算処理(タイマー)③ 接触判定(えさ捕食) XGrid=1 XGrid=2 _ObjSize • ハム子中心がColItemSetのXGrid,YGrid侵入 ⇒ 該当レコード消去 YGrid=1 (_hamX, _hamY) ※ すなわち捕食 • その後ColItemSetのレコード数を確認し_remainItemsに格納 YGrid=2 2025年10月18日 JPPGB ゲーム作成コンテスト #2

15.

演算処理(タイマー)④ 接触判定(猫接触) _ObjSize • ハム子中心が猫のエリアに侵入 ⇒ ゲームオーバー (_hamX, _hamY) (_catX, _catY) 2025年10月18日 JPPGB • またはメインタイマーが時間切れでもゲームオーバー • ColItemSetのレコード数がゼロになったらゲームクリア ゲーム作成コンテスト #2

16.

難易度調整 3要素で難易度調整 _catV: 猫の速度 _duration: 制限時間[msec] _IteNum: エサの個数 鬼Mode ⇒ 私と長女(小3)クリア済み 2025年10月18日 JPPGB ゲーム作成コンテスト #2

17.

あとがき 参加を決めてから約1週間、夜な夜な寝不足になりながら没頭しました 遊んでくれる人をどう楽しませるか? ⇒ 業務アプリ製作でも大事! こんなに少ないコード&短期間で実装できるPower Apps最高! 2025年10月18日 JPPGB ゲーム作成コンテスト #2

18.

これをCode Appsで 2025年10月18日 JPPGB ゲーム作成コンテスト #2

19.

Code Apps • 仕様書作って爆速でアプリ完成 • 仕様書そのものもCopilotと一緒に • Androidデバイスでは加速度が取得できなかった • 爆速だがやはり細かい指示しきれないので想定外の動作 • 表示や操作感のレビューと修正はまだ人間必須 • 制限はあるがキャンバスアプリの安心感◎ 2025年10月18日 JPPGB ゲーム作成コンテスト #2

20.

現場からは以上です。 ご安全に。 2025年10月18日 JPPGB ゲーム作成コンテスト #2