nes-starter-kit

所属分类:编程语言基础
开发工具:Assembly
文件大小:0KB
下载次数:0
上传日期:2022-04-05 19:56:12
上 传 者sh-1993
说明:  这是一个使用6502汇编语言开发任天堂娱乐系统游戏的初学者工具包,
(This is a starter kit for developing Nintendo Entertainment System games using 6502 Assembly language,)

文件列表:
LICENSE (1072, 2021-10-29)
cc65/ (0, 2021-10-29)
cc65/bin/ (0, 2021-10-29)
cc65/bin/ar65.exe (199152, 2021-10-29)
cc65/bin/ca65.exe (458362, 2021-10-29)
cc65/bin/cc65.exe (657267, 2021-10-29)
cc65/bin/chrcvt65.exe (184058, 2021-10-29)
cc65/bin/cl65.exe (230473, 2021-10-29)
cc65/bin/co65.exe (199810, 2021-10-29)
cc65/bin/da65.exe (287404, 2021-10-29)
cc65/bin/grc65.exe (194382, 2021-10-29)
cc65/bin/ld65.exe (323256, 2021-10-29)
cc65/bin/od65.exe (203009, 2021-10-29)
cc65/bin/sim65.exe (223527, 2021-10-29)
cc65/bin/sp65.exe (226028, 2021-10-29)
compile.bat (555, 2021-10-29)
img/ (0, 2021-10-29)
img/6502-assembler.png (7198, 2021-10-29)
img/6502-registers.png (7339, 2021-10-29)
img/6502.png (7956, 2021-10-29)
img/AsteroidSheetMonochrome.png (6033, 2021-10-29)
img/EmbedLogo.png (2905, 2021-10-29)
img/HelloWorld.chr (8192, 2021-10-29)
img/HelloWorld.png (5630, 2021-10-29)
img/HelloWorld.psd (49172, 2021-10-29)
img/NES-Architecture.png (4919, 2021-10-29)
img/NES-Hardware.png (68440, 2021-10-29)
img/PPU-Mirroring.png (6579, 2021-10-29)
img/PPU-backgrounds.png (7166, 2021-10-29)
img/PPU-palettes.png (7187, 2021-10-29)
img/PPU.png (7232, 2021-10-29)
src/ (0, 2021-10-29)
src/def/ (0, 2021-10-29)
src/def/header.asm (1323, 2021-10-29)
src/def/palette.asm (1783, 2021-10-29)
src/def/ppu.asm (15365, 2021-10-29)
src/lib/ (0, 2021-10-29)
src/lib/gamepad.asm (3638, 2021-10-29)
src/lib/utils.asm (1296, 2021-10-29)
... ...

# NES Starter Kit This is a starter kit for developing Nintendo Entertainment System games using 6502 Assembly language. I've included both code and tools for getting started in your adventure creating NES games for emulators or even cartridges. I will be stepping through this code in tutorials on youtube as well as in this readme. We will be using the [CA65 Assembler](https://www.cc65.org/index.php#Download) to assemble our program into the iNES Rom format. ## NES Architecture ![alt text](https://github.com/battlelinegames/nes-starter-kit/blob/master/img/NES-Architecture.png?raw=true "NES Architecture") The NES uses a variant of the 6502 processor as it's CPU. If you'd like to write code for the NES I'd highly recommend learning 6502 assembly language. The NES also has a PPU (Picture Processing Unit). The PPU was a kind of early stage GPU (graphics processing unit) which was capable of drawing images to the screen based on what was in the PPU's memory. The NES had no operating system, so early NES cartridges had 2 ROM chips in them. An 8K CHR ROM which contained all of the sprite and background image data using 2 bits per pixel. There was also a 32K PRG ROM wich contained all of the program data your game would run. Those ROM Chips were wired directly into the system's memory space. When we write a game using an emulator, we mimic this memory arrangement. ## 6502 CPU ### Actually a 2A03 CPU variant of the 6502 processor ![alt text](https://github.com/battlelinegames/nes-starter-kit/blob/master/img/6502.png?raw=true "6502 Processor") The 6502 was a popular CPU in the late 1970s and early 1980s for home systems and video games because it was cheap. It was used for several Atari systems including the 2600, by Commodore for the C64, and Apple for the Apple II. ### The 6502 is slow Programming with assembly languge can be quite time intensive. It takes many lines of code to do things that could be accomplished in a few lines of javascript. But by today's standards a 6502 is shockingly slow. It ran at 1.7Mhz (Million Cycles / Second). That may sound like a lot, but if you are trying to draw 60 frames per second, you have to divide 1700000 / 60 giving you about 28,000 or so cycles to draw a frame. On top of that there are no instructions that take less than 2 cycles, and most take many more. What it boils down to is you have a few thousand lines of assembly code to do everything. On top of that you only have 32,000 bytes to write your code. (later in the NES lifecycle chips were added into the carts to get around some of these limitations, but I won't be going into mappers here). ### 6502 Assembly has 56 documented commands Having to learn the 56 Assembly language commands isn't too bad. [Here's a link to my favorite 6502 reference](http://www.obelisk.me.uk/6502/reference.html) From what I can tell, there are 4 categories of commands * Move data commands (LDA, STA, LDX, TAX) * Status flag setting commands (CPA, BIT, SEC) * Math and Logic commands (ADC, SBC, AND) * Branching Commands (BEQ, JMP, BPL) 6502 Assembly doesn't really have variables. It just has places in RAM where you can put stuff. You can assign labels to these memory locations and they kind of act like variables, but these labels are effectively global in scope. Doing simple things can take several lines of code, and having a plan for organizing your code is very important. Assembly code can devolve into spaghetti quickly if you are not careful. ### 6502 Registers ![alt text](https://github.com/battlelinegames/nes-starter-kit/blob/master/img/6502-registers.png?raw=true "6502 Registers") According to Wikipedia, a register is a small amount of memory located directly on the CPU. In order to program 6502 assembler for the Nintendo Entertainment System, you will need to learn what registers exist on the 6502 and what there function is. * Accumulator (Register A) is used for most math and logic related tasks. It's really the only general purpose register on the 6502 * X and Y Registers are used for indexing. You may set aside a block of memory for game objects and grab a specific one using one of these registers as in index into that block of memory * The status register holds a series of flags that are set under certain conditions like if adding 2 numbers results in a value larger than an 8 bit value, or if an interrupt has been triggered. These flags are used for conditional branching. * Program Counter is the only 16 bit register. Because the bus is only 8 bits, this register must be loaded one byte at a time. This register is where the system keeps track of what op code is currently being executed. * Stack Pointer is a pointer to the current top of the stack in memory. You can push values to and pull values from the stack when you do things like run subroutines. ### CA65 Assembler ![alt text](https://github.com/battlelinegames/nes-starter-kit/blob/master/img/6502-assembler.png?raw=true "CA65 Assembler") I've included a copy of the CA65 Assembler in the project in cc65/bin. There is also a batch file that will assemble the code into a working .nes iNes file. In the tools directory I've included the Mesen.exe which is a copy of the Mesen emulator. With both of these you may want to go to the websites and download the current version, but my goal is to get you started so I've included them in the starter kit project. [Mesen NES emulator Download](https://www.mesen.ca/) [CC65 6502 Compiler Download](https://www.cc65.org/index.php#Download) The most popular NES tutorial is probably the [Nerdy Nights tutorial](http://nintendoage.com/forum/messageview.cfm?catid=22&threadid=7155) on [Nintendo Age](http://nintendoage.com). In that tutorial they use an assembler called NESASM3, which is a simpler assembler, but I found somewhat limiting. CA65 offers a larger selection of control commands, better macros, and segments, which can be difficult to understand at first but make life a lot easier once you get to know it. I found Nerdy Nights to be a great introduction into NES Game Development, but it didn't feel it gave me a good handle on how to organize my code once my projects got a little larger. #### CA65 Control Commands [CA65 Control commands](https://www.cc65.org/doc/ca65.html#toc11) are commands specific to the CA65 assembler. They typically begin with a '.' such as .res or .macro. They can be very useful and make organizing your code a lot easier. #### .CFG file I go into the config file quite a bit more below in the **File: starter.cfg** section. The config file is used to set up what the memory in both your iNes file and your runtime environment looks like. You also define segments that can be used to specify where your code or variable definitions are going in memory. #### Macros and Procedures (Subroutines) I personally find Macros and procedures to be quite helpful. Macros are a way to combine several lines of code into a single line to use later. For example ``` .macro set set_var, from lda from sta set_var .endmacro ``` This creates a macro called set, which can be used to set the first variable to the value in the second. I find myself using this all the time. It just makes things a little easier because instead of writing an lda and sta line everytime I want to move data from one variable to another, I can just do the following: ``` set var1, var2 ``` This would set the value of var1 to var2. I know this only saves one line of code, but I do it enough that I feel like it's worth writing the macro. Now a macro is actually expanded at compile time, so if you set through your code with a debugger you'll never see the "set" command. It will be replaced with an lda and an sta on two lines. Because of this, I have personally found that having large macros can make things very difficult to debug. Procedures are like macros except you can't pass in any values. Also every time you call a procedure it wastes 12 cycles pushing the program counter register onto the stack and pulling it back off again, not to mention executing the command that lets you jump into the procedure. Because of this, you probably don't want to have a ton of calls to procedures because in some ways you are just throwing away cycles. However, I have found that procedures both help orgainize your code and make things a lot easier to debug. I used them heavily when I wrote [Nesteroids](https://github.com/battlelinegames/nesteroids) with the intention of replacing the calls with faster macros when I needed to optimize. Fortunately it was fast enough without that optimization step so all the procedure calls were left in. [Link to Nesteroids Github Code](https://github.com/battlelinegames/nesteroids) ### 6502 Vectors The NES has no operating system. Because of this, you have to somehow tell the system where to start out, and what to run when the reset button is pressed. You also have to tell the system what to execute on an NMI (Non-maskable interrupt) and on an IRQ (Interrupt Request). The way the 6502 was designed, a pointer to the 16 bit address locations of the code you want to run has to be placed in very specific memory locations in the last 6 bytes of memory. To handle this, I placed a few lines of code at the end of the **header.asm** file. ``` .segment "VECTORS" ; THIS IS THE LAST 6 BYTES OF THE FILE, USED AS ADDRESSES FOR INTERRUPTS .word nmi .word reset .word irq ``` The segment *VECTORS* is defined in the .cfg file as the last 6 bytes of memory. the labels nmi, reset, and irq are labels defined in the *nmi.asm*, *reset.asm*, and *irq.asm* inside of the *vectors* directory. This means that when the system powers on, or when the reset button is pressed the game begins execution at the *reset:* label inside of the *reset.asm* file. Whenever an Non-Maskable Interrupt occurs, code starting at the *nmi:* label inside of the *nmi.asm* file will begin to execute. In this code the *irq* will simply call an *rti* (return from interrupt) when it is called. ## What is an NMI? NMI stands for Non-Maskable Interrupt. The PPU begins drawing pixels at the top of the screen and sweeps from left to right and top to bottom drawing pixels as it goes. While the PPU is drawing to the screen, we can't send commands to it without messing it up, so this is a good time to do things in your gameloop like collision detection, or calculating where game objects will be located on the next draw. When the PPU finishes drawing to the screen, on old televisions the beam that was doing the drawing would take little time to move back to the top left position. This is called a V-Blank. During this time period, the PPU isn't busy, so this is when you can start sending commands to the PPU. The NMI occurs on this V-Blank, and the CPU will actually stop executing whatever it is doing at the time to jump into the NMI code. When you're in the NMI, you have about 2200ish cycles to tell the PPU all that you would like it to do before it starts drawing to the screen again. If you take too much time, you'll start to see garbage getting rendered out to the screen as the PPU is trying to draw while you're harassing it. ## What is a gameloop? Modern games don't really need to worry about when the screen refresh happens. Most games today have plenty of memory which allows them to "double buffer" or draw everything that will appear on the screen to an offscreen buffer, then just swap the buffers out as you take another trip through your gameloop. On an NES the game loop has to coordinatte with the NMI so that all the necessary PPU instructions are issued during the NMI and not when the PPU is busy. You will have about 10x as many cycles in your gameloop as in your NMI, so try to do as much as you can in the gameloop leaving only interactions with the PPU for the NMI. # Understanding the NES PPU The PPU (Picture Processing Unit) was a kind of early GPU (Graphics Processing Unit) that the NES used to render images to the screen. If we go back to that architecture drawing I made earlier, you'll notice that the PPU has access to the CHR ROM which it maps as the first 8K of memory. ![alt text](https://github.com/battlelinegames/nes-starter-kit/blob/master/img/NES-Architecture.png?raw=true "NES Architecture") The CHR ROM is an 8K ROM chip that in early NES games held all of the sprite and background image data for the game. The CHR format used 2 bits per pixel, so each sprite and background image pixel only had 3 possible colors and a transparent color. Each one of these 3 potential colors were mapped to a Palette which referenced one of the 64 colors the NES was capable of drawing *(In reality it was more like 54 colors becasue for some reason black was in there 10 times and white twice)*. ### Sprites and Background Tiles Each sprite and background tile is an 8x8 square of pixels. The CHR ROM has 4K dedicated to sprite image data and 4K dedicated to background image data. This is a png version of the image data I used for [Nesteroids](https://github.com/battlelinegames/nesteroids): ![alt text](https://github.com/battlelinegames/nes-starter-kit/blob/master/img/AsteroidSheetMonochrome.png?raw=true "Nesteroids Image Data") The top half of the file is used for sprites and moving objects such as the space ship, the ufo, asteroids and bullets. The bottom half is used for background information that scrolls into view such as the **Nesteroids** logo. Now this is a .png file and not a .chr file which is required by the CA65 Assembler. To convert it to the chr file you will need to use a program like [YY-CHR](http://wiki.nesdev.com/w/index.php/YY-CHR) which I have included in the *tools* directory in this project. You can modify the images directly in YY-CHR, which I find difficult. My process involves creating and animating the sprites using [Aseprite](https://www.aseprite.org/), then putting it together into a 128x256 pixel file in Photoshop, then copy and pasting it into YY-CHR. That sounds like a pain... and it is, but I still found it easier than doing my art directly in YY-CHR. ### Telling the PPU what to do You can't directly execute code on the PPU. The 6502 interfaces with other devices by setting aside memory locations that aren't really memory locations but interfaces into those other devices. Those devices are responsible for watching the bus to see if the CPU is trying to talk to it. For instance, in this project we want to srite data to the nametable at a specific location to display the text in the macro *printf_nmi*. The first thing I do is read from PPU_STATUS ($2002) with an LDA. This isn't really to read that data. It's really to tell the PPU to get ready for me to send it a command. I want to write some bytes into the nametable to tell the PPU to swap out existing background tiles with new ones that I give it. To tell it where those tiles will be located in the backgound, I need to figure out where in memory those positions are located. The first thing I do in the macro is get the **ROW** and **COL** from the **XPOS** and **YPOS** that I passed in to the macro by dividing by 8. ``` .macro printf_nmi STRING, XPOS, YPOS .local ROW .local COL .local BYTE_OFFSET_HI .local BYTE_OFFSET_LO ROW = YPOS / 8 COL = XPOS / 8 ``` Right now you're probably saying: "Hey, that doesn't look like assembly" And you would be right. The magic of macros and CA65 is that those values get set when the program is assembled an not when it's running on the NES. ROW and COL are basically constants that get set every time we call printf_nmi. XPOS and YPOS have to be passed in as constants as well or this won't work. If you attempted to pass in a variable to printf_nmi it would blow up because it wouldn't be able to resolve ROW and COL when this is assembled. The next couple of lines are still done during assembly: ``` BYTE_OFFSET_HI = (ROW * 32 + COL) / 256 + 32 BYTE_OFFSET_LO = (ROW * 32 + COL) .mod 256 ``` When we come out of this after assembly we have constant values we've calculated for BYTE_OFFSET_HI AND BYTE_OFFSET_LO. The next few lines are actually done while the game is executing during the NMI. We read from $2002 (PPU_STATUS) to tell the PPU we are about to do something. Then we write to $2006 twice to send a 16 bit address one byte at a time. After that we write to $2007 (PPU_DATA) with the data we want to write to the address specified in the nametable. ``` lda PPU_STATUS ; PPU_STATUS = $2002 lda #BYTE_OFFSET_HI sta PPU_ADDR ; PPU_ADDR = $2006 lda #BYTE_OFFSET_LO sta PPU_ADDR ; PPU_ADDR = $2006 .repeat .strlen(STRING), I lda #.strat(STRING, I) sta PPU_DATA ; PPU_DATA = $2007 .endrep .endmacro ``` ## Understanding Palettes ![alt text](https://github.com/battlelinegames/nes-starter-kit/blob/master/img/PPU-palettes.png?raw=true "NES PPU Palettes") The NES has a pretty limited color table for you to chose from. Your game can have 4 palettes to use for background tiles and 4 palettes to use for sprites. Inside the *palette.asm* file I've defined all of the color values to make it easier to read. ``` DARK_GRAY = $00 MEDIUM_GRAY = $10 LIGHT_GRAY = $20 LIGHTEST_GRAY = $30 DARK_BLUE = $01 MEDIUM_BLUE = $11 LIGHT_BLUE = $21 LIGHTEST_BLUE = $31 DARK_INDIGO = $02 MEDIUM_INDIGO = $12 LIGHT_INDIGO = $22 LIGHTEST_INDIGO = $32 DARK_VIOLET = $03 MEDIUM_VIOLET = $13 LIGHT_VIOLET = $23 LIGHTEST_VIOLET = $33 DARK_PURPLE = $04 MEDIUM_PURPLE = $14 LIGHT_PURPLE = $24 LIGHTEST_PURPLE = $34 DARK_REDVIOLET = $05 MEDIUM_REDVIOLET = $15 LIGHT_REDVIOLET = $25 LIGHTEST_REDVIOLET = $35 DARK_RED = $06 MEDIUM_RED = $16 LIGHT_RED = $26 LIGHTEST_RED = $36 DARK_ORANGE = $07 MEDIUM_ORANGE = $17 LIGHT_ORANGE = $27 LIGHTEST_ORANGE = $37 DARK_YELLOW = $08 MEDIUM_YELLOW = $18 LIGHT_YELLOW = $28 LIGHTEST_YELLOW = $38 DARK_CHARTREUSE = $09 MEDIUM_CHARTREUSE = $19 LIGHT_CHARTREUSE = $29 LIGHTEST_CHARTREUSE = $39 DARK_GREEN = $0a MEDIUM_GREEN = $1a LIGHT_GREEN = $2a LIGHTEST_GREEN = $3a DARK_CYAN = $0b MEDIUM_CYAN = $1b LIGHT_CYAN = $2b LIGHTEST_CYAN = $3b DARK_TURQUOISE = $0c MEDIUM_TURQUOISE = $1c LIGHT_TURQUOISE = $2c LIGHTEST_TURQUOISE = $3c BLACK = $0f DARKEST_GRAY = $2d MEDIUM_GRAY2 = $3d ``` I have some labels inside the PRG ROM to use as the palettes in the game. ``` .segment "ROMDATA" palette_background: .byte BLACK, LIGHTEST_YELLOW, MEDIUM_ORANGE, DARK_ORANGE .byte BLACK, DARK_CHARTREUSE, MEDIUM_CHARTREUSE, LIGHT_CHARTREUSE .byte BLACK, DARK_BLUE, MEDIUM_BLUE, LIGHT_BLUE .byte BLACK, DARK_GRAY, MEDIUM_GRAY, LIGHTEST_GRAY palette_sprites: .byte BLACK, LIGHTEST_YELLOW, LIGHT_ORANGE, MEDIUM_ORANGE .byte BLACK, MEDIUM_PURPLE, LIGHT_PURPLE, LIGHTEST_PURPLE .byte BLACK, MEDIUM_CYAN, LIGHT_CYAN, LIGHTEST_CYAN .byte BLACK, MEDIUM_INDIGO, LIGHT_INDIGO, LIGHTEST_INDIGO ``` Inside the ppu.asm I've created a procedure called *load_palettes* which loads all the palette data into memory location $3F00 in PPU memory. ``` .proc load_palettes lda PPU_STATUS ; read PPU status to reset the high/low latch ; PPUADDR $2006 aaaa aaaa PPU read/write address (two writes: MSB, LSB) ;----------+-------+----------+---------------------------------------------' ;| $2006 | W2 | aaaaaaaa | PPU Memory Address [PPUADDR] | ;| | | | | ;| | | | Specifies the address in VRAM in which | ;| | | | data should be read from or written to. | ;| | | | T ... ...

近期下载者

相关文件


收藏者