Skip to content

2. Developer Guide

Ken St. Cyr edited this page Feb 3, 2023 · 3 revisions

Anatomy of a gameBadge3 Game

TBA...

Graphics

How Graphics are Drawn

Before jumping into the code for rendering graphics onto the display of gameBadge3, it's important to first run through a few key concepts. Please don't skip over this information - you will eventually get lost if you don't know it.

The Nametable

The first concept to understand is the nametable. If you're familiar with the nametable in the NES (Nintendo), you'll find the nametable implementation in gameBadge3 to be similar, but with a few differences. In short, the nametable acts as an in-memory display buffer for the screen.

The physical LCD screen used by gameBadge3 is 240 x 240 pixels, with a 1.3" display. Because the display size is small, gameBadge3 doubles up the size of each pixel it draws (also referred to as 'chunky' or 'packed' pixels). Therefore, the gameBadge3 screen can display 120 x 120 pixels on the LCD.

gameBadge3 uses tiles that are 8 pixels wide and 8 pixels high. So, with a 120 x 120 pixel display, 15 tiles can be displayed horizontally and 15 tiles can be displayed vertically (120 / 8 = 15). For example, here's the title screen for Grevious, with an 8-pixel grid overlayed on top. Each square in the grid represents one tile.

The nametable is allocated in memory as an array of 32 x 32 words (1 word = 2 bytes). This means that you can buffer 32 tiles (256 pixels) horizontally and vertically in the nametable. Because the screen displays 15 x 15 tiles and the nametable stores 32 x 32 tiles, there's enough room in the nametable for four full screens of tiles (2 full screens horizontally, and 2 full screens vertically), with 2 rows and columns left over in the buffer.

Note: You might be wondering why the nametable is 32 x 32 tiles. Wouldn't it make more sense for it to be 30 x 30 tiles, so it's aligned to 2 x 2 full screens? That may make sense from a practical standpoint, but by aligning the array boundary to a multiple of 8, bitwise operators can be used in the drawing algorithms, which are faster and more CPU efficient than the arithmetic operators in C++.

To understand why four screens of graphics are buffered in the nametable, you next need to know about the viewport.

The Viewport (Window)

The viewport represents the portion of the nametable that's displayed on the LCD screen. Recall that the nametable is 32 x 32 tiles. However, the LCD screen can only display 15 x 15 tiles. So, the viewport is what determines which portion of the nametable is drawn on the LCD. In this manner, you can think of the viewport as a window into nametable. In reality, it's a bit more complicated than that, but you'll read more about that in the section on status bars.

When you set the viewport, you specify the pixel coordinate (not the tile coordinate) where the upper-left corner of the viewport window overlaps with the nametable. For example, if you were to set the viewport to (0, 0), gameBadge3 will fill the screen with a 120-pixel square that consists of the tiles in nametable at x = 0, y = 0:

When you move the viewport to a different location, the tiles in the nametable at the new location are then displayed. In the following example, the viewport has now been moved to (50, 0). Note that the viewport now starts in the middle of a tile, so only partial tiles are displayed along the left and right edges.

By incrementing the viewport location one pixel at a time, you can achieve smooth scrolling for the backgrounds in your games. While the illustration above shows how to achieve horizontal scrolling, you're not limited to only the x-axis. You can just as easily achieve vertical and diagonal scrolling by adjusting the Y coordinate of the viewport.

For More Information...

If you want to dig into this further, take the time to watch Ben's overview of how the graphics rendering in gameBadge3 works:

Creating Your Graphics

When creating graphics for your game, we recommend using YY-CHR. gameBadge3 is modeled after NES-style graphics, so compatibility with YY-CHR's graphics format is built in.

The graphics for gameBadge3 are stored in 3 separate files:

  • RGB file (.PAL) - Contains the entire set of possible colors that each color palette can contain. You can define 64 colors in total, and those colors can be mixed and matched into different palettes.
  • Palette file (.DAT) - Defines which colors from the RGB file are in each palette set. You can define 8 palette sets per file, each with 4 colors.
  • Pattern file (.NES) - Contains the pixel data for each tile and sprite used in your game. Pixels are 2-bits each, and these 2 bits are mapped to one of the four colors in the palette set you're using.

The following image shows which pieces of the YY-CHR editor are exported into each of these files:

YY-CHR does provide primitive pixel art capabilities. You can draw your tiles and sprites directly in YY-CHR, or use a different image editing program and import them.

Loading Graphics

When you load the graphics files in your game code, each file must be loaded in a specific order, as the palette data in the .DAT file is indexed into each color defined in the .PAL file. Here's an example of how you would load each graphics file in your setup() function.

loadRGB("NEStari.pal");					// Load RGB file first
loadPalette("palette_0.dat");			// Then load the palettes
loadPattern("moonforce.nes", 0, 512);	// Load the patterns last

After the pattern file is initially loaded, it can later be swapped out and re-loaded with a different pattern mid-game. This will enable you to use different tiles and sprites throughout your game. To load a different pattern file, just call the loadPattern() function again.

Working with Tiles

To draw a tile to the screen, call the drawTile() function. When calling this function, you need to specify 3 things - (1) which tile you want to draw, (2) where in the nametable you want it drawn at, and (3) which color palette to use.

Reminder: As discussed earlier, tiles are drawn to the nametable directly. Once they're drawn to the nametable, the viewport will determine which tiles are displayed on the screen.

The syntax for the drawTile() function is as follows:

drawTile(int tileX, int tileY, uint16_t patternX, uint16_t patternY, char whatPalette);

Here's what each parameter means:

  • tileX - the X position of where to draw the tile in the nametable (0 - 31)
  • tileY - the Y position of where to draw the tile in the nametable (0 - 31)
  • patternX - the X location of the tile you're drawing from the pattern file (0 - 15)
  • patternY - the Y location of the tile you're drawing from the pattern file (0 - 63)
  • whatPalette - the zero-based index of the palette to use from the palette file (0 - 7)

For example, if you want to draw the tile at position (10, 1) in the pattern file onto the nametable at position (1, 1) with the first color palette, you would use the following code:

drawTile(1, 1, 10, 1, 0); 

If you want to clear the tiles in a portion of the nametable (or fill the nametable with a particular tile), you can use the fillTiles() function. Here's the syntax:

fillTiles(int startX, int StartY, int endX, int endY, uint16_t whatTile, char whatPalette);

The startX, startY, endX, and endY parameters boundaries of the location in the nametable that you'll either fill or clear. To clear the entire nametable with the background color, you would use:

fillTiles(0, 0, 32, 32, 0, 0);

To fill the nametable (instead of clearing it), set the whatTile parameter to the index of the tile in the pattern file that you want to use fill the nametable with. The whatPalette parameter is going to determine which palette set is used when drawing the tile (0 - 7).

Scrolling the Screen

Once your tiles are drawn to the nametable, the position of the viewport window will determine what's drawn to the screen, as discussed earlier. To set the viewport position, use the setWindow() function. This function uses the following syntax:

setWindow(uint8_t x, uint8_t y);

The x and y parameters refer to the pixel location in the nametable that the viewport should be set at. The gameBadge3 game library sets the viewport position at (0, 0) by default. You can just called setWindow() when you're ready to change it (for example, if you want to scroll the screen). The following sample code sets the viewport to (1, 0), which effectively scrolls the window horizontally to the right by one pixel:

setWindow(1, 0);

Freezing a Row of Tiles (Status Bars)

The gameBadge3 game library has the ability to freeze a row of tiles. When you freeze these tiles, they will be locked to a portion of the nametable and will not be scrolled or changed when you move the viewport around. You can use this technique to create persistent status bars within your game.

To set up row freezing, there are three things you need to do - (1) update the jump list, (2) freeze the row, and (3) define the sprite boundary.

Update the Jump List

To determine which tiles are displayed in the frozen row, there's an internal array called the 'jump list', which maps the frozen row to a row in the nametable that you define. The contents of the row that you're pointing to will be displayed in the frozen row. As you can see in the following example, the tiles displayed at the top of the screen are actually stored in row 17 of the nametable:

To add the row to the jump list, you will call the setWinYJump function in the setup1() function of your game.

void setWinYjump(int jumpFrom, int nextRow);

The jumpFrom parameter should be the zero-based index of the row that's frozen on the screen. The nextRow parameter will be the zero-based row number in the nametable, whose contents will be displayed. For example, to implement the example above, you would use the following:

void setup1() {
    ...
    
    setWinYjump(0, 16);
    
    ...
}

Freeze the Row

If you were to run your code now, you would notice that the proper tiles are showing in the frozen row, but the tiles in that row still scroll when the viewport changes. Therefore, you will need to tell the game library to freeze that row and not scroll it with the rest of the viewport. For this, you use the setWindowSlice() function.

void setWindowSlice(int whichRow, uint8_t x);

Here's what each parameter means:

  • whichRow - the zero-based index of the row in the nametable that you want to freeze. Note, this is not the screen row you're freezing, but rather the nametable row. The valid range is 0 - 63.
  • x - the column pixel that the row is frozen on. In most cases, this will be set to 0. The valid range is 0 - 255.

You'll need to call the setWindowSlice() function every time the viewport changes, after your call to setWindow() to move your viewport. So, if you're changing the viewport each frame, you'll need to call setWindowSlice each frame, as well. For example, the following snippet of code is called every game frame to scroll the screen horizontally. You'll notice that immediately after moving the viewport, setWindowSlice is called to freeze the row.

void gameFrame() { 
    ...

    setWindow(screenX, 0);
    setWindowSlice(16, 0);
    
    ...
}

Define the Sprite Boundary

At this point, the static row should be frozen while the rest of your viewport can move around the nametable freely. However, you'll notice that sprites can still draw over your frozen row of tiles. To prevent this from happening, you need to change the sprite boundary using the setSpriteWindow() function. Any sprites drawn outside of this boundary will not be displayed.

void setSpriteWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1);

The input parameters to this function are the starting and ending pixel-based coordinates of where sprites are allowed to draw on the screen.

  • x0 - Upper left X-coordinate of the sprite boundary. Valid values are 0 - 119.
  • y0 - Upper-left Y-coordinate of the sprite boundary. Valid values are 0 - 119.
  • x1 - Lower-right X-coordinate of the sprite boundary. Valid values are 0 - 119.
  • y1 - Lower-right Y-coordinate of the sprite boundary. Valid values are 0 - 119.

You will need to call the setSpriteWindow function during each game frame. For example, if you are using single frozen row at the top of the screen, you would use the following snippet to ensure that sprites can't draw over it:

void gameFrame() {
	...
    setWindow(screenX, 0);
    setWindowSlice(16, 0);
    setSpriteWindow(0, 16, 119, 119); 
    ...
}

Working with Sprites

Sprites are graphical elements that are different from tiles. Whereas tiles are intended to stay in place within the viewport (for example, background graphics), sprites are intended to move around the viewport and interact with tiles and other sprites.

Sprite Sizing Guidelines

In gameBadge3, sprites are composed of a grid of one or more tiles from the pattern file. For example, if you have a 16px x 16px sprite, it will take up 4 tiles worth of space (2 x 2):

Technically, sprites can be up to 16 tiles wide and 64 tiles high. A sprite of that size is impractical, however, as it would occupy the entire pattern file and pattern buffer with just one sprite. Realistically, your sprites are likely going to be 2 - 4 tiles in size, depending on your game.

Transparency

gameBadge3 does support transparent sprites. To make a transparent sprite, you must color your sprite so that all the pixels that you want to be transparent use the color 0x0001. Because an entire sprite is drawn with a single palette, this means that the transparency color (0x0001) will take up one of the four colors in your sprite's palette set. Therefore, the implication of using sprite transparency is that you can only use 3 colors for your sprite.

Note: 0x0001 is only a small fraction lighter than the color black. It's used as the transparency color in gameBadge3 because it's not likely that anyone will use it for their graphics. If you want to use the color black in your sprites, you would normally use 0x0000.

Drawing Sprites to the Screen

Unlike tiles, sprites are drawn to a separate memory buffer instead of the nametable. When a frame is drawn to the screen, the tiles and sprites are merged together. This approach enables you to independently control sprites without having to redraw the tiles underneath of them.

To draw a sprite, you should use the drawSprite() function. This function has the following syntax:

drawSprite(int xPos, int yPos, uint16_t tileX, uint16_t tileY, uint16_t xWide, uint16_t yHigh, uint8_t whichPalette, bool hFlip, bool vFlip);

Here's what each parameter means:

  • xPos - the X coordinate to draw the sprite on the screen, in pixels (0 - 119)
  • yPos - the Y coordinate to draw the sprite on the screen, in pixels (0 - 119)
  • tileX - the X index of the tile that the sprite is stored at in the pattern file (0 - 15)
  • tileY - the Y index of the tile that the sprite is stored at in the pattern file (0 - 63)
  • xWide - the width of the sprite, specified as a number of tiles
  • yHigh - the height of the sprite, specified as a number of tiles
  • whichPalette - zero-based index to the palette set used for coloring the sprite (0 - 7)
  • hFlip - if true, the sprite will be flipped horizontally
  • vFlip - if true, the sprite will be flipped vertically

Sprite Flipping

The gameBadge3 game library supports both horizontal and vertical sprite flipping. Flipping a sprite horizontally or vertically is as simple as passing a Boolean parameter into the function that you use to draw the sprite (see previous section).

Clearing Sprites

To clear the sprites from the sprite buffer, each pixel of the sprite must be replaced with the transparency color (0x0001). There's a function built into the game library to do this:

clearSprite();

Please note that the sprite buffer is cleared automatically as each frame is drawn, so there's no need to clear the buffer in an ongoing basis within your game. It is, however, recommended that you run clearSprite(); in your setup1() function, to make sure there's no leftover garbage data in the sprite buffer that may get accidentally drawn to the screen when your game first starts. In theory, that shouldn't be necessary - but it's a quick call to make and it's only running at the start of the game, so it's best to run it just in case.

Note: If you want to hide a sprite or just not display it on the screen, there's no need to remove it. Because all sprites are redrawn every game frame, you would just use your game logic to avoid calling thedrawSprite() function.

Displaying Text

Another built-in feature of gameBadge3 is the ability to display text on the screen. Text display works slightly differently than sprites. Rather than being drawn to a buffer, text characters are drawn directly onto the nametable, replacing the tiles that are underneath. This results in a couple of nuances that you need to be aware of when working with text, which are covered in this section.

Creating Your Fonts

The fonts for gameBadge3 text rendering are stored in the same pattern file as your tiles and sprites. There's a default set of fonts in the patterns\basefont.nes file, which you can copy into the pattern file for your own game (YY-CHR supports standard copy and paste commands between multiple pattern files).

When modifying these fonts or creating your own, be sure to use a different tile for each font character, as demonstrated in the included fonts. gameBadge3 does not have font kerning, so all text displayed to the screen uses fixed-width characters.

By default, the font set starts at tile index 32 in the pattern file (x = 0, y = 2). You do have the option of changing this index, so you can place your fonts anywhere you'd like in your pattern file, as long as the fonts are all kept together in one block of 96 consecutive tiles. Be sure to follow standard ASCII ordering for your custom font characters. Refer to an ASCII Table if you're not sure what the order is.

If you place your fonts at a different starting index in the pattern file, you'll need to call the following function during setup() to make sure the font rendering algorithms use the correct offset:

// Set the starting font index to 928 (x=0, y=58 in the pattern file)
setASCIIbase(928);

Drawing Text to the Screen

To draw text to the screen, you'll need to call the drawText() function:

void drawText(const char *text, uint8_t x, uint8_t y, bool doWrap = false);

Here's what each parameter means:

  • text - the null-terminated text string that you want to draw
  • x - the vertical tile position in the nametable that you want to draw the text at (0 - 31)
  • y - the horizontal tile position in the nametable that you want to draw the text at (0 - 31)
  • doWrap - set to true if you want to use word wrapping. The default is false.

The following example will draw "GAME OVER" in the center of the screen, without word wrapping:

drawText("GAME OVER", 3, 7);

Word Wrap

If you choose to word-wrap your text, you can adjust the margins used for wrapping by calling the setTextMargins() function during setup().

void setTextMargins(int left, int right);

The left and right parameters represent the left and right side margins that word wrapping will overflow on. Note that these values use tile column numbers, not pixels. The default values are set to left = 0 and right = 14, which will wrap text on the screen edges.

Limitations

When drawing text, there are a couple of limitations that you should be aware of.

Clearing Your Text

At this time, there is no built-in capability for clearing the text you display on the screen. Because the text is displayed directly to the nametable, the tiles that were previously underneath the text is overwritten. Therefore, if you want to clear the text you're displaying, you will need to keep track of which tiles are displayed in the space that you're rendering the text in your game code. Then, to clear the text, you would call the drawTile() function to restore each tile back over top the text.

Font Palettes

Currently, all fonts are displayed using the first palette set in your palette (.DAT) file. Please be aware of this as you're designing your fonts. It may make sense for you to designate your first palette as the "font palette" due to this limitation.

Input

TBA...

Sound

TBA...