亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

利用Jetpack?Compose復(fù)刻游戲Flappy?Bird

 更新時間:2022年02月15日 10:10:37   作者:ax2djmti  
Flappy?Bird是13年紅極一時的小游戲,其簡單有趣的玩法和變態(tài)的難度形成了強烈反差,引發(fā)全球玩家競相把玩!本文將通過Jetpack?Compose復(fù)刻這一游戲,感興趣的小伙伴可以了解一下

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

1.拆解游戲

不記得這個游戲或完全沒玩過的朋友,可以點擊下面的鏈接,體驗一下Flappy Bird的玩法。

https://flappybird.io/

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

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

  • 遠處的建筑和近處的土壤是靜止不動的
  • 小鳥一直在上下移動,伴隨著翅膀和身體的飛翔姿態(tài)
  • 管道和路面則不斷地向左移動,營造出小鳥向前飛翔的視覺效果

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

2.復(fù)刻畫面

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

ⅰ.布置遠近景

靜止不動的建筑遠景最為簡單,封裝到可組合函數(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()  
????????)  
????}  
}

遠景的下面由分割線、路面和土壤組成,封裝到NearForeground函數(shù)里。通過Modifierfraction參數(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豎著排列遠景和前景??紤]到移動的小鳥和管道需要呈現(xià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()  
????????????)  
????????}  
????}  
}

ⅱ.擺放管道

仔細觀察管道,會發(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())  
????????????  
????????????//?管道對添加遠景上去  
????????????PipeCouple(  
????????????????modifier?=?Modifier.fillMaxSize()  
????????????)  
????????}  
????????...  
????}  
}

ⅲ.放置小鳥

小鳥通過Image組件即可實現(xià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(?...?)  
????????????//?將小鳥添加到遠景上去  
????????????Bird(  
????????????????modifier?=?Modifier.fillMaxSize(),  
????????????????state?=?viewState  
????????????)  
????????}  
????}  
}

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

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

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

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

Compose通過State驅(qū)動可組合函數(shù)進行重組,進而達到畫面的重繪。所以我們將這些數(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  
????????????????????????)  
????????????????????}  
????????????????????...  
????????????????})  
????????????}  
????????}  
????}  
}

4.路面動起來

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

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

@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?->  
????????????????//?任意路面的偏移值達到兩張圖片位置差的時候  
????????????????//?重置路面位置,重新回到屏幕外  
????????????????if?(roadState.offset?<=?-?TempRoadWidthOffset)?{  
????????????????????viewModel.dispatch(GameAction.RoadExit,?roadIndex?=?index)  
????????????????}  
????????????}  
????????}  
????}  
}

ViewModel收到RoadExit的Action之后通知路面State進行位置的重置。

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)  
}

5.管道動起來

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

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

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

畫面重組的時候判斷管道偏移值是否達到屏幕寬度,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ā)送重置Action  
????????????if?(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)?/?2  
  
val?PipeStateList?=?listOf(  
????PipeState(),  
????PipeState(offset?=?(SecondPipeWidthOffset))  
)

6.小鳥飛起來

不斷調(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)?LiftingDegree  
????????else?if?(state.isFalling)?FallingDegree  
????????else?PendingDegree  
  
????Box(...)?{  
????????var?correctBirdHeight?=?state.birdState.birdHeight  
????????if?(state.playZoneSize.second?>?0)?{  
????????????...  
????????????val?fallingThreshold?=?BirdHitGroundThreshold  
????????????//?小鳥偏移值達到背景邊界時發(fā)送落地Action  
????????????if?(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)  
????????)  
????}  
}

image

7.碰撞和實時分值

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

仔細思考,發(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?+?birdHeightOffset  
????????val?birdBottom?=?(zoneHeight?+?BirdSizeHeight)?/?2?+?birdHeightOffset  
????????//?管道移動到小鳥運動區(qū)域并和小鳥重合  
????????if?(birdTop?<?pipeState.upHeight?||?birdBottom?>?zoneHeight?-?pipeState.downHeight)?{  
????????????return?PipeStatus.BirdHit  
????????}  
????????return?PipeStatus.BirdCrossing  
????}  
?}

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

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更新為Dying  
????????????????????????????gameStatus?=?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)?LiftingDegree  
????????else?if?(state.isFalling)?FallingDegree  
????????else?if?(state.isQuickFalling)?DyingDegree  
????????else?if?(state.isOver)?DeadDegree  
????????else?PendingDegree  
}

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

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

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

Compose的Preview功能很好用,但要留意一點:其Composable函數(shù)里不要放入ViewModel邏輯,否則會渲染失敗。我們可以拆分UI和ViewModel邏輯,在保證Preview能順利進行的同時能復(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?background  
????????Image(  
????????????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ā)送RestartExit的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)  
}

9.最終效果

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

圖片

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

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

1.比如增加簡易模式的選擇??梢詮男▲B的升降幅度、管道的間隔、管道移動的速度、連續(xù)出現(xiàn)的組數(shù)等角度入手

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

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

以上就是利用Jetpack Compose復(fù)刻游戲Flappy Bird的詳細內(nèi)容,更多關(guān)于Jetpack Compose Flappy Bird游戲的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Android布局技巧之使用ViewStub

    Android布局技巧之使用ViewStub

    這篇文章主要為大家詳細介紹了Android布局技巧之使用ViewStub,具有一定的實用性,感興趣的小伙伴們可以參考一下
    2016-06-06
  • 詳解關(guān)于MIUI 9沉浸式狀態(tài)欄的最新適配

    詳解關(guān)于MIUI 9沉浸式狀態(tài)欄的最新適配

    由于各系統(tǒng)版本的限制,沉浸式狀態(tài)欄對系統(tǒng)有要求,本篇文章主要介紹了詳解關(guān)于MIUI 9沉浸式狀態(tài)欄的最新適配,非常具有實用價值,需要的朋友可以參考下
    2018-05-05
  • Android自定義View詳解

    Android自定義View詳解

    這篇文章主要為大家詳細介紹了Android自定義View,幫助大家戰(zhàn)勝Android自定義View,為今后的學(xué)習(xí)打下基礎(chǔ),感興趣的小伙伴們可以參考一下
    2016-06-06
  • android中soap協(xié)議使用(ksoap調(diào)用webservice)

    android中soap協(xié)議使用(ksoap調(diào)用webservice)

    kSOAP是如何調(diào)用ebservice的呢,首先要使用SoapObject,這是一個高度抽象化的類,完成SOAP調(diào)用。可以調(diào)用它的addProperty方法填寫要調(diào)用的webservice方法的參數(shù)
    2014-02-02
  • 兩分鐘讓你徹底明白Android Activity生命周期的詳解(圖文介紹)

    兩分鐘讓你徹底明白Android Activity生命周期的詳解(圖文介紹)

    本篇文章是對Android的生命周期進行了詳細的分析介紹,需要的朋友參考下
    2013-05-05
  • Android ContentProvider基礎(chǔ)應(yīng)用詳解

    Android ContentProvider基礎(chǔ)應(yīng)用詳解

    ContentProvider是android四大組件之一。它是不同應(yīng)用程序之間交換數(shù)據(jù)的標(biāo)準(zhǔn)api,ContentProvider以某種uri的形式對外提供數(shù)據(jù),允許其它應(yīng)用程序?qū)ζ湓L問或者修改數(shù)據(jù)。本文將介紹ContentProvider的基礎(chǔ)應(yīng)用,感興趣的可以學(xué)習(xí)一下
    2021-12-12
  • android上一個可追蹤代碼具體到函數(shù)某行的日志類

    android上一個可追蹤代碼具體到函數(shù)某行的日志類

    追蹤代碼到函數(shù)具體某行,這樣的功能,是每一個程序員都希望會有的,因為它可以幫助我們追蹤到某行代碼的錯誤,接下來介紹下android上一個可追蹤代碼到函數(shù)具體某行的日志類,希望對開發(fā)者有所幫助
    2012-12-12
  • Android仿QQ聊天撒花特效 很真實

    Android仿QQ聊天撒花特效 很真實

    本文寫的這個特效,是關(guān)于聊天的,你肯定遇到過,就是你跟人家聊天的時候,比如發(fā)送應(yīng)(么么噠),然后屏幕上全部就是表情了,今天我們就是做這個,撒花的特效,感興趣的小伙伴們可以參考一下
    2016-05-05
  • Android EventBus 3.0.0 使用總結(jié)(必看篇)

    Android EventBus 3.0.0 使用總結(jié)(必看篇)

    下面小編就為大家?guī)硪黄狝ndroid EventBus 3.0.0 使用總結(jié)(必看篇)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-05-05
  • android studio 使用Mocklocation虛擬定位

    android studio 使用Mocklocation虛擬定位

    這篇文章主要介紹了android studio 使用Mocklocation虛擬定位總結(jié),本文分步驟給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-12-12

最新評論