日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

Compose 正式发布,来打造一个 Flappy Bird! | 开发者说·DTalk

發(fā)布時間:2023/12/14 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Compose 正式发布,来打造一个 Flappy Bird! | 开发者说·DTalk 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本文原作者:?小蝦米君,原文發(fā)布于:?TechMerger

https://mp.weixin.qq.com/s/Hpd2NF0hOw4xOo3wVb_VFg

之前看到 fun 神用 Compose 打造的俄羅斯方塊,深受啟發(fā),萌生了也打造一個游戲的想法。正值 Compose 1.0 的正式發(fā)布,及時跟進(jìn)一下 Compose 的學(xué)習(xí)!

Flappy Bird 是 13 年紅極一時的小游戲,其簡單有趣的玩法和變態(tài)的難度形成了強烈反差,引發(fā)全球玩家競相把玩,欲罷不能!遂選擇復(fù)刻這個小游戲,在實現(xiàn)的過程中向大家演示 Compose 工具包的 UI 組合、數(shù)據(jù)驅(qū)動等重要思想。

拆解游戲

不記得這個游戲或完全沒玩過的朋友,可以前往體驗一下 Flappy Bird 的玩法:?https://flappybird.io/

為拆解游戲,筆者也錄了一段游戲過程。

反復(fù)觀看這段 GIF,可以發(fā)現(xiàn)游戲的一些規(guī)律:

  • 遠(yuǎn)處的建筑和近處的土壤是靜止不動的

  • 小鳥一直在上下移動,伴隨著翅膀和身體的飛翔姿態(tài)

  • 管道和路面則不斷地向左移動,營造出小鳥向前飛翔的視覺效果


通過截圖、切圖、填充像素和簡單的 PS,可以拿到各元素的圖片。

復(fù)刻畫面

各方卡司已就位,接下來開始布置整個畫面。暫不實現(xiàn)元素的移動效果,先把靜態(tài)的整體效果搭建好。

布置遠(yuǎn)近景

靜止不動的建筑遠(yuǎn)景最為簡單,封裝到可組合函數(shù) FarBackground 里,內(nèi)部放置一張圖片即可。

@Composable fun FarBackground(modifier: Modifier) {Column {Image(painter = painterResource(id = R.drawable.background),contentScale = ContentScale.FillBounds,contentDescription = null,modifier = modifier.fillMaxSize())} }

遠(yuǎn)景的下面由分割線、路面和土壤組成,封裝到 NearForeground 函數(shù)里。通過 Modifier 的 fraction 參數(shù)控制路面和土壤的比例,保證在不同尺寸屏幕上能按比例呈現(xiàn)游戲界面。

@Composable fun NearForeground(...) {Column( modifier ) {// 分割線Divider(color = GroundDividerPurple,thickness = 5.dp)// 路面Box(modifier = Modifier.fillMaxWidth()) {Image(painter = painterResource(id = R.drawable.foreground_road),...modifier = modifier.fillMaxWidth().fillMaxHeight(0.23f))}}// 土壤Image(painter = painterResource(id = R.drawable.foreground_earth),...modifier = modifier.fillMaxWidth().fillMaxHeight(0.77f))} }

將整個游戲畫面抽象成 GameScreen 函數(shù),通過 Column 豎著排列遠(yuǎn)景和前景。考慮到移動的小鳥和管道需要呈現(xiàn)在遠(yuǎn)景之上,所以在遠(yuǎn)景的外面包上一層 Box 組件。

@Composable fun GameScreen( ... ) {Column( ... ) {Box(modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) {FarBackground(Modifier.fillMaxSize())}Box(modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) {NearForeground(modifier = Modifier.fillMaxSize())}} }

擺放管道

仔細(xì)觀察管道,會發(fā)現(xiàn)一些管道具備朝上朝下、高度隨機的特點。為此將管道的視圖分拆成蓋子和柱子兩部分:

  • 蓋子和柱子的放置順序決定管道的朝向

  • 柱子的高度則控制著管道整體的高度這樣的話,只使用蓋子和柱子兩張圖片,就可以靈活實現(xiàn)各種形態(tài)的管道。

先來組合蓋子 PipeCover 和柱子 PipePillar 的可組合函數(shù)。

@Composable fun PipeCover() {Image(painter = painterResource(id = R.drawable.pipe_cover),contentScale = ContentScale.FillBounds,contentDescription = null,modifier = Modifier.size(PipeCoverWidth, PipeCoverHeight)) }@Composable fun PipePillar(modifier: Modifier = Modifier, height: Dp = 90.dp) {Image(painter = painterResource(id = R.drawable.pipe_pillar),contentScale = ContentScale.FillBounds,contentDescription = null,modifier = modifier.size(50.dp, height)) }

管道的可組合函數(shù) Pipe 可以根據(jù)照朝向和高度的參數(shù),組合成對應(yīng)的管道。

@Composable fun Pipe( height: Dp = HighPipe,up: Boolean = true ) {Box( ... ) {Column {if (up) {PipePillar(Modifier.align(CenterHorizontally), height - 30.dp)PipeCover()} else {PipeCover()PipePillar(Modifier.align(CenterHorizontally), height - 30.dp)}}} }

另外,管道都是成對出現(xiàn)、且無論高度如何中間的間距是固定的。所以我們再實現(xiàn)一個管道組的可組合函數(shù) PipeCouple。

@Composable fun PipeCouple( ... ) {Box(...) {GetUpPipe(height = upHeight,modifier = Modifier.align(Alignment.TopEnd))GetDownPipe(height = downHeight,modifier = Modifier.align(Alignment.BottomEnd))} }

將 PipeCouple 添加到 FarBackground 的下面,管道就放置完畢了。

@Composable fun GameScreen( ... ) {Column(...) {Box(...) {FarBackground(Modifier.fillMaxSize())// 管道對添加遠(yuǎn)景上去PipeCouple(modifier = Modifier.fillMaxSize())}...} }

放置小鳥

小鳥通過 Image 組件即可實現(xiàn),默認(rèn)情況下放置到布局的 Center 方位。

@Composable fun Bird( ... ) {Box( ... ) {Image(painter = painterResource(id = R.drawable.bird_match),contentScale = ContentScale.FillBounds,contentDescription = null,modifier = Modifier.size(BirdSizeWidth, BirdSizeHeight).align(Alignment.Center))} }

視覺上小鳥呈現(xiàn)在管道的前面,所以 Bird 可組合函數(shù)要添加到管道組函數(shù)的后面。

@Composable fun GameScreen( ... ) {Column(...) {Box(...) {...PipeCouple( ... )// 將小鳥添加到遠(yuǎn)景上去Bird(modifier = Modifier.fillMaxSize(),state = viewState)}} }

至此,各元素都放置完了。接下來著手讓小鳥,管道和路面這些動態(tài)元素動起來。

狀態(tài)管理和架構(gòu)

Compose 中 Modifier#offset() 函數(shù)可以更改視圖在橫縱方向上的偏移值,通過不斷地調(diào)整這個偏移值,即可營造出動態(tài)的視覺效果。無論是小鳥還是管道和路面,它們的移動狀態(tài)都可以依賴這個思路。

那如何管理這些持續(xù)變化的偏移值數(shù)據(jù)?如何將數(shù)據(jù)反映到畫面上?

Compose 通過 State 驅(qū)動可組合函數(shù)進(jìn)行重組,進(jìn)而達(dá)到畫面的重繪。所以我們將這些數(shù)據(jù)封到 ViewState 中,交由 ViewModel 框架計算和更新,Compose 訂閱 State 之后驅(qū)動所有元素活動起來。除了各元素的偏移值數(shù)據(jù),State 中還要存放游戲分值,游戲狀態(tài)等額外信息。

data class ViewState(val gameStatus: GameStatus = GameStatus.Waiting,// 小鳥狀態(tài)val birdState: BirdState = BirdState(),// 管道組狀態(tài)val pipeStateList: List<PipeState> = PipeStateList,var targetPipeIndex: Int = -1,// 路面狀態(tài)val roadStateList: List<RoadState> = RoadStateList,var targetRoadIndex: Int = -1,// 分值數(shù)據(jù)val score: Int = 0,val bestScore: Int = 0, )enum class GameStatus {Waiting,Running,Dying, Over }

用戶點擊屏幕會觸發(fā)游戲開始、重新開始、小鳥上升等動作,這些視圖上的事件需要反向傳遞給 ViewModel 處理和做出響應(yīng)。事件由 Clickable 數(shù)據(jù)類封裝,再轉(zhuǎn)為對應(yīng)的 GameAction 發(fā)送到 ViewModel 中。

data class Clickable(val onStart: () -> Unit = {},val onTap: () -> Unit = {},val onRestart: () -> Unit = {},val onExit: () -> Unit = {} )sealed class GameAction {object Start : GameAction()object AutoTick : GameAction()object TouchLift : GameAction()object Restart : GameAction() }

前面說過,可以不斷調(diào)整下 Offset 數(shù)據(jù)使得視圖動起來。具體實現(xiàn)可以通過 LaunchedEffect 啟動一個定時任務(wù),定期發(fā)送一個更新視圖的動作 AutoTick。注意: Compose 里獲取 ViewModel 實例發(fā)生 NoSuchMethodError 錯誤的話,記得按照官方構(gòu)建的版本重新 Sync 一下。

setContent {FlappyBirdTheme {Surface(color = MaterialTheme.colors.background) {val gameViewModel: GameViewModel = viewModel()LaunchedEffect(key1 = Unit) {while (isActive) {delay(AutoTickDuration)gameViewModel.dispatch(GameAction.AutoTick)}}Flappy(Clickable(onStart = {gameViewModel.dispatch(GameAction.Start)}...))}}

ViewModel 收到 Action 后開啟協(xié)程,計算視圖的位置、更新對應(yīng) State,之后發(fā)射出去。

class GameViewModel : ViewModel() {fun dispatch(...) {response(action, viewState.value)}private fun response(action: GameAction, state: ViewState) {viewModelScope.launch {withContext(Dispatchers.Default) {emit(when (action) {GameAction.AutoTick -> run {// 路面,管道組以及小鳥移動的新State獲取...state.copy(gameStatus = GameStatus.Running,birdState = newBirdState,pipeStateList = newPipeStateList,roadStateList = newRoadStateList)}...})}}} }

路面動起來

如果畫面上只放一張路面圖片,更改 X 軸 Offset 的話,剩余的部分會沒有路面,無法呈現(xiàn)出不斷移動的效果。

思前想后,發(fā)現(xiàn)放置兩張路面圖片可以解決: 一張放在屏幕外側(cè),一張放在屏幕內(nèi)側(cè)。游戲的過程中同時同方向移動兩張圖片,當(dāng)前一張圖片移出屏幕后重置其位置,進(jìn)而營造出道路不斷移動的效果。

@Composable fun NearForeground( ... ) {val viewModel: GameViewModel = viewModel()Column( ... ) {...// 路面Box(modifier = Modifier.fillMaxWidth()) {state.roadStateList.forEach { roadState ->Image(...modifier = modifier...// 不斷調(diào)整路面在x軸的偏移值.offset(x = roadState.offset))}}...if (state.playZoneSize.first > 0) {state.roadStateList.forEachIndexed { index, roadState ->// 任意路面的偏移值達(dá)到兩張圖片位置差的時候// 重置路面位置,重新回到屏幕外if (roadState.offset <= - TempRoadWidthOffset) {viewModel.dispatch(GameAction.RoadExit, roadIndex = index)}}}} }

ViewModel 收到 RoadExit 的 Action 之后通知路面 State 進(jìn)行位置的重置。

class GameViewModel : ViewModel() {private fun response(action: GameAction, state: ViewState) {viewModelScope.launch {withContext(Dispatchers.Default) {emit(when (action) {GameAction.RoadExit -> run {val newRoadState: List<RoadState> =if (state.targetRoadIndex == 0) {listOf(state.roadStateList[0].reset(), state.roadStateList[1])} else {listOf(state.roadStateList[0], state.roadStateList[1].reset())}state.copy(gameStatus = GameStatus.Running,roadStateList = newRoadState)}})}}} }data class RoadState (var offset: Dp = RoadWidthOffset) {// 移動路面fun move(): RoadState = copy(offset = offset - RoadMoveVelocity)// 重置路面fun reset(): RoadState = copy(offset = TempRoadWidthOffset) }

管道動起來

設(shè)備屏幕寬度有限,同一時間最多呈現(xiàn)兩組管道就可以了。和路面運動的思路類似,只需要放置兩組管道,就可以實現(xiàn)管道不停移動的視覺效果。

具體的話,兩組管道相隔一段距離放置,游戲中兩組管道一起同時向左移動。當(dāng)前一組管道運動到屏幕外的時候,將其位置重置。

那如何計算管道移動到屏幕外的時機?

畫面重組的時候判斷管道偏移值是否達(dá)到屏幕寬度,YES 的話向 ViewModel 發(fā)送管道重置的 Action。

@Composable fun PipeCouple(modifier: Modifier = Modifier,state: ViewState = ViewState(),pipeIndex: Int = 0 ) {val viewModel: GameViewModel = viewModel()val pipeState = state.pipeStateList[pipeIndex]Box( ... ) {//從State中獲取管道的偏移值,在重組的時候讓管道移動 GetUpPipe(height = pipeState.upHeight,modifier = Modifier.align(Alignment.TopEnd).offset(x = pipeState.offset))GetDownPipe(...)if (state.playZoneSize.first > 0) {...// 移動到屏幕外的時候發(fā)送重置Actionif (pipeState.offset < - playZoneWidthInDP) {viewModel.dispatch(GameAction.PipeExit, pipeIndex = pipeIndex)}}} }

ViewModel 收到 PipeExit 的 Action 后發(fā)起重置管道數(shù)據(jù),并將更新發(fā)射出去。

class GameViewModel : ViewModel() {private fun response(action: GameAction, state: ViewState) {viewModelScope.launch {withContext(Dispatchers.Default) {emit(when (action) {GameAction.PipeExit -> run {val newPipeStateList: List<PipeState> =if (state.targetPipeIndex == 0) {listOf(state.pipeStateList[0].reset(),state.pipeStateList[1])} else {listOf(state.pipeStateList[0],state.pipeStateList[1].reset())}state.copy(pipeStateList = newPipeStateList)}})}}} }

但相比路面,管道還具備高度隨機、間距固定的特性。所以重置位置的同時記得將柱子的高度隨機賦值,并給另一根柱子賦值剩余的高度。

data class PipeState (var offset: Dp = FirstPipeWidthOffset,var upHeight: Dp = ValueUtil.getRandomDp(LowPipe, HighPipe),var downHeight: Dp = TotalPipeHeight - upHeight - PipeDistance ) {// 移動管道fun move(): PipeState =copy(offset = offset - PipeMoveVelocity)// 重置管道fun reset(): PipeState {// 隨機賦值上面管道的高度val newUpHeight = ValueUtil.getRandomDp(LowPipe, HighPipe)return copy(offset = FirstPipeWidthOffset,upHeight = newUpHeight,// 下面管道的高度由差值賦值downHeight = TotalPipeHeight - newUpHeight - PipeDistance)} }

需要留意一點的是,如果希望管道組出現(xiàn)的節(jié)奏固定,那么管道組之間的橫向間距 (不是上下管道的間距) 始終需要保持一致。為此兩組管道初始的 Offset 數(shù)據(jù)要遵循一些規(guī)則,此處省略計算的過程,大概規(guī)則如下。

val FirstPipeWidthOffset = PipeCoverWidth * 2 // 第二組管道的offset等于 // 屏幕寬度 加上 三倍第一組管道offset 的一半 val SecondPipeWidthOffset = (TotalPipeWidth + FirstPipeWidthOffset * 3) / 2val PipeStateList = listOf(PipeState(),PipeState(offset = (SecondPipeWidthOffset)) )

小鳥飛起來

不斷調(diào)整小鳥圖片在 Y 軸上的偏移值可以實現(xiàn)小鳥的上下移動。但相較于路面和管道,小鳥的需要些特有的處理:

  • 監(jiān)聽用戶的點擊事件,向上調(diào)整偏移值實現(xiàn)上升效果

  • 在上升和下降的過程中,調(diào)整小鳥的 Rotate 角度,以演示運動的姿態(tài)

  • 在觸碰到路面的時刻,發(fā)送 HitGround 的 Action 停止游戲

@Composable fun GameScreen(...) {...Column(modifier = Modifier.fillMaxSize().background(ForegroundEarthYellow).run {pointerInteropFilter {when (it.action) {// 監(jiān)聽點擊事件,觸發(fā)游戲開始或小鳥上升ACTION_DOWN -> {if (viewState.gameStatus == GameStatus.Waiting)clickable.onStart()else if (viewState.gameStatus == GameStatus.Running)clickable.onTap()}...}false}}) { ... } }

小鳥根據(jù) State 的 Offset 數(shù)據(jù)開始移動和調(diào)整姿態(tài),同時在觸地的時候告知 ViewModel。因為下降的偏移值誤差可能導(dǎo)致觸地的那刻小鳥位置發(fā)生偏差,所以在小鳥下落到路面的臨界點后需要手動調(diào)整下 Offset 值。

@Composable fun Bird(...) {...// 根據(jù)小鳥上升或下降的狀態(tài)調(diào)整小鳥的Roate角度val rotateDegree =if (state.isLifting) LiftingDegreeelse if (state.isFalling) FallingDegreeelse PendingDegreeBox(...) {var correctBirdHeight = state.birdState.birdHeightif (state.playZoneSize.second > 0) {...val fallingThreshold = BirdHitGroundThreshold// 小鳥偏移值達(dá)到背景邊界時發(fā)送落地Actionif (correctBirdHeight + fallingThreshold >= playZoneHeightInDP / 2) {viewModel.dispatch(GameAction.HitGround)// 修改下offset值避免下落到臨界位置的誤差correctBirdHeight = playZoneHeightInDP / 2 - fallingThreshold}}Image(...modifier = Modifier.size(BirdSizeWidth, BirdSizeHeight).align(Alignment.Center).offset(y = correctBirdHeight)// 將旋轉(zhuǎn)角度應(yīng)用到小鳥,展示飛翔姿態(tài).rotate(rotateDegree))} }

碰撞和實時分值

動態(tài)的元素都實現(xiàn)好了,下一步開始安排碰撞算法,并將實時分值同步展示到游戲上方。

仔細(xì)思考,發(fā)現(xiàn)當(dāng)管道組移動到小鳥飛翔區(qū)域的時候,計算小鳥是否處在管道區(qū)域即可判斷是否產(chǎn)生了碰撞。而當(dāng)管道移動出小鳥飛翔范圍的時候,即可判定小鳥成功穿過了管道,開始計分。

如下圖所示當(dāng)管道移動到小鳥飛翔區(qū)域的時候,紅色部分為危險地帶,綠色部分才是安全區(qū)域。

@Composable fun GameScreen(...) {...Column(...) {Box(...) {...// 添加實時展示分值的Text組件ScoreBoard(modifier = Modifier.fillMaxSize(),state = viewState,clickable = clickable)// 遍歷兩個管道組,檢查小鳥的穿過狀態(tài)if (viewState.gameStatus == GameStatus.Running) {viewState.pipeStateList.forEachIndexed { pipeIndex, pipeState ->CheckPipeStatus(viewState.birdState.birdHeight,pipeState,playZoneWidthInDP,playZoneHeightInDP).also {when (it) {// 碰撞到管道的話通知ViewModel,安排墜落PipeStatus.BirdHit -> {viewModel.dispatch(GameAction.HitPipe)}// 成功通過的話通知ViewModel計分PipeStatus.BirdCrossed -> {viewModel.dispatch(GameAction.CrossedPipe, pipeIndex = pipeIndex)}}}}}}} }@Composable fun CheckPipeStatus(...): PipeStatus {// 管道尚未移動到小鳥運動區(qū)域if (pipeState.offset - PipeCoverWidth > - zoneWidth / 2 + BirdSizeWidth / 2) {return PipeStatus.BirdComing} else if (pipeState.offset - PipeCoverWidth < - zoneWidth / 2 - BirdSizeWidth / 2) {// 小鳥成功穿過管道return PipeStatus.BirdCrossed} else {val birdTop = (zoneHeight - BirdSizeHeight) / 2 + birdHeightOffsetval birdBottom = (zoneHeight + BirdSizeHeight) / 2 + birdHeightOffset// 管道移動到小鳥運動區(qū)域并和小鳥重合if (birdTop < pipeState.upHeight || birdBottom > zoneHeight - pipeState.downHeight) {return PipeStatus.BirdHit}return PipeStatus.BirdCrossing}}

ViewModel 收到碰撞 HitPipe 和穿過管道 CrossedPipe 的 Action 后進(jìn)行墜落或計分的處理。

class GameViewModel : ViewModel() {private fun response(action: GameAction, state: ViewState) {viewModelScope.launch {withContext(Dispatchers.Default) {emit(when (action) {GameAction.HitPipe -> run {// 撞擊到管道后快速墜落val newBirdState = state.birdState.quickFall()state.copy(// 并將游戲Status更新為DyinggameStatus = GameStatus.Dying,birdState = newBirdState)}GameAction.CrossedPipe -> run {val targetPipeState = state.pipeStateList[state.targetPipeIndex]// 計算過分值的話跳過,避免重復(fù)計分if (targetPipeState.counted) {return@run state.copy()}// 標(biāo)記該管道組已經(jīng)統(tǒng)計過分值val countedPipeState = targetPipeState.count()val newPipeStateList = if (state.targetPipeIndex == 0) {listOf(countedPipeState, state.pipeStateList[1])} else {listOf(state.pipeStateList[0], countedPipeState)}state.copy(pipeStateList = newPipeStateList,// 當(dāng)前分值累加score = state.score + 1,// 最高分取最高分和當(dāng)前分值的較大值即可bestScore = (state.score + 1).coerceAtLeast(state.bestScore))}})}}} }

當(dāng)小鳥碰撞到了管道,立刻將下落的速度提高,并將 Rotate 角度加大,營造出快速墜落的效果。

@Composable fun Bird(...) {...val rotateDegree =if (state.isLifting) LiftingDegreeelse if (state.isFalling) FallingDegreeelse if (state.isQuickFalling) DyingDegreeelse if (state.isOver) DeadDegreeelse PendingDegree }

結(jié)束分值和重新開始

結(jié)束和實時兩種分值功能有交叉,統(tǒng)一封裝到 ScoreBoard 可組合函數(shù)中,根據(jù)游戲狀態(tài)自由切換。

游戲結(jié)束時展示的信息較為豐富,包含本次分值、最高分值,以及重新開始和退出兩個按鈕。為了方便視圖的 Preview 和提高重組性能,我們將其拆分為單個分值、按鈕、分值儀表盤和結(jié)束分值四個部分。

Compose 的 Preview 功能很好用,但要留意一點:?其 Composable 函數(shù)里不要放入 ViewModel 邏輯,否則會渲染失敗。我們可以拆分 UI 和 ViewModel 邏輯,在保證 Preview 能順利進(jìn)行的同時能復(fù)用視圖部分的代碼。

@Composable fun ScoreBoard(...) {when (state.gameStatus) {// 開始的狀態(tài)下展示簡單的實時分值GameStatus.Running -> RealTimeBoard(modifier, state.score)// 結(jié)束的話展示豐富的儀表盤GameStatus.Over -> GameOverBoard(modifier, state.score, state.bestScore, clickable)} }// 包含豐富分值和按鈕的Box組件 @Composable fun GameOverBoard(...) {Box(...) {Column(...) {GameOverScoreBoard(Modifier.align(CenterHorizontally),score,maxScore)Spacer(...)GameOverButton(modifier = Modifier.wrapContentSize().align(CenterHorizontally), clickable)}} }

豐富分值和按鈕的可組合函數(shù)的分別實現(xiàn)。

// 展示豐富分值,包括背景邊框、當(dāng)前分值和最高分值 @Composable fun GameOverScoreBoard(...) {Box(...) {// Score board backgroundImage(painter = painterResource(id = R.drawable.score_board_bg),...)Column(...) {LabelScoreField(modifier, R.drawable.score_bg, score)Spacer(modifier = Modifier.wrapContentWidth().height(3.dp))LabelScoreField(modifier, R.drawable.best_score_bg, maxScore)}} }// 重新開始和退出按鈕 @Composable fun GameOverButton(...) {Row(...) {// 重新開始按鈕Image(painter = painterResource(id = R.drawable.restart_button),...modifier = Modifier....clickable(true) {clickable.onRestart()})Spacer(...)// 退出按鈕Image(painter = painterResource(id = R.drawable.exit_button),...modifier = Modifier....clickable(true) {clickable.onExit()})} }

再監(jiān)聽重新開始和退出按鈕的事件,發(fā)送 Restart 和 Exit 的 Action。Exit 的響應(yīng)比較簡單,直接關(guān)閉 Activity 即可。

setContent {FlappyBirdTheme {Surface(color = MaterialTheme.colors.background) {val gameViewModel: GameViewModel = viewModel()Flappy(Clickable(...onRestart = {gameViewModel.dispatch(GameAction.Restart)},onExit = {finish()}))}} }

Restart 則要告知 ViewModel 去重置各種游戲數(shù)據(jù),包括小鳥位置、管道和道路的位置、以及分值,但最高分值數(shù)據(jù)應(yīng)當(dāng)保留下來。

class GameViewModel : ViewModel() {private fun response(action: GameAction, state: ViewState) {viewModelScope.launch {withContext(Dispatchers.Default) {emit(when (action) {GameAction.Restart -> run {state.reset(state.bestScore)}})}}} }data class ViewState(...// 重置State數(shù)據(jù),最高分值除外fun reset(bestScore: Int): ViewState =ViewState(bestScore = bestScore) }

最終效果

給復(fù)刻好的游戲做個 Logo: 采用小鳥的 Icon 和特有的藍(lán)色背景作成的 Adaptive Icon。

從點擊 Logo 到游戲結(jié)束再到重新開始,錄制一段完整游戲。

代碼地址

代碼開源至 https://github.com/ellisonchan/ComposeBird,感謝不吝 Star。

復(fù)刻的效果還是比較完整的,但仍然有不少可以優(yōu)化和擴展的地方:

  • 比如增加簡易模式的選擇。可以從小鳥的升降幅度、管道的間隔、管道移動的速度、連續(xù)出現(xiàn)的組數(shù)等角度入手

  • 增加翅膀扇動的姿態(tài)。實現(xiàn)的話也不難,比如將小鳥的翅膀部分扣出來,在飛翔的過程中不斷地來回 Rotate 一定角度

  • Canvas 自定義描畫。部分視圖元素采用的是圖片,其實也可以通過 Canvas 來實現(xiàn),順道強化一下 Compose 的描畫使用

感興趣的朋友可以 Fork 一下,試著改改!

結(jié)語

復(fù)刻 Flappy Bird 的中途,發(fā)現(xiàn)一位韓國朋友早在去年底就用 Compose 實現(xiàn)過了。忍不住下載試用了一下,發(fā)現(xiàn)只完成了基礎(chǔ)功能,而且實現(xiàn)的思路和我完全不同。

談不上孰優(yōu)孰劣,感興趣的朋友可以看看他的效果和代碼,地址: github.com/elye/demo_android_jetpack_compose_flappy_bird

整個游戲復(fù)刻下來,發(fā)現(xiàn) Compose 數(shù)據(jù)驅(qū)動視圖的思想特別適合游戲開發(fā)。再加上 Compose 對 ViewModel、Flow 和 Coroutines 等技術(shù)的優(yōu)秀支持,使這個過程變得更加簡單和高效。

現(xiàn)在 Jetpack Compose 1.0 已正式發(fā)布!這將是 Android 平臺重要的 UI 編程方式。仍未嘗鮮的朋友,可以安排上了,就從復(fù)刻這個小游戲開始!


長按右側(cè)二維碼

查看更多開發(fā)者精彩分享

"開發(fā)者說·DTalk" 面向中國開發(fā)者們征集 Google 移動應(yīng)用 (apps & games)?相關(guān)的產(chǎn)品/技術(shù)內(nèi)容。歡迎大家前來分享您對移動應(yīng)用的行業(yè)洞察或見解、移動開發(fā)過程中的心得或新發(fā)現(xiàn)、以及應(yīng)用出海的實戰(zhàn)經(jīng)驗總結(jié)和相關(guān)產(chǎn)品的使用反饋等。我們由衷地希望可以給這些出眾的中國開發(fā)者們提供更好展現(xiàn)自己、充分發(fā)揮自己特長的平臺。我們將通過大家的技術(shù)內(nèi)容著重選出優(yōu)秀案例進(jìn)行谷歌開發(fā)技術(shù)專家 (GDE)?的推薦。

?點擊屏末?|?閱讀原文?|?即刻報名參與?"開發(fā)者說·DTalk"?


總結(jié)

以上是生活随笔為你收集整理的Compose 正式发布,来打造一个 Flappy Bird! | 开发者说·DTalk的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。