Blog Infos
Author
Published
Topics
Published
A Step-by-Step Guide to Creating an Immersive Mobile Game

Article Cover [Download Game Here]

 

Introduction

Flappy Musketeer isn’t just another mobile game; it’s a fusion of addictive “tap-to-fly” game play and captivating visuals that draw players into Elon Musk’s remarkable ventures, including SpaceX and Twitter (X). What’s more, players have the freedom to personalize their gaming experience by choosing from an array of themes and color schemes.

In this article, we embark on a journey to construct Flappy Musketeer from scratch using Jetpack Compose. We’ll dissect the code, logic, and design decisions that bring this mobile game to life, guiding you through the process of creating an immersive Android gaming experience.

Source Code Overview Diagram

Game Source Code Overview Diagram

 

Setting the Stage with Jetpack Compose Theming

Photo by Pawel Czerwinski on Unsplash

 

In Flappy Musketeer, creating the right atmosphere and visual aesthetics is essential to the player’s experience. Let’s take a closer look at how it’s done.

1. The AppTheme Composable

The heart of our theming system lies in the AppTheme composable. This composable takes care of applying the chosen color scheme to the entire game’s UI. Here’s what it looks like —

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

The AppTheme composable is responsible for configuring the color scheme and applying it to the status bar and navigation bar of the Android device. This ensures a consistent visual experience throughout the game.

2. Custom Color Schemes

Flappy Musketeer offers a wide range of themes and color schemes for players to choose from. Here’s a glimpse of some of the available options —

Game Theme Colors

  • Space.X.Mars — Inspired by the rust-colored terrain of Mars.
  • Twitter.Doge — A playful theme featuring the Dogecoin mascot.
  • Twitter.White — A clean and minimalist design with a white color scheme.
  • Space.X.Moon — A dark theme inspired by the moon’s serene beauty.

These themes are defined as ColorScheme objects in the code, allowing for easy customization and application to different parts of the game’s UI.

val spaceX = darkColorScheme(
    primary = spacePurple,
    secondary = Color.Black,
    tertiary = Color.Black
)

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

By providing players with a variety of themes, Flappy Musketeer offers a personalized gaming experience.

3. Theme Backgrounds

In addition to color schemes, Flappy Musketeer also provides various backgrounds that match the chosen color scheme. These backgrounds add depth and immersion to the game environment. We use the GameBackground enum class to maintain the collection of backgrounds.

Photo by malith d karunarathne on Unsplash

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

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

These background images are loaded dynamically based on the selected theme, ensuring that the game’s visuals align with the player’s preferences.

4. Switching Themes in the Game

To make the theme selection process seamless, Flappy Musketeer provides a getGameTheme function that takes a theme name as input and returns the corresponding ColorScheme. Here’s how it works —

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

This function allows the game to switch between themes based on game option selection from the menu.

Navigating Through Flappy Musketeer

Photo by Heidi Fin on Unsplash

Now that we’ve covered the basics of theming with Jetpack Compose, let’s shift our focus to the game’s navigation. Flappy Musketeer utilizes the Navigation Component to seamlessly transition between different screens and game states.

1. The App Composable

At the core of our navigation system is the App composable. This composable sets up the game’s navigation using Jetpack Compose’s Navigation Component. Here’s what it looks like —

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

This code sets up the navigation graph, defining the flow of the game. Let’s break it down —

  • The App composable initializes the NavController that manages navigation within the game.
  • We start with the MENU screen as the initial destination.
  • Inside the NavHost, we define composable functions for each screen, such as the game menu and game screen.
  • We apply the appropriate theme using AppTheme for each screen to maintain visual consistency.
2. Navigation Destinations

Navigation Destinations

 

  • Menu Screen (AppRoutes.MENU.name) — This is where players land when they first open the game. The AppTheme composable sets the menu theme, creating a cohesive look for the game’s main menu. Players can select a game from here.
  • Game Screen (${AppRoutes.GAME.name}/{gameId}): The game screen dynamically adjusts its theme based on the selected game. Using the getGameTheme function, we fetch the appropriate color scheme and apply it with AppTheme. This ensures that each game’s theme matches its environment, be it Twitter.Doge or Space.X.Mars.
  • Game Over Screen (AppRoutes.GAME_OVER.name): This screen is displayed when the game is over, and it features an Twitter X Lottie animation. The navigation system seamlessly transitions to this screen when a player’s game ends.

With this navigation setup, players can easily navigate through the different sections of Flappy Musketeer, from selecting a game to playing it and experiencing the game over screen.

Crafting an Engaging Game Menu

Photo by SwapnIl Dwivedi on Unsplash

 

The game menu is often the first interaction point for players, setting the tone for the entire gaming experience. In Flappy Musketeer, the game menu is designed to be visually engaging and user-friendly. Let’s take a closer look at how it’s implemented —

1. The GameMenu Composable

The GameMenu composable is the entry point to Flappy Musketeer. It provides players with access to various game themes and essential links. Here’s the code snippet that defines the GameMenu

Game Menu

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

Quick Explaination —

  • The game’s logo is displayed as an IconButton that opens a link when clicked. This adds a touch of interactivity to the menu screen.
  • The app name is styled using buildAnnotatedString, allowing for customized font weights and styles.
  • The menu is implemented as a horizontal scrollable row, allowing players to swipe through different themes. Each theme is represented as a MenuItem composable, with its background image and name.
2. MenuItem Composable
@Composable
fun MenuItem(
    navController: NavController,
    backgroundUrl: String,
    name: String,
    originalName: String
) {
    // ... (Menu item content)
}

This composable is responsible for displaying a theme’s background image and name. When clicked, it navigates the player to the corresponding game screen based on the selected theme.

Mastering Game Screen Logic

The GameScreen composable is the central component responsible for managing the game play logic in the Flappy Musketeer game. This code file defines the behavior of the game during game play, including handling user input, updating the game state, and rendering game elements.

Let’s break down the GameScreen composable step by step and explain each part of the code with relevant code snippets —

1. State Initialization
// Game state and scores initialization
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)) }

In this section, we initialize various game state variables such as gameStatescorelastScorebestScorebirdOffset, and birdRect. These variables are used to track the game’s progress and the position of the bird.

2. Pipe (Obstacles) Dimensions Initialization

Pipe Dimensions

var pipeDimensions by remember {
    mutableStateOf(Triple(0.1f, 0.4f, 0.5f))
}

Here, we initialize pipeDimensions as a Triple to store the weights of the top, gap, and bottom pipes. These weights determine the relative sizes of the pipes.

3. Update Score Callback
// Callback function to update the score
val updateScoreCallback: (Long) -> Unit = {
    score += it
}

updateScoreCallback is a callback function used to update the game’s score whenever necessary.

4. Bird Falling Animation

Bird Falling

LaunchedEffect(key1 = birdOffset, gameState) {
    while (gameState == GameState.PLAYING) {
        delay(16)
        birdOffset += 4.dp
    }
}

In this section, we use a LaunchedEffect to continuously update the birdOffset and simulate the bird falling when the game is in the PLAYING state.

5. Update Bird and Pipe Rectangles
// Callback function to update the bird's rectangle
val updateBirdRect: (birdRect: Rect) -> Unit = {
    birdRect = it
    pipeDimensions = getPipeDimensions(it, screenHeight)
}

These callback functions are responsible for updating the bird’s and pipes’ rectangles. These rectangles are crucial for collision detection between the bird and pipes.

6. Collision Detection
Collision Detection
// Callback function to update the pipe's rectangle
val updatePipeRect: (pipeRect: Rect) -> Unit = {
    if (!it.intersect(birdRect).isEmpty) {
        // Handle collision with pipes
        // ...
    }
}

This callback function handles collision detection between the bird and pipes. When a collision is detected, the game state transitions to COMPLETED and we updated the best score and last scores.

In addition to this we also navigate to the game over screen (see navigation section to learn more)

7. Tap Gesture Handling
// Tap Gesture Handling
Box(
    modifier = Modifier
        .fillMaxSize()
        .pointerInput(Unit) {
            detectTapGestures(
                onTap = {
                    if (gameState == GameState.PLAYING) {
                        // Handle bird jump
                        coroutineScope.launch {
                            var offsetChange = 80.dp
                            while (offsetChange > 0.dp) {
                                birdOffset -= 2.dp
                                delay(2L)
                                offsetChange -= 2.dp
                            }
                        }
                    }
                }
            )
        }
)

Here, we set up tap gesture handling. When the player taps the screen during game play, the bird’s position is updated to simulate a jump.

8. Game Layout
  1. Box Composable —
Box(
    modifier = Modifier.fillMaxSize()
) {
    // ...
}
  • The game layout is encapsulated within a Box composable, allowing the placement of multiple components on top of each other.

2. Background —

Background()
  • The Background composable renders the game’s background, setting the appropriate background image based on the selected theme.

3. Pipes —

Pipes(
    updatePipeRect = updatePipeRect,
    updateScoreCallback = updateScoreCallback,
    gameState = gameState,
    pipeDimensions = pipeDimensions.copy()
)
  • The Pipes composable manages the generation and movement of pipes in the game. It handles collision detection with the bird and updates the score.

4. GameState Handling —

when (gameState) {
    // ...
}

This section uses a when expression to handle different game states —

  • GameState.PLAYING — Displays the bird, score, and pause button during game play. Tapping the pause button triggers the pause callback.
  • GameState.NOT_STARTED, GameState.COMPLETED — Shows the “Play” button to start or restart the game. Displays the last score and best score if available.
  • GameState.PAUSE — Displays the “Play” button to resume the game.

5. Bird —

Bird(birdOffset, updateBirdRect)
  • The Bird composable renders the bird character on the screen. The birdOffset determines the bird’s vertical position, simulating its movement.

6. Play Button —

Play(onPlayCallback)
  • The Play composable displays the “Play” button, allowing the player to start or resume the game when tapped. It triggers the onPlayCallback when pressed.

7. Ground —

Ground("Flappy Score", score, enablePause = true, onPauseCallback)
  • The Ground composable displays the game’s score and includes an optional pause button when enablePause is set to true. The onPauseCallback is triggered when the pause button is tapped.

 

In this section, we’ll dive deeper into the game layout of our Flappy Musketeer game. We’ll explore the following essential components that make up the game screen —

1. Bird

Sourced from Lottie Files (SVGenius)

 

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

The Bird composable in the Flappy Musketeer game is responsible for rendering the player’s character, often referred to as the “bird,” that the player controls to navigate through the pipes. Let’s break down the key aspects of this composable —

  • birdOffset — This parameter represents the vertical offset of the bird, indicating its position on the screen. It is updated based on player input and gravity to simulate the bird’s flapping and falling.
  • updateBirdRect— A callback function that updates the position and dimensions of the bird for collision detection.

Inside the composable —

  1. The bird variable is assigned an image resource based on the current theme’s primary color. The game provides different bird images depending on the selected theme, such as “spaceX”, “spaceXMars” “spaceXMoon” or “twitterDoge”
  2. Box composable is used to contain the bird image. It has a fixed size of 64×64 density-independent pixels (dp) and is positioned vertically based on the birdOffset.
  3. The onGloballyPositioned modifier is used to detect the bird’s position on the screen and invoke the updateBirdRect callback with its bounds.
  4. The content of the Box varies based on the theme —
  • For space-themed themes (spaceXspaceXMarsspaceXMoon), it displays an Image with the rocket image representing the bird.
  • For the “twitterDoge” theme, it displays an Image with a doge-themed rocket image.
  • For other themes, it displays an Icon with the bird image, with the icon’s color tinted to match the secondary color of the current theme.

This composable allows the bird character to be rendered differently based on the game’s theme, giving players a visual experience that matches the selected theme.

2. Pipes (Obstacles)

Twitter Pink

 

A) The Main Composable —

The Pipes composable is responsible for rendering and managing the pipes that the player must navigate through in the game. It also handles the logic for generating and moving the pipes based on game events.

Key Points —

  • updatePipeRect— A callback function that updates the position and dimensions of the pipes for collision detection.
  • updateScoreCallback— A callback function to update the player’s score.
  • gameState— Represents the current state of the game (e.g., playing, completed).
  • pipeDimension — A tuple representing the weights of the top, gap, and bottom pipes.

The Pipes composable manages the creation and movement of pipes based on the game state and time elapsed.

B) The Pipe Data Class —

data class Pipe(
    val width: Dp = 100.dp,
    val topPipeWeight: Float,
    val gapWeight: Float,
    val bottomPipeWeight: Float,
    var position: Dp,
)
  • Pipe is a data class representing a single pipe in the game. It contains properties such as width, weights for top, gap, and bottom pipes, and its position on the screen.

C) Pipe Generation Logic —

if (System.currentTimeMillis() - PipeTime.lastPipeAddedTime >= 2000L) {
    // Generate a new pipe and add it to the list
    // ...
    // adding logic
    val addToList = if (pipes.isNotEmpty()) {
        abs(pipes.last().position.minus(newPipe.position).value) > 500f
    } else {
        true
    }

    if (addToList) {
        pipes = pipes + newPipe
        updateScoreCallback(10)
        PipeTime.lastPipeAddedTime = System.currentTimeMillis()
    }
}
  • The game checks if it’s time to generate a new pipe based on the time elapsed since the last pipe was added and if the distance from the last shown pipe is big enough. If the conditions are met, a new pipe is created and added to the list of pipes.
  • The updateScoreCallback function is called to update the player’s score when a new pipe is generated.

D) Pipe Movement —

// move pipes from right to left
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) // remove from list when off the screen
        }
    }
}
  • The pipes are moved from right to left on the screen using a LaunchedEffect. The effect runs as long as the game is in the “playing” state.
  • The delay(16L) ensures that the pipes are moved at a consistent rate, providing smooth animation.
  • The pipes list is updated by mapping each pipe’s position, subtracting the pipe’s speed (pipeSpeed). Pipes that go off the screen (position < -pipeWidth) are removed from the list.

E) The GapPipe Composable —

@Composable
fun GapPipe(pipe: Pipe, updatePipeRect: (Rect) -> Unit) {
    // ...
}
  • The GapPipe composable is responsible for rendering an individual pipe and its gap.
.onGloballyPositioned {
    val pipeRect = it.boundsInRoot()
    updatePipeRect(pipeRect)
}
  • It takes a Pipe object and a callback function updatePipeRect for collision detection.

F) Pipe Dimensions Calculation —

fun getPipeDimensions(
    birdPosition: Rect,
    screenHeight: Dp
): Triple<Float, Float, Float> {
    // ...
}
  • The getPipeDimensions function calculates the weights (relative heights) of the top, gap, and bottom pipes based on the position of the bird and the screen height.
  • It ensures that the generated pipe weights are within certain limits to create challenging but fair game play.
3. Ground and Score Display

The Ground composable in the Flappy Musketeer game is responsible for rendering a ground area that displays game-related information. Here’s an explanation of this composable —

Ground and Score Display

@Composable
fun Ground(
    label: String,
    score: Long,
    enablePause: Boolean = false,
    onPauseCallback: () -> Unit = {}
) {
    // ...
}
Key Points —
  • label— A string that represents the label or title for the ground area.
  • score— A long integer representing the player’s score.
  • enablePause— A boolean indicating whether to enable a pause button. It’s set to false by default.
  • onPauseCallback— A callback function that is invoked when the pause button is clicked. It’s an empty function by default.

The Ground composable creates a visual ground area that displays the label, score, and an optional pause button. It’s used to provide information and interactions related to the game’s progress.

4. Play Button

Play Button

 

@Composable
fun Play(onPlayCallback: () -> Unit) {
    // ...
}

Key Points —

  • onPlayCallback— This parameter is a callback function that will be invoked when the play button is clicked. It typically triggers the start or restart of the game.
  • The Play composable creates a visually appealing play button that matches the game’s theme.
Conclusion

Photo by Kelly Sikkema on Unsplash

 

In Flappy Musketeer, we’ve embarked on an exciting journey of creating an Android mobile game using the power of Jetpack Compose.

We’ve delved into themes, navigation, game menus, and game screen logic, dissecting each aspect to give you the tools you need to craft your own immersive gaming experience.

As you embark on your game development adventures, remember that Jetpack Compose opens up a world of possibilities for creating visually stunning and engaging Android games. So, go forth, unleash your creativity, and build something cool!

Closing Remarks and Credits

If you liked what you read, please feel free to leave your valuable feedback or appreciation. I am always looking to learn, collaborate and grow with fellow developers.

If you have any questions feel free to message me!

Credits to all the people, resources and tools used for building this game!

Here is the Linktree link for all resources linked to the game including the GitHub repository containing the entire source code of the app.

Follow me on Medium for more articles — Medium Profile

Connect with me on LinkedInand Twitter for collaboration.

Happy Composing!

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu