Jetpack Compose製テトリス compose-tetrisのコードを読んだ

2023/03/19に公開

Jetpack Composeで実装されたテトリスをGitHubで見つけ、面白そうだな〜と思い実装を覗いてみました。
宣言型UIの可読性の高さ、Composeのパワーを感じる内容でした。
https://github.com/vitaviva/compose-tetris/tree/main

メモを兼ねて面白かった・勉強になった点をいくつか挙げていきます。

アーキテクチャ

READMEにもありますが、アーキテクチャはMVIです。

UIで発生するイベントが Action というsealed classでまとめられており、UIで何が起こり得るのか予想しやすいです。ゲームのリセットやポーズ、ミノの移動などが定義されています。
https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/logic/GameViewModel.kt#L267-L276

stateで積まれたミノ、落下中のミノ、ゲームのスコアなどが表現されています。
UIで発生した Action を受けてviewModelでstateを更新します。
ミノの移動を示すActionを受け取るとstateが持つミノの位置を更新します。
https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/logic/GameViewModel.kt#L26-L173

更新したstateをUIに反映するのはComposeの仕事です。
UI構築に必要な情報はstateが全て持ち、Composeで宣言的に記述されたUIはそれを反映することに専念しており、動きを予測しやすいです。
MainActivity

基本的なゲームの進行

LaunchedEffectでcoroutineを起動し、delayを挟んで Action.GameTick をdispatchするという処理をループしています。 dispatchされるとviewModelでは、ゲームオーバーの判定、ミノの落下、ミノの消去などを行いstateを更新します。これによってゲームが進行していきます。
https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/MainActivity.kt#L38-L43
level が上がるとdelayが短くなりゲームスピードが上がっていきます(20ライン消す毎にlevelが上がるというルールになっていました)。

ボタン操作

ミノの移動や回転は各ボタンの操作に対応したActionがdispatchされることで反映されます。
https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/MainActivity.kt#L63-L89

十字ボタンでミノを移動させたら位置を更新する、回転させるとミノの形を変更するといった具合です。

ボタンを押し続ける操作

ミノを移動させる十字ボタンを押し続けている間は同じ操作をが繰り返します。「 ボタンが押されている間はミノを左に移動し続ける」といった動きです。
この挙動はtickerで実装されています。

Creates a channel that produces the first item after the given initial delay and subsequent items with the given delay between them.

tickerは与えられた時間が経過する毎に次のアイテムを Channelを生成します。
生成されたChannelから値を受け取る度に同じActionをdispatchすることで、 「 ボタンが押されている間は一定間隔でミノを左に移動するActionをdispatchする」などの挙動が実現されます。
https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/ui/GameButton.kt#L86-L91

ボタンから指が離れるときには ticker で生成したChannelを閉じ、

ミノのパターンの定義とプレビュー

ミノはCanvasで描画されます。( Spilit(ミノ) は形状と位置を保持しており、それを基に Blick(マス) を塗っていく )

https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/ui/GameScreen.kt#L267-L313

https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/ui/GameScreen.kt#L267-L313
定義したミノのパターンがCanvasで描画されるのをプレビュー機能で確認できます。
これをAndroid Viewで確認するとなると、結構な手間がかかると想像できます。とても便利で、Composeのプレビュー機能の強力さを感じるポイントですね。

ミノのパターンの定義
https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/logic/Spirit.kt#L45-L53

ミノのプレビュー
https://github.com/vitaviva/compose-tetris/blob/234416c455cd0b5524b7f2a7e91aaa9f6206457a/app/src/main/java/com/jetgame/tetris/ui/GameScreen.kt#L359-L387
preview

おわりに

他にもアニメーションやミノの削除の演出の実装などがあり。コードをいじりながら読み進めるとComposeでリッチなUIを実現するための良い素振りにもなりそうです。
作者の方に感謝です!

Discussion