Skip to content

3. TItle Screen (also init)

linkous8 edited this page Jan 8, 2019 · 9 revisions

Setup/Init

So while the title screen is the bulk of this chapter, we do have to do a bit of set up before we can jump into that. I wont be going into much detail here as I more or less copied what crashoverride does in his app_main methods and utilize his init methods. Again I am no C guru but it works so thats all that matters (feel free to create an issue/PR for this).

For starters, most GO programs are made in FreeRTOS which can be thought of as a very basic operating system. The organization of such projects can be found here: https://github.com/espressif/esp-idf-template

To build such a project (assuming you cloned it) you would navigate a terminal to esp-idf-template and run 'make' at which point it would generate a 'build' folder containing files from the build process including the resulting .bin file. The other folder in the template is 'main' which houses main.c which serves as the starting point for a FreeRTOS program.

Switching back to our project in homebrew-example, you can see there is another folder 'components'. This is a convention I followed from other developers and will contain the bulk of our game code (tetroidgo folder) along with the libraries from crashoverrides emulator implementations (odroid folder) which we utilize to interface with the hardware.

Now that you know a bit of the layout, it should make sense that we would want to start with the file homebrew-example/main/main.c and the function app_main() therein. The first thing that happens is a printf copied from one of crashoverrides app_mains. Other than looking cool, it serves to let us know (via serial monitor) that our execution is at least reaching app_main() which is helpful for debugging.

After that (again mostly copying what crashoverride does) we call the following:

nvs_flash_init(); - initalizes flash storage

odroid_system_init(); - initializes gpio for led

odroid_input_gamepad_init(); - initializes gpio for gamepad

odroid_input_battery_level_init(); - creates a FreeRTOS task to monitor battery level and turns on LED when low

ili9341_prepare(); --v

ili9341_init(); - initialize the display

bootloader_random_enable(); - initialize hardware TRNG (I actually researched this part on my own :D)

initRandom(); - initialize our homemade RNG solution

ili9341_clear(0); - clear the screen so its ready for drawing

game(); - init is finished and we hand control to the game loop

And thats it for app_main, the rest of our code will be in the components. First open components/tetroidgo/game.c. If you go to game() you'll see the first thing it does call the drawTitle() method located in components/tetroidgo/screen.c which will be the main focus of this page.

Drawing the Title

First lets talk about how we want to render the screen for this title. Rather than allocating a buffer in RAM for whole title screen at once, we instead draw with a 320x12 row of pixels to the screen 20 times to fill the screen. This happens so quickly that its imperceptible and uses less of our limited RAM pool.

To start, allocate the necessary RAM to the tileRowBuffer pointer. The type of this pointer (uint16_t) is two bytes, the same size as a single pixel which is useful for pointer arithmetic. When allocating our memory, we call heap_caps_malloc(size_t size, uint32_t caps) which works much like a traditional malloc. The first parameter is the number of bytes we are requesting, the second parameter specifies settings for that memory on the GO. Don't worry if the memory dynamics seem too dense right now, it should become apparent how it works as we start drawing. Lets looks at the arguments provided to heap_caps_malloc briefly:

SCREEN_WIDTH * TILE_WIDTH * 2 - this tells the system to allocate two bytes for each pixel in our 320x12 row

MALLOC_CAP_8BIT | MALLOC_CAP_DMA - this bitwise OR combines the two options which respectively indicate: we want access to individual bytes, not 32 bit "chunks", and also that we want that memory to be fast and not located somewhere slow like PSRAM

This is the method we will be using for all memory allocation in the game. We test the pointer with a simple if() to ensure the allocation was successful then print the start address of the allocation.

With that setup out of the way, we can begin drawing the title. To be optimal, we draw the most repetitive elements first; the rows of block tiles which accounts for 14 rows of the screen. The benefit is we only have to set up the row buffer once and then can draw it 14 times.

In the Graphics section, we covered the sprite sheet (SPRITE_SHEET) located in sprite_sheet.c. Now we will be using it. We start with a for loop iterating 12 times for each row of a tile and each iteration we copy the respective row from the tile in the sprite sheet onto an x position in the tile buffer. We do this using memcpy like such:

memcpy(

    tileRowBuffer + (y * SCREEN_WIDTH + tile_x * TILE_WIDTH) + 4, 

    (uint16_t *)&SPRITE_SHEET + getSpriteRow(BLOCK_SHINE_1, y),

    TILE_WIDTH * sizeof(uint16_t)

);

The first argument is the destination to copy to. In most cases it will be our buffer plus an offset (in number of pixels which are 2 bytes each) depending on where we want to put that row. In order to get the y offset, we multiply the current y loop control variable's (LCV) value by the screen width. To get the x offset, we multiply the tile width by the tile_x LCV. The plus 4 on the end accounts for the screen fringe (more on that later).

The second argument is the source. Again most cases this will be our sprite sheet plus an offset for which tile we want to draw. I wont be going into depth on the offset helper method, just know it takes an xy coordinate (top left origin) of the block in the sprite sheet as well as the current y pixel row and gives back the offset in number of pixels from the start of the sprite sheet to the desired pixel row's first pixel.

The third argument is the number of bytes to be copied; 12 times 2 bytes for a single full tile row of 12 pixels.

You may notice some memcpy calls will omit portions of the offset calculation in certain cases but the context of what is being drawn should make it apparent why that happens.

So from the top, we loop once for each row of the tile. Within that loop, first copy a 4 pixel wide fringe from the sprite sheet to the buffer. This is necessary because 320/12 leaves a remainder of 8 so we use the fringe to center the graphics.

Then execution enters another loop, this time for the x axis. This inner loop runs 26 times as that is the maximum number of full tiles that can fit on the width of the screen. Each iteration, it copies a 12 pixel long row from the sprite sheet to the buffer. Once the x loop completes, we draw another 4 pixel fringe before ending the y loop.

Once the y loop completes iterating, we have a buffer containing a centered row of BLOCK_SHINE_1 tiles one tile tall that spans the width of the screen. We simply send this to the screen 20 times (to keep the loop simple) to fill the whole screen with tiles:

for (uint8_t tile_y = 0; tile_y < 20; ++tile_y)
{

    ili9341_write_frame_rectangleLE(0, tile_y * TILE_WIDTH, SCREEN_WIDTH, TILE_WIDTH, tileRowBuffer);

}

ili9341_write_frame_rectangleLE parameters are as follows:

0 - left screen offset in number of pixels

tile_y * TILE_WIDTH - top screen offset in number of pixels

SCREEN_WIDTH - width of screen portion to be drawn to

TILE_WIDTH - height of screen portion to be drawn to (tiles are 12x12)

tileRowBuffer - location in memory of the pixels to be drawn

And with that, you should now have an understanding of how graphics go from a dev machine to the GO's screen. I'll briefly run through the rest of the drawTitle() routine then in the next section we will start on the actual game screen and the logic therein.

So recall now we have filled the screen with brick blocks. Bare in mind any further draws to the screen go on top of previous draws but do not effect the screen outside the region being drawn to.

Next we set the buffer to be the text "PRESS START" and draw that to the bottom of the screen

After that we write the word TETROID in blocks choosing a random block ID for each letter (except I block since it has multiple orientations). This is six tile rows in total starting with a black row to separate the words from the brick background, then 4 rows for the word tiles, followed by another black row.

And thats it. At the end of the method, we free the memory used for tileRowBuffer back to the RAM pool and return to the game() function.

Clone this wiki locally