Creating a retro-style game with Jetpack Compose: Introduction

tkuenneth

Thomas Künneth

Posted on June 26, 2021

Creating a retro-style game with Jetpack Compose: Introduction

Writing games has always been a key discipline in programming. And quite a lot of fun. When we learn a new technology, we usually make faster and better progress if the teaching or training material is easily digestible. We can see this in user manuals of the so-called home computers from the late 1970s and early 1980s. Those little machines were programmed in BASIC. As they were intended for both parents and children, the content was written in a friendly and informal tone.

A home computer and its manual

Compared to the capabilities of todays' machines, home computers had very limited power. Its graphics and sound were quite basic, too. Typically, they could operate in two modes.

Modes

In graphics mode the screen was accessed by setting individual pixels. How many of them were available depended on the model. 320×200 pixels was considered high resolution. The number of colours a pixel could take was limited, too. Usually 2 to 16. The actual value was not set through rgb values - the appearance was fixed. So a pixel could be red or blue, but brightness and saturation were given through the hardware. Later home computers switched to the usual rgb mechanics, by the way.

How the pixels were set, again depended on the model and the capabilities of the BASIC dialect being used. Some had line, draw, or plot commands. Others didn't. As the video chip just displayed some area of the computers' memory, they needed to manipulate bytes in that area. Which bits and bytes related to which pixel on screen varied from model to model. As the computing power was quite limited, doing so in BASIC took quite some time. This is illustrated in the following clip:

Even though the speed could be improved quite a bit by switching to Assembler language, most home computers offered an even more interesting alternative approach.

In text mode (sometimes also referred to as character mode) the video chip accesses a part of the memory and interprets it as some sort of table, or grid. How many rows and columns are present depends on the model. The Commodore 64 used 40 columns and 25 rows. Each cell was represented by one byte, so each location could show one of 256 characters. How a byte value maps to an ASCII character depended on the model, too.

Part of the Commodore 64 symbol table

As you can see, the characters included quite a few graphics symbols. You could use them to draw your game scenes. The next clip shows how to do this.

Some home computers allowed the programmer to define custom symbols and map them to any of the 256 values. This way one could create quite nice-looking graphics. Usually such self-defined symbols consisted of of 8×8 pixels. 40 times 8 is 320 and 25 times 8 is 200. If this rings a bell, you are certainly right. The Commodore 64 uses this to provide its high resolution graphics. By populating the bitmap you basically define custom characters.

Back to present

At this point you are probably asking yourself why I am telling you this. After all, the article is supposed to cover Jetpack Compose. Well, I will reuse the concept of text mode to implement the prototype version of a maze puzzle game. You can find the source code on GitHub.

So, let's get started.

First, please recall that in text mode the contents on screen are represented by a grid, for example 40 by 25 symbols. Each cell in this grid represents the symbol or character on display at this location. Movement is achieved by changing the location of a particular element in this grid.

To understand how this works, please take a look at how I define a level of the game.

Definition of a game level

A level is defined through a multiline string. Each of its 14 lines has the same number of characters. The game does not operate on this string, though, because strings are immutable. But we need to change cells to achieve any movement.

const val COLUMNS = 40
const val ROWS = 14
Enter fullscreen mode Exit fullscreen mode

A function called createLevelData() (see source code below) returns a list of characters. This is quite similar to the succession of bytes in the memory of home computers, which are accessed by the video chip to display something in a particular cell on screen. Whenever a particular byte changes, the new symbol is immediately visible. The list being returned by createLevelData() represents a grid of 14 rows and 40 columns. Elements 0 to 39 belong to the first row, 40 to 79 to the second, and so on.

private fun createLevelData(): SnapshotStateList<Char> {
  val data = mutableStateListOf<Char>()
  var rows = 0
  level.split("\n").forEach {
    if (it.length != COLUMNS)
      throw RuntimeException("length of row $rows is not $COLUMNS")
    data.addAll(it.toList())
    rows += 1
  }
  if (rows != ROWS)
    throw RuntimeException("number of rows is not $ROWS")
  return data
}
Enter fullscreen mode Exit fullscreen mode

Please recall that with Jetpack Compose recomposition takes place whenever state a composable relies on changes. That's why createLevelData() returns a SnapshotStateList.

LazyVerticalGrid(
  modifier = Modifier.fillMaxSize(),
  cells = GridCells.Fixed(COLUMNS)
) {
  itemsIndexed(levelData, itemContent = { index, item ->
    var background = Color.Transparent
    val symbol = when (item) {
      '#' -> BRICK
      CHAR_GEM -> GEM
      'O' -> ROCK
      '.' -> {
        background = Color(0xffc2b280)
        SAND
      }
      '@' -> PLAYER
      else -> 32
    }
    Text(
      modifier = Modifier
        .background(background)
        .clickable {
          movePlayerTo(levelData, index, gemsCollected)
        },
      text = symbol.unicodeToString()
    )
  })
}
Enter fullscreen mode Exit fullscreen mode

The game screen (video memory 😊) is implemented using LazyVerticalGrid(). It receives the SnapshotStateList through levelData and iterates over it using itemsIndexed(). Its elements are converted to a Unicode symbol and then passed to Text(). When an element of the list changes (because of movement or game physics) the corresponding composable will be recomposed automatically.

The game running on the Android Emulator

Conclusion

You may be wondering why I store the level data as a list of Chars and convert its elements to Unicode strings. Using Unicode symbols and passing them to Text() is perfect for a prototype version (as you can see in the screenshot above). But for a real game we might want to use our own images instead. As the movements of the player, gems and rocks are done through changing the list, changing the drawing algorithm is piece of cake.

Regarding movement... I will cover this in the next episode. I hope you liked this article. Please share your thoughts in the comments.

💖 💪 🙅 🚩
tkuenneth
Thomas Künneth

Posted on June 26, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related