使用Jetpack Compose构建Flappy Musketeer街机游戏

使用Jetpack Compose构建Flappy Musketeer街机游戏

一步一步创建沉浸式移动游戏的指南

引言

Flappy Musketeer不仅是又一个移动游戏;它将令人上瘾的"轻点飞行"游戏玩法和引人入胜的视觉效果融合在一起,吸引玩家进入埃隆·马斯克(Elon Musk)的非凡事业,包括SpaceX和Twitter(X)。此外,玩家可以通过选择各种主题和配色方案来个性化他们的游戏体验。

在本文中,我们将使用Jetpack Compose从头开始构建Flappy Musketeer。我们将剖析代码、逻辑和设计决策,让您了解创造沉浸式安卓游戏体验的过程。

App实现架构

源代码概述图表

使用Jetpack Compose 设置主题

在Flappy Musketeer中,创建正确的氛围和视觉美学对玩家的体验至关重要。让我们更详细地看看如何实现这一点。

  1. AppTheme Composable

我们的主题系统的核心在于AppTheme Composable。这个Composable负责将选定的配色方案应用到整个游戏的用户界面。以下是它的外观 -

kt 复制代码
@Composable
fun AppTheme(
    colorScheme: ColorScheme = twitter,
    content: @Composable () -> Unit
) {
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = colorScheme.primary.toArgb()
            window.navigationBarColor = colorScheme.primary.toArgb()
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        content = content
    )
}

AppTheme Composable负责配置配色方案,并将其应用于Android设备的状态栏和导航栏。这确保了游戏中始终具有一致的视觉体验。

  1. 自定义配色方案

Flappy Musketeer为玩家提供了各种主题和配色方案可供选择。以下是一些可用选项的概览

Space.X.Mars - 受火星的锈色地形启发。
Twitter.Doge - 一个玩味十足的主题,以Dogecoin吉祥物为特色。
Twitter.White - 白色配色方案的清洁和极简设计。
Space.X.Moon - 受月球宁静美丽的黑暗主题启发。

这些主题在代码中被定义为ColorScheme对象,可以轻松自定义并应用于游戏用户界面的不同部分。

kt 复制代码
val spaceX = darkColorScheme(
    primary = spacePurple,
    secondary = Color.Black,
    tertiary = Color.Black
)

val twitter = darkColorScheme(
    primary = earthYellow,
    secondary = twitterBlue,
    tertiary = Color.Black
)

通过提供各种主题,Flappy Musketeer为玩家提供了个性化的游戏体验。

  1. 主题背景

除了配色方案,Flappy Musketeer还提供了与所选配色方案相匹配的各种背景。这些背景为游戏环境增添了深度和沉浸感。我们使用GameBackground枚举类来维护背景集合。这些背景图像根据所选主题动态加载,确保游戏的视觉效果与玩家的偏好相一致。

kt 复制代码
enum class GameBackground(val url: String) {
    TWITTER_DOGE("https://source.unsplash.com/qIRJeKdieKA"),
    SPACE_X("https://source.unsplash.com/ln5drpv_ImI"),
    SPACE_X_MOON("https://source.unsplash.com/Na0BbqKbfAo"),
    SPACE_X_MARS("https://source.unsplash.com/-_5dCixJ6FI")
}
  1. 在游戏中切换主题

为了使主题选择过程无缝进行,Flappy Musketeer提供了一个getGameTheme函数,该函数以主题名称作为输入,并返回相应的ColorScheme。以下是其工作原理 -

kt 复制代码
fun getGameTheme(gameId: String?): ColorScheme {
    return when (gameId) {
        GameBackground.SPACE_X.name -> spaceX
        GameBackground.TWITTER.name -> twitter
        // ... (other theme mappings)
        else -> twitter
    }
}

这个函数允许游戏根据菜单中的游戏选项选择来切换主题。

通过Flappy Musketeer导航

现在我们已经介绍了使用Jetpack Compose进行主题设置的基础知识,让我们将重点转向游戏的导航。Flappy Musketeer利用Navigation组件来在不同的屏幕和游戏状态之间实现无缝切换。

  1. App Composable
    导航系统的核心是App composable。这个composable使用Jetpack Compose的Navigation组件来设置游戏的导航。以下是它的样子 -
kt 复制代码
@Composable
fun App() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = AppRoutes.MENU.name) {
        composable(AppRoutes.MENU.name) {
            AppTheme(colorScheme = menuTheme) {
                GameMenu(navController)
            }
        }
        composable("${AppRoutes.GAME.name}/{gameId}") {
            val gameId = it.arguments?.getString("gameId")
            val gameTheme = getGameTheme(gameId)

            AppTheme(colorScheme = gameTheme) {
                GameScreen(navController, gameId)
            }
        }

        composable(AppRoutes.GAME_OVER.name) {
            XLottie(navController)
        }
    }
}

这段代码设置了导航图,定义了游戏的流程。让我们来详细解析一下:

App composable 初始化了NavController,用于管理游戏内的导航。

我们将MENU屏幕设置为初始目标。

在NavHost中,我们为每个屏幕定义了可组合函数,例如游戏菜单和游戏界面。

我们使用AppTheme为每个屏幕应用相应的主题,以保持视觉一致性。

  1. 导航目的地

    菜单屏幕(AppRoutes.MENU.name)- 当玩家第一次打开游戏时,他们会进入这个屏幕。AppTheme可组合函数设置了菜单主题,为游戏的主菜单创建了一个统一的外观。玩家可以在这里选择游戏。

游戏屏幕(${AppRoutes.GAME.name}/{gameId})- 游戏屏幕根据所选的游戏动态调整其主题。使用getGameTheme函数,我们获取相应的配色方案,并使用AppTheme应用它。这确保每个游戏的主题与其环境相匹配,无论是Twitter.Doge还是Space.X.Mars。

游戏结束屏幕(AppRoutes.GAME_OVER.name)- 当游戏结束时,显示此屏幕,并带有一个Twitter X Lottie动画。当玩家的游戏结束时,导航系统会无缝地过渡到这个屏幕。

通过这种导航设置,玩家可以轻松浏览Flappy Musketeer的不同部分,从选择游戏到游戏过程中,再到游戏结束屏幕的体验。

游戏菜单通常是玩家的第一个互动点,为整个游戏体验设定了基调。在Flappy Musketeer中,游戏菜单被设计成视觉上引人入胜且用户友好的。让我们更详细地看一下它是如何实现的

  1. GameMenu可组合函数
    GameMenu可组合函数是进入Flappy Musketeer的入口点。它为玩家提供了访问各种游戏主题和重要链接的方式。以下是定义GameMenu的代码片段 -
kt 复制代码
@Composable
fun GameMenu(navController: NavController) {
    val uriHandler = LocalUriHandler.current

    // ... (Theme setup)

    Column(modifier = Modifier.fillMaxSize()) {
        // ... (Top bar styling)

        Column(
            modifier = Modifier
                .wrapContentHeight()
                .fillMaxWidth()
                .background(Color(0xFF0E2954)),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            // ... (Logo and app name)

            Spacer(modifier = Modifier.height(10.dp))

            Row {
                // ... (About App button)

                Spacer(modifier = Modifier.width(12.dp))

                // ... (Creator button)
            }
        }

        Row(
            modifier = Modifier
                .fillMaxSize()
                .horizontalScroll(rememberScrollState(), enabled = true)
                .background(
                    brush = Brush.verticalGradient(
                        listOf(
                            Color(0xFF0E2954),
                            Color(0xFF1F6E8C)
                        )
                    )
                ),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Spacer(modifier = Modifier.width(30.dp))

            GameBackground.values().forEach {
                MenuItem(
                    navController,
                    backgroundUrl = it.url,
                    name = it.name.replace("_", ".").uppercase(),
                    originalName = it.name
                )
                Spacer(modifier = Modifier.width(30.dp))
            }
        }
    }
}
  • 游戏的标志以IconButton的形式显示,当点击时会打开一个链接。这为菜单屏幕增添了一丝互动性。
  • 应用程序名称使用buildAnnotatedString进行样式设置,允许自定义字体粗细和样式。
  • 菜单以水平滚动行的形式实现,允许玩家通过滑动浏览不同的主题。每个主题都表示为一个MenuItem可组合函数,具有其背景图像和名称。
  1. MenuItem Composable
kt 复制代码
@Composable
fun MenuItem(
    navController: NavController,
    backgroundUrl: String,
    name: String,
    originalName: String
) {
    // ... (Menu item content)
}

这个可组合函数负责显示主题的背景图像和名称。当单击时,它会根据所选主题将玩家导航到相应的游戏屏幕。

主屏幕逻辑

GameScreen可组合函数是Flappy Musketeer游戏中负责管理游戏逻辑的核心组件。这个代码文件定义了游戏在游戏过程中的行为,包括处理用户输入、更新游戏状态和渲染游戏元素。

让我们逐步分解GameScreen可组合函数,并用相关的代码片段解释每部分代码

  1. 状态初始化
kt 复制代码
// 初始化游戏状态和得分
var gameState by remember { mutableStateOf(GameState.NOT_STARTED) }
var score by remember { mutableLongStateOf(0L) }
var lastScore by remember { mutableLongStateOf(preferencesManager.getData("last_score", 0L)) }
var bestScore by remember { mutableLongStateOf(preferencesManager.getData("best_score", 0L)) }
var birdOffset by remember { mutableStateOf(0.dp) }
var birdRect by remember { mutableStateOf(Rect(0f, 0f, 64.dp.value, 64.dp.value)) }

在这一部分,我们初始化各种游戏状态变量,如gameState、score、lastScore、bestScore、birdOffset和birdRect。这些变量用于跟踪游戏的进展和鸟的位置。

  1. 管道(障碍物)尺寸初始化
kt 复制代码
var pipeDimensions by remember {
    mutableStateOf(Triple(0.1f, 0.4f, 0.5f))
}

在这里,我们将pipeDimensions初始化为Triple,以存储顶部、间隙和底部管道的权重。这些权重确定了管道的相对大小。

管道(障碍物)尺寸初始化

通过remember创建一个 mutableStateOf,将 pipeDimensions 初始化为 Triple(0.1f, 0.4f, 0.5f)。这个 Triple 存储了顶部管道、间隙和底部管道的权重,这些权重决定了管道的相对大小。

  1. 更新分数回调
kt 复制代码
val updateScoreCallback: (Long) -> Unit = {
    score += it
}

updateScoreCallback 是一个回调函数,用于在必要时更新游戏的分数。

  1. 小鸟下落动画
kt 复制代码
LaunchedEffect(key1 = birdOffset, gameState) {
    while (gameState == GameState.PLAYING) {
        delay(16)
        birdOffset += 4.dp
    }
}

在这部分中,我们使用 LaunchedEffect 来持续更新 birdOffset,并在游戏处于 PLAYING 状态时模拟小鸟的下落。

  1. 更新小鸟和管道的矩形区域
kt 复制代码
val updateBirdRect: (birdRect: Rect) -> Unit = {
    birdRect = it
    pipeDimensions = getPipeDimensions(it, screenHeight)
}

这些回调函数负责更新小鸟和管道的矩形区域。这些矩形区域对于检测小鸟和管道之间的碰撞非常重要。

  1. 碰撞检测
kt 复制代码
val updatePipeRect: (pipeRect: Rect) -> Unit = {
    if (!it.intersect(birdRect).isEmpty) {
        // 处理与管道的碰撞
        // ...
    }
}

这个回调函数处理小鸟和管道之间的碰撞检测。当检测到碰撞时,游戏状态转变为 COMPLETED,并更新最高分和最近分数。此外,我们还导航到游戏结束界面(详见导航部分)。

  1. 点击手势处理
kt 复制代码
Box(
    modifier = Modifier
        .fillMaxSize()
        .pointerInput(Unit) {
            detectTapGestures(
                onTap = {
                    if (gameState == GameState.PLAYING) {
                        // 处理小鸟跳跃
                        coroutineScope.launch {
                            var offsetChange = 80.dp
                            while (offsetChange > 0.dp) {
                                birdOffset -= 2.dp
                                delay(2L)
                                offsetChange -= 2.dp
                            }
                        }
                    }
                }
            )
        }
)

在这里,我们设置了点击手势处理。当玩家在游戏过程中点击屏幕时,更新小鸟的位置以模拟跳跃。

  1. 游戏布局
  2. Box Composable ---
kt 复制代码
Box(
    modifier = Modifier.fillMaxSize()
) {
    // ...
}

游戏布局被封装在一个 Box composable 中,允许在其上方放置多个组件。

  1. 背景 ---
kt 复制代码
Background()

Background composable 渲染游戏的背景,并根据选择的主题设置适当的背景图像。

  1. 管道 ---
kt 复制代码
Pipes(
    updatePipeRect = updatePipeRect,
    updateScoreCallback = updateScoreCallback,
    gameState = gameState,
    pipeDimensions = pipeDimensions.copy()
)

Pipes composable 管理管道的生成和移动。它处理与小鸟的碰撞检测并更新分数。

  1. 游戏状态处理 ---
kt 复制代码
when (gameState) {
    // ...
}

这部分使用 when 表达式处理不同的游戏状态 ---

GameState.PLAYING --- 在游戏进行中显示小鸟、分数和暂停按钮。点击暂停按钮触发暂停回调。

GameState.NOT_STARTED, GameState.COMPLETED --- 显示"Play"按钮以开始或重新开始游戏。如果有可用的最近分数和最高分,则显示上次分数和最高分。

GameState.PAUSE --- 显示"Play"按钮以恢复游戏。

  1. 小鸟 ---
kt 复制代码
Bird(birdOffset, updateBirdRect)

Bird composable 在屏幕上渲染小鸟角色。birdOffset 决定了小鸟的垂直位置,模拟其移动。

  1. Play 按钮 ---
kt 复制代码
Play(onPlayCallback)
  • Play composable 显示"Play"按钮,允许玩家在点击时开始或恢复游戏。按下按钮时触发 onPlayCallback。
  1. 地面 ---
kt 复制代码
Ground("Flappy Score", score, enablePause = true, onPauseCallback)

Ground composable 显示游戏的分数,并在 enablePause 设置为 true 时包含一个可选的暂停按钮。按下暂停按钮时触发 onPauseCallback

kt 复制代码
@Composable
fun Bird(birdOffset: Dp, updateBirdRect: (Rect) -> Unit) {

    val bird = when (MaterialTheme.colorScheme.primary) {
        spaceX.primary, spaceXMars.primary, spaceXMoon.primary -> {
            R.drawable.space
        }

        twitterDoge.primary -> {
            R.drawable.doge
        }

        else -> R.drawable.bird
    }

    bird.let {
        Box(
            modifier = Modifier
                .size(64.dp)
                .offset(y = birdOffset)
                .padding(5.dp)
                .onGloballyPositioned {
                    updateBirdRect(it.boundsInRoot())
                }
        ) {
            when (MaterialTheme.colorScheme.primary) {
                spaceX.primary, spaceXMoon.primary, spaceXMars.primary -> {
                    Image(painterResource(id = it), contentDescription = "rocket")
                }

                else -> {
                    if (MaterialTheme.colorScheme.primary == twitterDoge.primary) {
                        Image(painterResource(id = it), contentDescription = "doge rocket")
                    } else {
                        Icon(
                            painterResource(id = it),
                            tint = MaterialTheme.colorScheme.secondary,
                            contentDescription = "bird"
                        )
                    }
                }
            }
        }
    }
}

深入了解每个组件

在本节中,我们将深入探讨《Flappy Musketeer》游戏的游戏布局。我们将探索构成游戏界面的以下关键组件:

1. 小鸟( Bird)

kt 复制代码
@Composable
fun Bird(birdOffset: Dp, updateBirdRect: (Rect) -> Unit) {

    val bird = when (MaterialTheme.colorScheme.primary) {
        spaceX.primary, spaceXMars.primary, spaceXMoon.primary -> {
            R.drawable.space
        }

        twitterDoge.primary -> {
            R.drawable.doge
        }

        else -> R.drawable.bird
    }

    bird.let {
        Box(
            modifier = Modifier
                .size(64.dp)
                .offset(y = birdOffset)
                .padding(5.dp)
                .onGloballyPositioned {
                    updateBirdRect(it.boundsInRoot())
                }
        ) {
            when (MaterialTheme.colorScheme.primary) {
                spaceX.primary, spaceXMoon.primary, spaceXMars.primary -> {
                    Image(painterResource(id = it), contentDescription = "rocket")
                }

                else -> {
                    if (MaterialTheme.colorScheme.primary == twitterDoge.primary) {
                        Image(painterResource(id = it), contentDescription = "doge rocket")
                    } else {
                        Icon(
                            painterResource(id = it),
                            tint = MaterialTheme.colorScheme.secondary,
                            contentDescription = "bird"
                        )
                    }
                }
            }
        }
    }
}

在《Flappy Musketeer》游戏中,Bird 组合负责渲染玩家控制的角色,通常称为"小鸟",玩家通过控制小鸟穿越管道。让我们详细了解这个组合的关键方面:

  • birdOffset - 这个参数表示小鸟的垂直偏移量,表示其在屏幕上的位置。它根据玩家的输入和重力进行更新,模拟小鸟的飞翔和下落。
  • updateBirdRect - 一个回调函数,用于更新小鸟的位置和尺寸,以进行碰撞检测。

在composable中

  1. 根据当前主题的主色调,将 bird 变量分配给图像资源。游戏根据所选主题提供不同的小鸟图像,例如"spaceX"、"spaceXMars"、"spaceXMoon"或"twitterDoge"。
  2. 使用 Box 组合来容纳小鸟图像。它有一个固定大小为64x64密度无关像素(dp),并根据 birdOffset 在垂直方向上定位。
  3. 使用 onGloballyPositioned 修饰符来检测小鸟在屏幕上的位置,并使用其边界调用 updateBirdRect 回调函数。

Box 的内容根据主题而变化

  • 对于太空主题(spaceX、spaceXMars、spaceXMoon),它显示一个带有火箭图像的 Image,代表小鸟。
  • 对于"twitterDoge"主题,它显示一个带有 doge 主题的火箭图像的 Image。
  • 对于其他主题,它显示一个带有小鸟图像的图标,图标的颜色会根据当前主题的次要颜色进行着色。

这个组合允许根据游戏的主题以不同的方式呈现小鸟角色,为玩家提供与所选主题相匹配的视觉体验。

2. 管道(障碍物)

A)主要组合

管道组件负责在游戏中渲染和管理玩家必须穿过的管道。它还处理生成和移动管道的逻辑,这些都是基于游戏事件的。

关键点

updatePipeRect - 一个回调函数,用于更新管道的位置和尺寸,以进行碰撞检测。

updateScoreCallback - 一个回调函数,用于更新玩家的分数。

gameState - 表示游戏的当前状态(例如,正在播放,已完成)。

pipeDimension - 代表顶部,间隙和底部管道的权重的元组。

管道组件根据游戏状态和经过的时间来管理管道的创建和移动。

B)管道数据类

kt 复制代码
data class Pipe(
val width:Dp = 100.dp,
val topPipeWeight:Float,
val gapWeight:Float,
val bottomPipeWeight:Float,
var position:Dp,
)

Pipe是表示游戏中单个管道的数据类。它包含诸如宽度,顶部,间隙和底部管道的权重以及其在屏幕上的位置等属性。

C)管道生成逻辑

kt 复制代码
if(System.currentTimeMillis()- PipeTime.lastPipeAddedTime> = 2000L){
//生成新管道并将其添加到列表中
//...
//添加逻辑
val addToList = if(pipes.isNotEmpty()){
abs(pipes.last()。position.minus(newPipe.position)。value)> 500f
} else {
true
}

如果满足条件,则添加到列表中。更新得分回调函数会在生成新管道时调用以更新玩家的分数。

D)管道运动

kt 复制代码
//从右到左移动管道
LaunchedEffect(key1 = pipes.size,gameState){
while(gameState == GameState.PLAYING){
delay(16L)
pipes = pipes.map {pipe->
val newPosition = pipe.position-pipeSpeed
pipe.copy(position = newPosition)
} .filter {pipe->
pipe.position>(-pipeWidth)//从屏幕上删除
}
}
}

使用LaunchedEffect将管道从右向左移动。该效果在游戏处于"播放"状态时运行。

delay(16L)确保以一致的速率移动管道,提供平滑的动画效果。

通过映射每个管道的位置来更新管道列表,减去管道速度(pipeSpeed)。从屏幕上删除的管道(position < -pipeWidth)将从列表中删除。

E)GapPipe组合

kt 复制代码
@Composable
fun GapPipe(pipe:Pipe,updatePipeRect:(Rect) - > Unit){
//...
}

GapPipe组合负责呈现单个管道及其间隙。

kt 复制代码
.onGloballyPositioned {
val pipeRect = it.boundsInRoot()
updatePipeRect(pipeRect)
}

它接受一个Pipe对象和一个回调函数updatePipeRect以进行碰撞检测。

F)管道尺寸计算

kt 复制代码
fun getPipeDimensions(
birdPosition:Rect,
screenHeight:Dp
):Triple<Float,Float,Float> {
//...
}
  • getPipeDimensions函数根据鸟的位置和屏幕高度计算顶部,间隙和底部管道的权重(相对高度)。

  • 它确保生成的管道权重在一定限制范围内,以创建具有挑战性但公平的游戏。

3. 地面和分数显示

Flappy Musketeer游戏中的Ground组合负责呈现显示与游戏相关的信息的地面区域。以下是这个组合的说明

kt 复制代码
@Composable
fun Ground(
label:String,
score:Long,
enablePause:Boolean = false,
onPauseCallback:() - > Unit = {}
){
//...
}

关键点

  • label-表示地面区域的标签或标题的字符串。

  • score-表示玩家得分的长整数。

  • enablePause-一个布尔值,指示是否启用暂停按钮。默认情况下设置为false。

  • onPauseCallback-单击暂停按钮时调用的回调函数。默认情况下,它是一个空函数。

Ground组合创建一个可视的地面区域,显示标签,得分和可选的暂停按钮。它用于提供与游戏进度相关的信息和交互。

4. 游戏按钮

kt 复制代码
@Composable
fun Play(onPlayCallback:() - > Unit){
//...
}

关键点

  • onPlayCallback-此参数是单击播放按钮时将调用的回调函数。通常会触发游戏的开始或重新开始。

  • Play组合创建一个视觉上吸引人的播放按钮,与游戏的主题相匹配。

结论

在Flappy Musketeer游戏中,我们开始了一段充满激情的旅程,利用Jetpack Compose的强大功能创建Android移动游戏。我们深入探讨了主题、导航、游戏菜单和游戏屏幕逻辑,剖析每个方面,为您提供打造自己沉浸式游戏体验所需的工具。

当您踏上游戏开发之旅时,请记住Jetpack Compose为创建视觉上令人惊叹和引人入胜的Android游戏开启了无限可能。因此,前进吧,释放您的创造力,构建出一些酷炫的东西!

GitHub

https://github.com/nirbhayph/flappymusketeer/

相关推荐
阿巴斯甜3 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker4 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95275 小时前
Andorid Google 登录接入文档
android
黄林晴6 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab18 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿21 小时前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android