In this part, the first playable demo in the style of Mario appears. To do this, you need to understand the scrolling and debugging methods.
The $ 2005 register controls the background scrolling. The first entry there sets the position of the horizontal scroll, and the second - the vertical. If you do not know which scrolling has been set, you can reset to horizontal reading from the register $ 2002.
99% of games use only 2 tables of names - in fact, this is the background of the screen. Two more available tables mirror the first two. The emulator receives information about the table settings from the header of the iNES-image of the cartridge. Bytes 6 and 7 describe the mapper - coprocessor in the cartridge. The low bit of byte 6 describes the scroll direction. 0 - scroll vertically, name tables are mirrored horizontally. 1 is the opposite. As a result, we get an accessible 1x2 screen area (or 2x1, depending on the selected scrolling) and a window that slides through this area and is rendered on the TV.
Gauntlet game uses four-sided scrolling. This requires 2k of additional RAM on the cartridge. Games with the MMC3 mapper can switch scrolling modes in the middle of the game. But in most cases, the scroll mode is the same for the whole game, and only 2 tables of names are used.
In the first example, we will adjust the horizontal scrolling. This is set in the reset.s file. The arrows on the joystick will move the background. Sprites implemented the background position display: H for horizontal shift and V for vertical. I strongly recommend that you run this image in FCEUX and see the debugger name table during the movement.
After crossing $ FF horizontally, a change in the name table is invoked through a call to the PPU_CTRL register - it is located at $ 2000. For the user, it is invisible.
Such tools were used to prepare the demo: the letters were drawn in Photoshop, then they were indexed into a four-color image and copied in YY-CHR. Then they need to be saved to a chr-file and open it in the NES Screen Tool, compose the background, and then export with the RLE-compression as .h file. Now you can download it when you start the console. The movement of the character is realized through the background shift, and the position of the sprite does not change.
void move_logic(void) { if ((joypad1 & RIGHT) != 0){ state = Going_Right; ++Horiz_scroll; if (Horiz_scroll == 0) ++Nametable; } if ((joypad1 & LEFT) != 0){ state = Going_Left; --Horiz_scroll; if (Horiz_scroll == 0xff) ++Nametable; } Nametable = Nametable & 1; // 0<->1 if ((joypad1 & DOWN) != 0){ state = Going_Down; ++Vert_scroll; if (Vert_scroll == 0xf0) Vert_scroll = 0; } if ((joypad1 & UP) != 0){ state = Going_Up; --Vert_scroll; if (Vert_scroll == 0xff) Vert_scroll = 0xef; } }
And when the frame is updated, the sprites are updated and the scrolling position is set:
void every_frame(void) { OAM_ADDRESS = 0; OAM_DMA = 2; // $200-$2FF RAM PPU_CTRL = (0x90 + Nametable); // NMI PPU_MASK = 0x1e; SCROLL = Horiz_scroll; SCROLL = Vert_scroll; // Get_Input(); }
In the second example, the scrolling is vertical, and the table of names is "looped" across the left and right edges of the screen. This is set in the same reset.s. For vertical scrolling, tables of names 0 and 2 are used.
The maximum vertical scroll position is $ EF, because the screen is 240 pixels high. This is handled similarly to the previous example. Another difference is the switching of name tables from zero to second and back:
PPU_CTRL = (0x90 + (Nametable << 1));
And now we will make a demo with horizontal scrolling and jumping on platforms. Collision maps for 2 pages of the background will be stored in memory and will take there $ 200 bytes.
First we make gravity. Each frame sprites should fall on (++ Y), if they are not standing on the platform. We assume that the bottom of the meta-list is aligned with the background. So you can check if the lower corners of the metasprite have failed in the platform:
// // ? NametableB = Nametable; Scroll_Adjusted_X = (X1 + Horiz_scroll + 3); // high_byte = Scroll_Adjusted_X >> 8; if (high_byte != 0){ // 255 , ++NametableB; NametableB &= 1; // 0<->1 } // ? collision_Index = (((char)Scroll_Adjusted_X>>4) + ((Y1+16) & 0xf0)); collision = 0; Collision_Down(); // , ++collision // ... , (X1 + Horiz_scroll + 12); void Collision_Down(void){ if (NametableB == 0){ // temp = C_MAP[collision_Index]; collision += PLATFORM[temp]; } else { // temp = C_MAP2[collision_Index]; collision += PLATFORM[temp]; } } // platform // , // if(collision == 0){ Y_speed += 2; } else { Y_speed = 0; Y1 &= 0xf0; // }
Next you need to work on the smoothness of movements and jumps. It will take a lot of variables for the coordinates of the sprite's position and background, speed, acceleration, and a pair of constants for maximum speeds. But I scored on it. As a result, the scroll speed is stored in the higher nibble X_speed.Horiz_scroll += (X_speed >> 4);
Usually the scrolling of the background begins when the character approaches the edge of the screen. And when it is in the central part, it moves by itself with a static background. Here this technique is not used, again for simplification. Maybe someday I'll refactor.
Sprite Zero Hit is one of the ways to track an event in the middle of a frame, for example, changing the position of horizontal scrolling. This will allow us to make a static top of the screen, such as a points counter, and scroll the bottom of the screen.
There are several ways to implement:
We only need the first method - it is the easiest and the most bug-free.
A zero sprite is stored in OAM at $ 0- $ 3. If it contains an opaque pixel and this pixel is drawn over the opaque background pixel, then in the $ 2002 register bit 0x40 will be set. If the sprite is drawn over a transparent background, the game goes into an infinite loop. We can use this to adjust the scrolling. The procedure is written in Assembler.
First, we will do everything we need in V-blank. Then set horizontal scrolling to zero and turn on the desired name table. Then we call SpriteZero (), and it will go away waiting for the event - drawing the line where the necessary pixels will be superimposed. Then we can switch the scrolling and the table of names - this will happen in the middle of the screen drawing.
// NMI - Sprite_Zero(); // SCROLL = Horiz_scroll; SCROLL = 0; // PPU_CTRL = (0x94 + Nametable);
In our example, the zero sprite contains the zero character, just for clarity. And also made it disappear when you click Start.
if ((joypad1 & START) > 0){ SPRITE_ZERO[1] = 0xff; // SPRITE_ZERO[2] = 0x20; // }
In the first version of the tutorial, the lesson ended here, but then decided to expand the topic. So now let's add a demo with a background width of 4 screens and dynamic background generation on metatayles, without RLE compression.
When moving a character by 16 pixels, the demo will finish drawing 2 columns of tiles outside the screen border, into the desired name table. It can fit in V-blank. To speed up the recording procedure, PPUupdate had to write in Assembler and deploy the loops. The background attribute table also changes as you work.
It turned out difficult and cumbersome, and it took a lot of time to debug. So there will be a good example to show debugging techniques.
First, the scroll implementation is slow and does not invest in time. To understand this, I had to insert the command
PPU_MASK = 0x1F;
in main () before waiting for V-blank. From now on, the screen lines will be rendered in black and white. This hack is not compatible with all emulators, for example, in FCEUX you need to enable the 'old PPU' option. It turned out like this:
Half of the available CPU time has already been spent, and this is without music and opponents. For the profiling of the functions made an entry in the variable before and after the execution of the function, and included in the debugger tracking records at the address of this variable. And FCEUX can count processor cycles between shutdowns. It turned out something like this:
TEST = *((unsigned char*)0xFF) // ++TEST; Should_We_Buffer(); // 4422 ++TEST;
It turned out that the work with the buffer slows down. It can be divided into two shorter functions, and perform them through the frame. Now the CPU load looks better:
Next I remove the scroll left. Now it’s good to realize that you can run to the left and rest against the edge of the screen. This did not work out right away , and debugging by analytic tupling into the code ((c) DIHALT ) did not help. I had to generate an address map. To do this, call the linker with the option:ld65 ... -Ln “labels.txt”
And compiler with translator with option -g.
These files show that the suspicious move_logic () function is located at $ C5B2, so I’ll put a breakpoint there. In principle, you can place labels directly in the sish code and disable optimization, but I made a call to an empty function in the right place (movement of the character to the left) and tracked its exact location on the map of the tags. But intercepting a variable record is more compact and convenient.
Debugging still had to be done on assembler listing, but the wrong comparison 'if (X_speed <0)' was found pretty quickly. At this point, X_speed was reset to zero, even if you press Left. Changed the comparison to <=, and everything became good.
In FCEUX, in order to process a joystick with a debugger enabled, you must apply the 'auto-hold' option to the keyboard button and first turn on the hold, press Left, and then set breakpoint in the debugger.
The user of the Rainwarrior from Nesdev did, and I slightly corrected the Python script that converts the ca65 labels to a file for the FCEUX debugger. At the entrance, he takes the label.txt. An example of use is in the makefile and bat file in the source code for the lesson.
Now scrolling on 4 screens works, but harder than I imagined. Option B is similar, but all debugging is cut in it. I recommend to look at the table of names with a debugger and find out how scrolling works.
Dropbox
Github
In some cases, the linker may refuse to enter all the labels in the map, then you need to add this line to each assembly file:.debuginfo
Otherwise, you have to add -g every time you call cc65 and ca65.
Source: https://habr.com/ru/post/349376/
All Articles