之前看到fundroid大神用Compose打造了俄罗斯方块游戏,深受启发。便萌生了也打造一个游戏的想法,顺便精进一下Compose的学习。
Flappy Bird
是13年红极一时的小游戏,其简单有趣的玩法和变态的难度形成了强烈反差,引发全球玩家竞相把玩,欲罢不能!遂选择复刻这个小游戏,在实现的过程中向大家演示Compose
工具包的UI组合、数据驱动等重要思想。
Ⅰ.拆解游戏
不记得这个游戏或完全没玩过的朋友,可以点击下面的链接,体验一下Flappy Bird
的玩法。
为拆解游戏,笔者也录了一段游戏过程。
反复观看这段GIF,可以发现游戏的一些规律:
- 远处的建筑和近处的土壤是静止不动的
- 小鸟一直在上下移动,伴随着翅膀和身体的飞翔姿态
- 管道和路面则不断地向左移动,营造出小鸟向前飞翔的视觉效果
通过截图、切图、填充像素和简单的PS,可以拿到各元素的图片。
Ⅱ.复刻画面
各方卡司已就位,接下来开始布置整个画面。暂不实现元素的移动效果,先把静态的整体效果搭建好。
ⅰ.布置远近景
静止不动的建筑远景最为简单,封装到可组合函数FarBackground
里,内部放置一张图片即可。
@Composable
fun FarBackground(modifier: Modifier) {
Column {
Image(
painter = painterResource(id = R.drawable.background),
contentScale = ContentScale.FillBounds,
contentDescription = null,
modifier = modifier.fillMaxSize()
)
}
}
远景的下面由分割线、路面和土壤组成,封装到NearForeground
函数里。通过Modifier
的fraction
参数控制路面和土壤的比例,保证在不同尺寸屏幕上能按比例呈现游戏界面。
@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
函数,通过Column
竖着排列远景和前景。考虑到移动的小鸟和管道需要呈现在远景之上,所以在远景的外面包上一层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()
)
}
}
}
ⅱ.摆放管道
仔细观察管道,会发现一些管道具备朝上朝下、高度随机的特点。为此将管道的视图分拆成盖子和柱子两部分:
- 盖子和柱子的放置顺序决定管道的朝向
- 柱子的高度则控制着管道整体的高度
这样的话,只使用盖子和柱子两张图片,就可以灵活实现各种形态的管道。
先来组合盖子PipeCover
和柱子PipePillar
的可组合函数。
@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)
)
}
管道的可组合函数Pipe
可以根据照朝向和高度的参数,组合成对应的管道。
@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)
}
}
}
}
另外,管道都是成对出现、且无论高度如何中间的间距是固定的。所以我们再实现一个管道组的可组合函数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()
)
}
...
}
}