使用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/

相关推荐
长亭外的少年5 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿7 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神9 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛9 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法9 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter11 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快12 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl12 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江12 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
Dnelic-12 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记