In this part we consider the work with graphics: background and sprites of characters.
<<< previous next >>>
A source
PPU - a graphics processor - can either send a signal to the TV, or receive information from the processor, but not simultaneously. So the only time for sending is V-blank, the period of personnel extinguishing impulse.
PPU sends pixels to the video output 90% of the time, line by line from left to right and top to bottom. A pause is made at the bottom of the screen, and everything repeats. This happens 60 times per second. Pause after the frame is drawn and there is a V-blank. This is a very short time. It is really possible to attach an update of 2-4 columns of background tiles and an update of sprites into it. Updating the background is especially critical for scrolling games.
There are two ways to learn about the occurrence of V-blank. First, PPU sets the flag in the high bit of the $ 2002 register. Secondly, a nonmaskable interrupt (NMI) is called, and a transition occurs at a given address — the interrupt vector. It can be used to work with PPU, control the timing of music, move sprites. The interval between NMI calls is 1/60 of a second, and it can be used to count the time in the game.
Now we will write a program that will output “HELLO WORLD” by 1 letter every 30 frames. After displaying the entire phrase, clear the screen, and repeat everything in a circle.
Pay attention to the fragment
nmi: inc _NMI_flag inc _Frame_Count
in the reset.s file. This is a nonmaskable interrupt handler code that actually implements the frame counter in the global variable. Each frame sets the NMI_flag flag, and if 30 frames have passed (0.5 seconds), the next letter sprite is displayed. All logic is implemented in main ().
void Load_Text(void) { if (Text_Position < sizeof(TEXT)){ // PPU_ADDRESS = 0x21; // Y- PPU_ADDRESS = 0xca + Text_Position; //X- PPU_DATA = TEXT[Text_Position]; ++Text_Position; } else { // , Text_Position = 0; PPU_ADDRESS = 0x21; PPU_ADDRESS = 0xca; for ( index = 0; index < sizeof(TEXT); ++index ){ PPU_DATA = 0; // } } } void main (void) { All_Off(); // Load_Palette(); Reset_Scroll(); All_On(); // while (1){ // while (NMI_flag == 0); // NMI NMI_flag = 0; if (Frame_Count == 30){ // 30 Load_Text(); Reset_Scroll(); Frame_Count = 0; } } } // NMI ++NMIflag ++FrameCount V-blank
The source code is here and here .
Ideally, you should wait for V-blank before calling All_On (), otherwise one frame after it will be distorted and the screen will flicker. In this example, this effect is invisible, because All_On () is called only once when the console starts, the screen is black at this time, and the distortion is not noticeable.
Now you can color what happened.
A few words about the palette in the NES. In the PPU memory, 16 bytes are allocated for it at the addresses $ 3F00- $ 3F0F for 4 background palettes and 16 bytes at the addresses $ 3F10- $ 3F1F for 4 sprites palettes. The first color of the sprite is always drawn as transparent, and the first background color as the default background color. The bytes in the PPU memory corresponding to the first colors are mirrored for all 8 entries, and correspond to the default background color. So we can use 3 unique colors for the sprite and 3 colors plus a common background for tiles.
Thus, you can simultaneously draw 25 colors out of 50 possible.
You can darken these colors through the color emphasis registers, but this feature is not compatible with all clones of the console. In addition, some televisions tear off roofs of $ 0D and $ 1D colors. They are blacker than black, and this is an abnormal situation. Use colors $ xF.
For the background, the palette is defined for a block of 2x2 tiles, 16x16 pixels in size. For this breakdown it is convenient to use the Nes Screen Tool. Most games build the background from such quad metatails.
In each table of names (in fact, a blank for rendering the screen), 64 bytes are allocated for attributes. For example, for table 0, the addresses are PPU $ 2000- $ 23FF, and attributes are stored in $ 23C0- $ 23FF. Each attribute byte describes 4 metatails, that is, an area of ​​32x32 pixels. Accordingly, 2 bits select the palette number for the metathile. Matching a bit to a metatail is:
Here one letter corresponds to one tile, a small square is a 2x2 metatyle.
That is, in order to change the palette of the right lower quadrant for the first of 4 possible ones, it is necessary to change the two bits of the attribute to 01: 01.
I added to the past example of the palette, so that the letters are now multicolored. Palettes are moved to a separate file and imported via #include, and variables are defined in the zero page of memory via #pragma - just to demonstrate this feature. The zero page is faster, but 10-20 variables are used there for internal compiler needs. So you need to count somewhere on the 235 available bytes.
#pragma bss-name(push, "ZEROPAGE") // #include "PALETTE.c" #include "CODE.c"
const unsigned char PALETTE[]={ 0x11, 0x00, 0x00, 0x31, // 0x00, 0x00, 0x00, 0x15, // 0x00, 0x00, 0x00, 0x27, // 0x00, 0x00, 0x00, 0x1a, // }; // 011 - , // const unsigned char Attrib_Table[]={ 0x44, // 0100 0100, 0xbb, // 1011 1011, 0x44, // 0100 0100, 0xbb}; // 1011 1011 }; // - 2 1616 PPU_ADDRESS = 0x23; PPU_ADDRESS = 0xda; for( index = 0; index < sizeof(Attrib_Table); ++index ){ PPU_DATA = Attrib_Table[index]; } // PPU
The source code is here and here .
Here is a useful cheat sheet for the addresses of the attribute table and its binding to screen coordinates:
The screen is 256 pixels wide, each cell of the table corresponds to a 32x32 square.
Sprite is an 8x8 image that can move around the screen. There is a tricky way to use 8x16 sprites, but we will not do that. Almost all game characters are sprites. Although in some cases you need to draw them with background tiles due to restrictions on the number of sprites, this is used for the final bosses in some games.
8x8 points is very small, so you have to collect a character from several sprites: a small Mario from 2x2, and a large one from 2x4.
We must remember the limitations. Supports no more than 64 sprites, and no more than 8 on one line of the screen. If the limit is exceeded, only high priority sprites will be drawn. If you change the priority of the sprite between frames, it will flash. This method is often used in games.
PPU stores information on sprites in the OAM table. Its size is 256 bytes: 4 bytes for each of the 64 possible sprites. If two sprites with the same priority are superimposed, then the sprite with the lower number is drawn over. If on one line of the screen there are more than 8 sprites, then only 8 with smaller numbers will be drawn.
The OAM table stores these attributes:
Coordinates are calculated on the upper left corner. X can be from $ 00 to $ F8, Y from $ 00 to $ EE. You can partially hide the sprite down or to the right of the screen, but the left and upper borders are inviolable. In our example, the initialization code hides the sprites below, Y = $ F8.
Details on the OAM table.
How all this is stored in memory:
The truth is, here the sprite table is stored in RAM at $ 700, and in the example $ 200 is used.
Writing to the OAM table is implemented through memory registers: you need to write the address of the sprite at $ 2003, and then the data for it at $ 2004. This is rarely used because there is a more convenient and faster way to fill with DMA. It is activated through the register $ 4014: we write $ xx to the address $ 4014, 256 bytes from the range $ xx00- $ xxFF are poured into the OAM. This should be done during the V-Blank period.
In our example, we will create a meta sprite of 4 sprites and add animation.
We must remember that the sprites are drawn 1 point below the expected coordinate. The picture with Mario at the top of the post shows that he went 1 pixel to the floor.
#pragma bss-name(push, "ZEROPAGE") unsigned char NMI_flag; unsigned char Frame_Count; unsigned char index; unsigned char index4; unsigned char X1; unsigned char Y1; unsigned char move; unsigned char move_count; #pragma bss-name(push, "OAM") unsigned char SPRITES[256]; // OAM $200-$2FF, cfg const unsigned char PALETTE[]={ 0x19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x19, 0x37, 0x24, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; const unsigned char MetaSprite_Y[] = {0, 0, 8, 8}; // Y- const unsigned char MetaSprite_Tile[] = {0, 1, 0x10, 0x11}; // const unsigned char MetaSprite_Attrib[] = {0, 0, 0, 0}; // : const unsigned char MetaSprite_X[] = {0, 8, 0, 8}; // X- // 4 , - void every_frame(void) { OAM_ADDRESS = 0; OAM_DMA = 2; // OAM DMA PPU_CTRL = 0x90; // NMI PPU_MASK = 0x1e; SCROLL = 0; SCROLL = 0; // , } // void update_Sprites (void) { index4 = 0; for (index = 0; index < 4; ++index ){ SPRITES[index4] = MetaSprite_Y[index] + Y1; // ++index4; SPRITES[index4] = MetaSprite_Tile[index]; // ++index4; SPRITES[index4] = MetaSprite_Attrib[index]; // ++index4; SPRITES[index4] = MetaSprite_X[index] + X1; // ++index4; } } // , void main (void) { All_Off(); // X1 = 0x7f; // Y1 = 0x77; // Load_Palette(); Reset_Scroll(); All_On(); // while (1){ // while (NMI_flag == 0); // NMI NMI_flag = 0; every_frame(); // v-blank if (move == 0) ++X1; if (move == 1) ++Y1; if (move == 2) --X1; if (move == 3) --Y1; ++move_count; if (move_count == 20){ // 20 move_count = 0; ++move; } if (move == 4) move=0; update_Sprites(); } }
There is a slightly improved version where the character turns when moving in a square.
const unsigned char MetaSprite_Tile[] = { // 2, 3, 0x12, 0x13, // right 0, 1, 0x10, 0x11, // down 6, 7, 0x16, 0x17, // left 4, 5, 0x14, 0x15}; // up void update_Sprites (void) { move4 = move << 2; // 4 index4 = 0; for (index = 0; index < 4; ++index ){ SPRITES[index4] = MetaSprite_Y[index] + Y1; // ++index4; SPRITES[index4] = MetaSprite_Tile[index + move4]; // ++index4; SPRITES[index4] = MetaSprite_Attrib[index]; // ++index4; SPRITES[index4] = MetaSprite_X[index] + X1; // ++index4; } }
Source: https://habr.com/ru/post/348212/
All Articles