NES game development in 6502 assembly - Part 2
Welcome to Part 2 of the NES game development series. We are going to focus on the improvement of the code structure by utilizing ca65 features and as promised write an actual game.
Today we will be developing Pong.
Improving code structure
ca65 provides lots of useful directives that can improve the code readability. I will cover the most basic of them.
In the previous article we were writing all code in a single file. However, ca65 contains .include directive that allows us to split the code between multiple source files. Similarly palette and sprites data that were also part of the source code can also be included as a binary file using .incbin directive. Next is the .struct that allows us to define C-like structs used in combination with .tag which reserves space in the memory for this struct.
I have declared a few new types. point_t will be used to describe the x and y position of the ball. paddle_part and paddle_full are used to describe the paddles. If you remember each sprite is described with 4 bytes: y position, tile intex, attributes and x position. This is what paddle_part is for. Now a paddle in pong is quite long and won’t fit into a single 8x8 sprite. So, we need to combine several sprites into a big sprite called meta sprite. paddle_full describes 4 sprites that will be uploaded into RAM on each NMI.
Another feature of the ca65 is the C-like define. We will hide registers under meaningful names so that it is easier to navigate through the code.
Let’s introduce the concept of zeropage. Memory range of $0000 - $00FF is called a zeropage because the high byte of the address is $00. Games that require very few variables can store them in this range. 256 bytes are plenty for pong. We will store the position of the ball and paddles here.
.res directive will allocate the necessary amount of the bytes in the zeropage area. If you need to initialize this variable with some value then after the number of bytes put a comma followed by the value. Structs are created a little differently. .tag directive should be followed by the type of the struct.
NES allocates 8 KB for the game graphics that is located in the .chr file. So how do we create art for our game? YY-CHR is a small tool written in C++ that allows you to draw pixel art and then export it to a chr file.
On the left side there is a general view of the tilemap. By clicking on the left side, we can choose the index of the tilemap. On the right side is where we do an actual drawing. Select a color from the pallet on the bottom and start drawing pixels. For pong we will need a sprite for a ball and paddles.
In the source code you will notice that Reset interrupt now uses register defines from the header and also vblank labels got replaced by “:”. If the body of the label is short then instead of cluttering the code with lots of labels we can use “:” as an anonymous label to which we can later jump using “:-”.
LoadPaddle1 and LoadPaddle2 initialize the sprites of the paddles as well as the variables that will be used later to move the paddles. The members of the paddle1 and paddle2 reside in the memory consecutively so we can use the same loop to copy sprites first into RAM and then into variables.
We also need to initialize the ball position and direction. At the beginning the ball will be placed in the middle of the screen and moving in the DOWN + RIGHT direction. ball_dir is a single byte variable that holds the direction of the ball
Only two of the directions can be set at time. A new instruction ORA is used to set the bits 3 and 1 to indicate the initial direction of the ball. Notice how I use # before the defines. If I omit # then those defines will be treated as the address values as opposed to the literal values.
Lastly, let's set the initial position of the ball on the screen. Remember that s_ball_pos is a struct so to access its members we need to first indicate the type of the struct and then ::member. This under the hood will be converted to the offset from the s_ball_pos. So, x_pos is s_ball_pos + 0 and y_pos is s_ball_pos + 1.
To check whether there is a collision between the left paddle and the ball we need to comply with a few conditions. Firstly, the x position of the ball is less or equal than the x position of the paddle. Secondly, the y position of the ball lies between the y positions of the top and bottom sprites of the paddle. This will imply that the ball touches the paddle and that it needs to change the x direction to the opposite. In this case from LEFT to RIGHT.
For the second paddle the logic is the same except that the x position of the ball should be greater or equal than the x position of the paddle and the direction changes from RIGHT to LEFT.
Each NMI we check which directions are set on the ball and update the coordinates accordingly. Let’s first take a look at UP and DOWN. We first load the ball_dir into the A and then AND it with #UP which is %00001000. If the bit 3 is not set then this will mean that the ball is now moving in the downwards direction and we need to skip the MoveBallUp routine. Otherwise, decrease the y coordinate and store it in s_bal_pos. Also, we need to bounce the ball from the top wall if the y position of the ball is equal to the y position of the top wall. If this is the case, using EOR instruction we unset the 3rd bit meaning UP and set bit 2 using ORA so that the next NMI ball will start moving downwards.
LEFT and RIGHT operate based on the same logic except we should not bounce from the right or the left wall. Instead, we need to reset the state of the ball and give points to the player.
First let’s understand how to read the controller input in the NES. There are two controllers that can be accessed at the memory address $4016 and $4017 for player1 and player2 respectively. We first latch the controller by writing $01 and $00 to data port $4016. Then we can start reading from port $4016 one by one to access the state of each button.
JOYPAD1 here is $4016. Instead of writing several lines of the repetitive code let’s use a technique from NerdyNights. So basically, we read the value from the controller data port into the accumulator. Then by using LSR (Logical Shift Right) move bit 0 into carry. Using ROL move from the carry into the variable button1.
ca65 lets us define macros. You can think of them as functions. Macro starts with .macro followed by the name and optionally the list of the parameters. Macro used in the code will be replaced by its body and any occurrence of the parameters will be replaced by the supplied arguments. For example:
.macro do_something par1, par2
The call to macro do_something #$01, #$02 will be replaced with
Each paddle consists of 4 sprites that need to be moved simultaneously. Writing the code for each sprite separately will end up producing lots of repetitive code. I am using macros to overcome this problem. paddle_up_macro and paddle_down_macro move each sprite of the paddle up or down. I just need to pass which paddle and part to move.
We have updated the position of the ball and paddle. Now let’s update the sprites to reflect the changes on the screen. Information about the ball is located in the range of $0200 and $0204 which is very straightforward to update. However, updating 8 sprites of the both paddles by hand is a tedious process. If you remember the struct for a paddle struct has the exact same members in exactly the same order as required by the ppu. So we can write the sprites into memory by just looping through paddle variables.
You can improve the game by displaying the score for each player and also changing the background to display a classical white vertical line in the center of the screen.
Full source code: https://github.com/neonwalker/nes_pong