一步一步创建沉浸式移动游戏的指南
Flappy Musketeer不仅是又一个移动游戏;它将令人上瘾的“轻点飞行”游戏玩法和引人入胜的视觉效果融合在一起,吸引玩家进入埃隆·马斯克(Elon Musk)的非凡事业,包括SpaceX和Twitter(X)。此外,玩家可以通过选择各种主题和配色方案来个性化他们的游戏体验。
在本文中,我们将使用Jetpack Compose从头开始构建Flappy Musketeer。我们将剖析代码、逻辑和设计决策,让您了解创造沉浸式安卓游戏体验的过程。
在Flappy Musketeer中,创建正确的氛围和视觉美学对玩家的体验至关重要。让我们更详细地看看如何实现这一点。
我们的主题系统的核心在于AppTheme Composable。这个Composable负责将选定的配色方案应用到整个游戏的用户界面。以下是它的外观 -
@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设备的状态栏和导航栏。这确保了游戏中始终具有一致的视觉体验。
Flappy Musketeer为玩家提供了各种主题和配色方案可供选择。以下是一些可用选项的概览
Space.X.Mars
- 受火星的锈色地形启发。
Twitter.Doge
- 一个玩味十足的主题,以Dogecoin吉祥物为特色。
Twitter.White
- 白色配色方案的清洁和极简设计。
Space.X.Moon
- 受月球宁静美丽的黑暗主题启发。
这些主题在代码中被定义为ColorScheme
对象,可以轻松自定义并应用于游戏用户界面的不同部分。
val spaceX = darkColorScheme(
primary = spacePurple,
secondary = Color.Black,
tertiary = Color.Black
)
val twitter = darkColorScheme(
primary = earthYellow,
secondary = twitterBlue,
tertiary = Color.Black
)
通过提供各种主题,Flappy Musketeer为玩家提供了个性化的游戏体验。
除了配色方案,Flappy Musketeer还提供了与所选配色方案相匹配的各种背景。这些背景为游戏环境增添了深度和沉浸感。我们使用GameBackground枚举类来维护背景集合。这些背景图像根据所选主题动态加载,确保游戏的视觉效果与玩家的偏好相一致。
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")
}
为了使主题选择过程无缝进行,Flappy Musketeer提供了一个getGameTheme函数,该函数以主题名称作为输入,并返回相应的ColorScheme。以下是其工作原理 -
fun getGameTheme(gameId: String?): ColorScheme {
return when (gameId) {
GameBackground.SPACE_X.name -> spaceX
GameBackground.TWITTER.name -> twitter
// ... (other theme mappings)
else -> twitter
}
}
这个函数允许游戏根据菜单中的游戏选项选择来切换主题。
现在我们已经介绍了使用Jetpack Compose进行主题设置的基础知识,让我们将重点转向游戏的导航。Flappy Musketeer利用Navigation组件来在不同的屏幕和游戏状态之间实现无缝切换。
@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为每个屏幕应用相应的主题,以保持视觉一致性。
游戏屏幕(${AppRoutes.GAME.name}/{gameId})- 游戏屏幕根据所选的游戏动态调整其主题。使用getGameTheme函数,我们获取相应的配色方案,并使用AppTheme应用它。这确保每个游戏的主题与其环境相匹配,无论是Twitter.Doge还是Space.X.Mars。
游戏结束屏幕(AppRoutes.GAME_OVER.name)- 当游戏结束时,显示此屏幕,并带有一个Twitter X Lottie动画。当玩家的游戏结束时,导航系统会无缝地过渡到这个屏幕。
通过这种导航设置,玩家可以轻松浏览Flappy Musketeer的不同部分,从选择游戏到游戏过程中,再到游戏结束屏幕的体验。
游戏菜单通常是玩家的第一个互动点,为整个游戏体验设定了基调。在Flappy Musketeer中,游戏菜单被设计成视觉上引人入胜且用户友好的。让我们更详细地看一下它是如何实现的
@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
可组合函数,具有其背景图像和名称。@Composable
fun MenuItem(
navController: NavController,
backgroundUrl: String,
name: String,
originalName: String
) {
// ... (Menu item content)
}
这个可组合函数负责显示主题的背景图像和名称。当单击时,它会根据所选主题将玩家导航到相应的游戏屏幕。
GameScreen可组合函数是Flappy Musketeer游戏中负责管理游戏逻辑的核心组件。这个代码文件定义了游戏在游戏过程中的行为,包括处理用户输入、更新游戏状态和渲染游戏元素。
让我们逐步分解GameScreen可组合函数,并用相关的代码片段解释每部分代码
// 初始化游戏状态和得分
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
。这些变量用于跟踪游戏的进展和鸟的位置。
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 存储了顶部管道、间隙和底部管道的权重,这些权重决定了管道的相对大小。
val updateScoreCallback: (Long) -> Unit = {
score += it
}
updateScoreCallback
是一个回调函数,用于在必要时更新游戏的分数。
LaunchedEffect(key1 = birdOffset, gameState) {
while (gameState == GameState.PLAYING) {
delay(16)
birdOffset += 4.dp
}
}
在这部分中,我们使用 LaunchedEffect
来持续更新 birdOffset
,并在游戏处于 PLAYING
状态时模拟小鸟的下落。
val updateBirdRect: (birdRect: Rect) -> Unit = {
birdRect = it
pipeDimensions = getPipeDimensions(it, screenHeight)
}
这些回调函数负责更新小鸟和管道的矩形区域。这些矩形区域对于检测小鸟和管道之间的碰撞非常重要。
val updatePipeRect: (pipeRect: Rect) -> Unit = {
if (!it.intersect(birdRect).isEmpty) {
// 处理与管道的碰撞
// ...
}
}
这个回调函数处理小鸟和管道之间的碰撞检测。当检测到碰撞时,游戏状态转变为 COMPLETED
,并更新最高分和最近分数。此外,我们还导航到游戏结束界面(详见导航部分)。
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
}
}
}
}
)
}
)
在这里,我们设置了点击手势处理。当玩家在游戏过程中点击屏幕时,更新小鸟的位置以模拟跳跃。
Box(
modifier = Modifier.fillMaxSize()
) {
// ...
}
游戏布局被封装在一个 Box composable 中,允许在其上方放置多个组件。
Background()
Background composable 渲染游戏的背景,并根据选择的主题设置适当的背景图像。
Pipes(
updatePipeRect = updatePipeRect,
updateScoreCallback = updateScoreCallback,
gameState = gameState,
pipeDimensions = pipeDimensions.copy()
)
Pipes composable 管理管道的生成和移动。它处理与小鸟的碰撞检测并更新分数。
when (gameState) {
// ...
}
这部分使用 when 表达式处理不同的游戏状态 —
GameState.PLAYING — 在游戏进行中显示小鸟、分数和暂停按钮。点击暂停按钮触发暂停回调。
GameState.NOT_STARTED, GameState.COMPLETED — 显示“Play”按钮以开始或重新开始游戏。如果有可用的最近分数和最高分,则显示上次分数和最高分。
GameState.PAUSE — 显示“Play”按钮以恢复游戏。
Bird(birdOffset, updateBirdRect)
Bird composable 在屏幕上渲染小鸟角色。birdOffset 决定了小鸟的垂直位置,模拟其移动。
Play(onPlayCallback)
Ground("Flappy Score", score, enablePause = true, onPauseCallback)
Ground composable 显示游戏的分数,并在 enablePause
设置为 true 时包含一个可选的暂停按钮。按下暂停按钮时触发 onPauseCallback
。
@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》游戏的游戏布局。我们将探索构成游戏界面的以下关键组件:
@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中
birdOffset
在垂直方向上定位。onGloballyPositioned
修饰符来检测小鸟在屏幕上的位置,并使用其边界调用 updateBirdRect
回调函数。Box 的内容根据主题而变化
这个组合允许根据游戏的主题以不同的方式呈现小鸟角色,为玩家提供与所选主题相匹配的视觉体验。
2. 管道(障碍物)
A)主要组合
管道组件负责在游戏中渲染和管理玩家必须穿过的管道。它还处理生成和移动管道的逻辑,这些都是基于游戏事件的。
关键点
updatePipeRect
- 一个回调函数,用于更新管道的位置和尺寸,以进行碰撞检测。
updateScoreCallback
- 一个回调函数,用于更新玩家的分数。
gameState
- 表示游戏的当前状态(例如,正在播放,已完成)。
pipeDimension
- 代表顶部,间隙和底部管道的权重的元组。
管道组件根据游戏状态和经过的时间来管理管道的创建和移动。
B)管道数据类
data class Pipe(
val width:Dp = 100.dp,
val topPipeWeight:Float,
val gapWeight:Float,
val bottomPipeWeight:Float,
var position:Dp,
)
Pipe是表示游戏中单个管道的数据类。它包含诸如宽度,顶部,间隙和底部管道的权重以及其在屏幕上的位置等属性。
C)管道生成逻辑
if(System.currentTimeMillis()- PipeTime.lastPipeAddedTime> = 2000L){
//生成新管道并将其添加到列表中
//...
//添加逻辑
val addToList = if(pipes.isNotEmpty()){
abs(pipes.last()。position.minus(newPipe.position)。value)> 500f
} else {
true
}
如果满足条件,则添加到列表中。更新得分回调函数会在生成新管道时调用以更新玩家的分数。
D)管道运动
//从右到左移动管道
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组合
@Composable
fun GapPipe(pipe:Pipe,updatePipeRect:(Rect) - > Unit){
//...
}
GapPipe组合负责呈现单个管道及其间隙。
.onGloballyPositioned {
val pipeRect = it.boundsInRoot()
updatePipeRect(pipeRect)
}
它接受一个Pipe对象和一个回调函数updatePipeRect
以进行碰撞检测。
F)管道尺寸计算
fun getPipeDimensions(
birdPosition:Rect,
screenHeight:Dp
):Triple<Float,Float,Float> {
//...
}
getPipeDimensions
函数根据鸟的位置和屏幕高度计算顶部,间隙和底部管道的权重(相对高度)。
它确保生成的管道权重在一定限制范围内,以创建具有挑战性但公平的游戏。
3. 地面和分数显示
Flappy Musketeer游戏中的Ground组合负责呈现显示与游戏相关的信息的地面区域。以下是这个组合的说明
@Composable
fun Ground(
label:String,
score:Long,
enablePause:Boolean = false,
onPauseCallback:() - > Unit = {}
){
//...
}
关键点
label
-表示地面区域的标签或标题的字符串。
score
-表示玩家得分的长整数。
enablePause
-一个布尔值,指示是否启用暂停按钮。默认情况下设置为false。
onPauseCallback
-单击暂停按钮时调用的回调函数。默认情况下,它是一个空函数。
Ground组合创建一个可视的地面区域,显示标签,得分和可选的暂停按钮。它用于提供与游戏进度相关的信息和交互。
@Composable
fun Play(onPlayCallback:() - > Unit){
//...
}
关键点
onPlayCallback
-此参数是单击播放按钮时将调用的回调函数。通常会触发游戏的开始或重新开始。
Play组合创建一个视觉上吸引人的播放按钮,与游戏的主题相匹配。
在Flappy Musketeer游戏中,我们开始了一段充满激情的旅程,利用Jetpack Compose的强大功能创建Android移动游戏。我们深入探讨了主题、导航、游戏菜单和游戏屏幕逻辑,剖析每个方面,为您提供打造自己沉浸式游戏体验所需的工具。
当您踏上游戏开发之旅时,请记住Jetpack Compose为创建视觉上令人惊叹和引人入胜的Android游戏开启了无限可能。因此,前进吧,释放您的创造力,构建出一些酷炫的东西!
https://github.com/nirbhayph/flappymusketeer/