diff --git a/docs/stdlib/nes.md b/docs/stdlib/nes.md index fa18f096..683007a4 100644 --- a/docs/stdlib/nes.md +++ b/docs/stdlib/nes.md @@ -4,13 +4,71 @@ ## nes_hardware -The `nes_hardware` module is imported automatically on NES targets. +The `nes_hardware` module is imported automatically on NES targets. +It contains defintions for the NES's memory-mapped registers, as well +as some joypad and PPU management routines. -TODO +#### `PPU memory-mapped registers` + +Named memory locations for interfacing with the PPU's memory-mapped registers. + +Available variables: + +* `byte ppu_ctrl @$2000` +* `byte ppu_mask @$2001` +* `byte ppu_status @$2002` +* `byte oam_addr @$2003` +* `byte oam_data @$2004` +* `byte ppu_scroll @$2005` +* `byte ppu_addr @$2006` +* `byte ppu_data @$2007` +* `byte oam_dma @$4014` + +#### `byte strobe_joypad()` + +Updates joypad1 by querying for new button states. + +#### `byte read_joypad1()` + +Get joypad1's state as a byte. + +#### `byte read_joypad2()` + +Get joypad2's state as a byte. + +#### `void simulate_reset()` + +Simulates a hardware reset by jumping to the reset vector, +which then calls main(). + +#### `void ppu_set_addr(word ax)` + +Sets the PPU to point at the VRAM address at ax, usually in preparation +for a write via ppu_write_data(). + +#### `byte read_ppu_status()` + +Gets the PPU status byte. + +#### `void ppu_set_scroll(byte a, byte x)` + +Sets the PPU scroll register. Parameter a defines the horizontal +(X-axis) scroll value, and parameter x defines the vertical (Y-axis) +scroll value. + +#### `void ppu_write_data(byte a)` + +Writes a byte to the PPU's VRAM at the address the PPU +is currently pointing to. Usually used after a call to ppu_set_addr(). + +#### `void ppu_oam_dma_write(byte a)` + +Initiates a DMA transfer of 256 bytes from CPU memory address $xx00-$xxFF +to PPU OAM memory, where xx is the hexadecimal representation of parameter a. ## nes_mmc4 -The `nes_mmc4` module is imported automatically on the NES MMC4 target. +The `nes_mmc4` module is imported automatically on the NES MMC4 target and contains routines related to MMC4 bankswitching. #### `void set_prg_bank(byte a)` diff --git a/examples/README.md b/examples/README.md index 216b954c..3e535400 100644 --- a/examples/README.md +++ b/examples/README.md @@ -49,6 +49,8 @@ how to create a program made of multiple files loaded on demand * [MMC4 example](nes/nestest_mmc4.mfk) – the same thing as above, but uses a MMC4 mapper just to test bankswitching +* [Pong example](nes/pong.mfk) – simple pong example based off pong1.asm by bunnyboy of the nintendoage.com forums + ## Atari Lynx examples * [Lynx demo example](atari_lynx/atari_lynx_demo.mfk) – a simple sprite demo diff --git a/examples/nes/pong.mfk b/examples/nes/pong.mfk new file mode 100644 index 00000000..1b88dd9c --- /dev/null +++ b/examples/nes/pong.mfk @@ -0,0 +1,630 @@ +// Taken from Nerdy Nights Week 7's pong example game +// (http://nintendoage.com/forum/messageview.cfm?catid=22&threadid=8747) +// +// Original example made by bunnyboy of nintendoage.com +// Millfork adaptation by Garydos (https://github.com/Garydos) +// +// compile with -t nes_small + +import random +import nes_joy + +// *STRUCT DEFINTIONS* + +struct Ball { + byte x, // ball horizontal position + byte y, // ball vertical position + byte up, // 1 = ball moving up + byte down, // 1 = ball moving down + byte left, // 1 = ball moving left + byte right, // 1 = ball moving right + byte speedx, // ball horizontal speed per frame + byte speedy // ball vertical speed per frame +} + +struct Sprite { //NES hardware sprite layout + byte y, // Y Coordinate - 1 + byte tile, // tile index # + byte attrs, // attributes + byte x // X Coordinate +} + +struct Paddle_Sprs { + Sprite paddletop, + Sprite paddlebody1, + Sprite paddlebody2, + Sprite paddlebottom +} + +// *VARIABLES* + +byte paddle1ytop // player 1 paddle top vertical position +byte paddle2ytop // player 2 paddle bottom vertical position +byte score1 // player 1 score, 0-15 +byte score2 // player 2 score, 0-15 +Ball ball // the ball +array oam_buffer [256] @$200 // sprite buffer +Sprite ball_spr @$200 // ball's sprite +Paddle_Sprs left_paddle_sprs @$204 // left paddle's sprites +Paddle_Sprs right_paddle_sprs @$214 // right paddle's sprites +word framecounter // counts the amount of frames that have passed + +volatile Gamestate gamestate // the current Gamestate + + +// *CONSTANTS* + +enum Gamestate { + STATETITLE, // displaying title screen + STATEPLAYING, // move paddles/ball, check for collisions + STATEGAMEOVER // displaying game over screen +} + +const byte RIGHTWALL = $F4 // when ball reaches one of these, do something +const byte TOPWALL = $18 +const byte BOTTOMWALL = $B0 +const byte LEFTWALL = $04 + +const byte PADDLE1X = $08 // horizontal position for paddles, doesnt move +const byte PADDLE2X = $F0 + +const byte PADDLEHEIGHT = $20 // height of each paddle in pixels + +// sprite tiles used by the paddles +const byte PADDLETOPBOTSPR = $00 +const byte PADDLEBODYSPR = $01 + +// sprite attributes used by the paddles +const byte PADDLESPRATTR = $01 +const byte PADDLESPRATTRHFLIP = $41 +const byte PADDLESPRATTRVFLIP = $81 +const byte PADDLESPRATTRHVFLIP = $C1 + +// sprite tiles and attributes used by the ball +const byte BALLSPRATTR = $01 +const byte BALLSPR = $02 + +// vram locations declared as constants for readability/convenience +// *note that these do not correlate to CPU ram locations* +const word ppu_pallete_ram = $3F00 +const word ppu_nametable_ram = $2000 +const word ppu_nametable_0_attr_ram = $23C0 + + +void main() { + //Set starting game state + gamestate = STATETITLE + //prepare the title screen gamestate + game_title_init() + while(true){} // all work is done in NMI +} + +void nmi() { + //Push sprite information to the PPU through DMA transfer + ppu_oam_dma_write(oam_buffer.addr.hi) + + //Run graphics updates + game logic specific to each gamestate + main_game_logic() +} + +void irq() { + +} + +inline void main_game_logic() { + // use a return dispatch here + // to use different logic for each screen/gamestate + return [gamestate] { + STATETITLE @ game_title_logic + STATEPLAYING @ game_playing_logic + STATEGAMEOVER @ game_gameover_logic + } +} + +void game_title_init() { + byte i + //for now, turn off the screen and nmi + ppu_ctrl = 0 + ppu_mask = 0 + + //initialize the sprites and palletes + init_graphics() + + //write a full screen of background data + + load_sky_background() + + //write the title screen message + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$018B) // point the PPU to the message's start + for i,0,until,$0B { + ppu_write_data(title_msg[i]) + } + + //write the border + //top border + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$0020) + for i,0,until,$20 { + ppu_write_data($01) + } + //bottom border + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$0380) + for i,0,until,$20 { + ppu_write_data($01) + } + + //set ppu address increment to 32 so we can draw the left and right borders + //(allows us to draw to the nametable in vertical strips rather than horizontal) + ppu_ctrl = %00000100 + + //left border + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram) + for i,0,until,$20 { + ppu_write_data($01) + } + //right border + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$1F) + for i,0,until,$20 { + ppu_write_data($01) + } + + + framecounter = 0 + ppu_set_scroll(0,0) + ppu_wait_vblank() //wait for next vblank before re-enabling NMI + //so that we don't get messed up scroll registers + //re-enable the screen and nmi + ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 + ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side +} + +void game_title_logic() { + read_joy1() + if input_start != 0 { + rand_seed = framecounter //seed the random number generator with the amount of frames + //that have passed since the title screen was shown + gamestate = STATEPLAYING + game_playing_init() + return + } + framecounter += 1 +} + +inline asm void ppu_wait_vblank() { + vblankwait: + BIT $2002 + ! BPL vblankwait + ? RTS +} + +void game_playing_init() { + //for now, turn off the screen and nmi + ppu_ctrl = 0 + ppu_mask = 0 + + //write a full screen of data + load_sky_background() + draw_score_text_background() + draw_boundaries_background() + load_play_attribute_table() + //initialize the game + init_game() + reset_ball() + + ppu_set_scroll(0,0) + ppu_wait_vblank() //wait for next vblank before re-enabling NMI + //so that we don't get messed up scroll registers + //re-enable the screen and nmi + ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 + ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side +} + +void game_playing_logic() { + draw_score() + //update scroll last because writes to vram also + //overwrite the scroll register + ppu_set_scroll(0,0) // tell the ppu there is no background scrolling + + move_ball() + move_paddles() + check_paddle_collision() + update_sprites() + if score1 >= 15 || score2 >= 15{ + //Someone's reached 15 points, the game is over, + //so set the state to game over and reset the + //framecounter + gamestate = STATEGAMEOVER + + //move all the sprites off screen + //in preperation for the gameover screen + ball_spr.y = $ef + + left_paddle_sprs.paddletop.y = $ef + left_paddle_sprs.paddlebody1.y = $ef + left_paddle_sprs.paddlebody2.y = $ef + left_paddle_sprs.paddlebottom.y = $ef + + right_paddle_sprs.paddletop.y = $ef + right_paddle_sprs.paddlebody1.y = $ef + right_paddle_sprs.paddlebody2.y = $ef + right_paddle_sprs.paddlebottom.y = $ef + + game_gameover_init() + } +} + +void game_gameover_init() { + //for now, turn off nmi and sprites + ppu_ctrl = 0 + ppu_mask = 0 + + draw_score() //draw the final score + draw_game_over_screen() //draw the game over message + + framecounter = 0 + ppu_set_scroll(0,0) // tell the ppu there is no background scrolling + ppu_wait_vblank() //wait for next vblank before re-enabling NMI + //so that we don't get messed up scroll registers + //re-enable the screen and nmi + ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 + ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side +} + +void game_gameover_logic() { + if framecounter >= 240{ + //3 seconds have passed, + //reset the game + simulate_reset() + } + framecounter += 1 +} + +void move_ball() { + if ball.up == 1 { + ball.y -= ball.speedy + if ball.y <= TOPWALL { + //bounce, ball now moving down + ball.down = 1 + ball.up = 0 + } + } + else if ball.down == 1 { + ball.y += ball.speedy + if ball.y + 8 >= BOTTOMWALL { + //bounce, ball now moving up + ball.down = 0 + ball.up = 1 + } + } + + if ball.right == 1 { + ball.x += ball.speedx + if ball.x >= RIGHTWALL { + // ball has gone past a paddle and hit the right wall + // give player1 a point and reset the ball + score1 += 1 + reset_ball() + } + } + else if ball.left == 1 { + ball.x -= ball.speedx + if ball.x <= LEFTWALL { + // ball has gone past a paddle and hit the left wall + // give player2 a point and reset the ball + score2 += 1 + reset_ball() + } + } +} + +void move_paddles() { + // Player 1 controls + read_joy1() + if input_dy < 0 { // Up button + if paddle1ytop > TOPWALL { + paddle1ytop -= 1 + } + } + else if input_dy > 0 { // Down button + if (paddle1ytop + PADDLEHEIGHT) < BOTTOMWALL { + paddle1ytop += 1 + } + } + + // Player 2 controls + read_joy2() + if input_dy < 0 { // Up button + if paddle2ytop > TOPWALL { + paddle2ytop -= 1 + } + } + else if input_dy > 0 { // Down button + if (paddle2ytop + PADDLEHEIGHT) < BOTTOMWALL { + paddle2ytop += 1 + } + } +} + +void check_paddle_collision() { + //Check left paddle collision + if ball.x <= PADDLE1X+8 && ball.y >= paddle1ytop && ball.y <= paddle1ytop + PADDLEHEIGHT { + //bounce the ball back, move it right + ball.left = 0 + ball.right = 1 + } + + //Check right paddle collision + if ball.x >= PADDLE2X-8 && ball.y >= paddle2ytop && ball.y <= paddle2ytop + PADDLEHEIGHT { + //bounce the ball back, move it left + ball.left = 1 + ball.right = 0 + } +} + +inline void init_graphics() { + init_sprites() + load_palletes() +} + +macro void load_palletes() { + byte i + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_pallete_ram) // point the PPU to palette ram + for i,0,until,$20 { + ppu_write_data(pallete[i]) + } +} + +inline void load_sky_background() { + word xx + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram) // point the PPU to palette ram + for xx,0,until,$0400 { + ppu_write_data($00) // $00 = sky + } +} + +macro void draw_score_text_background() { + byte i + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$20) // point the PPU to score text's start + for i,0,until,$1C { + ppu_write_data(scorebackground[i]) + } +} + +macro void draw_boundaries_background() { + byte i + + //draw top boundary + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$40) // point the PPU to the top boundary's start + for i,0,until,$20 { + ppu_write_data($81) //write the top boundary tile + } + + //draw bottom boundary + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$02C0) // point the PPU to the top boundary's start + for i,0,until,$20 { + ppu_write_data($80) //write the bottom boundary tile + } +} + +void draw_game_over_screen() { + byte i + + //draw the static game over message + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$0107) // point the PPU to the message's start + for i,0,until,$12 { + ppu_write_data(gameover_msg[i]) + } + //draw the win message + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$01AC) // point the PPU to the message's start + if score1 >= score2 { + for i,0,until,$0B { + ppu_write_data(p1_win_msg[i]) + } + } + else { + for i,0,until,$0B { + ppu_write_data(p2_win_msg[i]) + } + } +} + +void draw_p1_win_msg() { + byte i + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$0140) // point the PPU to the message's start + for i,0,until,$20 { + ppu_write_data(p1_win_msg[i]) + } +} + +void draw_p2_win_msg() { + byte i + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$0140) // point the PPU to the message's start + for i,0,until,$20 { + ppu_write_data(p2_win_msg[i]) + } +} + +macro void load_play_attribute_table() { + byte i + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_0_attr_ram) // point the PPU to nametable 0's attribute table + for i,0,until,$10 { + ppu_write_data(attribute[i]) + } +} + +void init_sprites() { + byte i + for i,0,to,255 { + if (i & %00000011) == 0 { + //each sprite takes up 4 bytes, and we want to edit + //the y position of each sprite (0th byte) + //so we use the %00000011 mask to write every 4th byte (every 0th sprite byte) + + oam_buffer[i] = $ef // move the sprite off screen + } + else { + oam_buffer[i] = 0 + } + } +} + +void init_game() { + //Set some initial ball stats + ball.down = $00 + ball.right = $01 + ball.up = $00 + ball.left = $00 + ball.y = $50 + ball.x = $80 + ball.speedx = $02 + ball.speedy = $02 + + //Set initial paddle states + paddle1ytop = $70 + paddle2ytop = $70 +} + +void update_sprites() { + //Update ball sprite + ball_spr.y = ball.y + ball_spr.tile = BALLSPR + ball_spr.attrs = BALLSPRATTR + ball_spr.x = ball.x + + //update paddle sprites + + // update top of left paddle + left_paddle_sprs.paddletop.y = paddle1ytop + left_paddle_sprs.paddletop.tile = PADDLETOPBOTSPR + left_paddle_sprs.paddletop.attrs = PADDLESPRATTRVFLIP + left_paddle_sprs.paddletop.x = PADDLE1X + + // update body of left paddle + left_paddle_sprs.paddlebody1.y = paddle1ytop + 8 + left_paddle_sprs.paddlebody1.tile = PADDLEBODYSPR + left_paddle_sprs.paddlebody1.attrs = PADDLESPRATTRVFLIP + left_paddle_sprs.paddlebody1.x = PADDLE1X + + left_paddle_sprs.paddlebody2.y = paddle1ytop + 16 + left_paddle_sprs.paddlebody2.tile = PADDLEBODYSPR + left_paddle_sprs.paddlebody2.attrs = PADDLESPRATTRVFLIP + left_paddle_sprs.paddlebody2.x = PADDLE1X + + // update bottom of left paddle + left_paddle_sprs.paddlebottom.y = paddle1ytop + 24 + left_paddle_sprs.paddlebottom.tile = PADDLETOPBOTSPR + left_paddle_sprs.paddlebottom.attrs = PADDLESPRATTR + left_paddle_sprs.paddlebottom.x = PADDLE1X + + // update top of right paddle + right_paddle_sprs.paddletop.y = paddle2ytop + right_paddle_sprs.paddletop.tile = PADDLETOPBOTSPR + right_paddle_sprs.paddletop.attrs = PADDLESPRATTRHVFLIP + right_paddle_sprs.paddletop.x = PADDLE2X + + // update body of right paddle + right_paddle_sprs.paddlebody1.y = paddle2ytop + 8 + right_paddle_sprs.paddlebody1.tile = PADDLEBODYSPR + right_paddle_sprs.paddlebody1.attrs = PADDLESPRATTRHVFLIP + right_paddle_sprs.paddlebody1.x = PADDLE2X + + right_paddle_sprs.paddlebody2.y = paddle2ytop + 16 + right_paddle_sprs.paddlebody2.tile = PADDLEBODYSPR + right_paddle_sprs.paddlebody2.attrs = PADDLESPRATTRHVFLIP + right_paddle_sprs.paddlebody2.x = PADDLE2X + + // update bottom of right paddle + right_paddle_sprs.paddlebottom.y = paddle2ytop + 24 + right_paddle_sprs.paddlebottom.tile = PADDLETOPBOTSPR + right_paddle_sprs.paddlebottom.attrs = PADDLESPRATTRHFLIP + right_paddle_sprs.paddlebottom.x = PADDLE2X +} + +inline void draw_score() { + byte digit01 + byte digit10 + read_ppu_status() // read PPU status to reset the high/low latch + + //display player1's score + digit01 = score1 %% 10 //get the ones digit + digit10 = score1 / 10 //get the tens digit + digit10 %%= 10 + + ppu_set_addr(ppu_nametable_ram+$29) // point the PPU to player1's score number + if digit10 > 0 { + ppu_write_data(digit10 + '0') + } + ppu_write_data(digit01 + '0') + + //display player2's score + digit01 = score2 %% 10 //get the ones digit + digit10 = score2 / 10 //get the tens digit + digit10 %%= 10 + + ppu_set_addr(ppu_nametable_ram+$3C) // point the PPU to player2's score number + if digit10 > 0 { + ppu_write_data(digit10 + '0') + } + ppu_write_data(digit01 + '0') +} + +void reset_ball() { + byte dir + + //randomize up/down motion + dir = rand() + dir = dir & %00000001 // dir is now either 0 or 1 + ball.down = dir + ball.up = dir ^ %00000001 //flip the bit + + //randomize left/right motion + dir = rand() + dir = dir & %00000001 // dir is now either 0 or 1 + ball.right = dir + ball.left = dir ^ %00000001 //flip the bit + + //reset the ball to the center and set its speed + ball.y = $50 + ball.x = $80 + ball.speedx = $02 + ball.speedy = $02 +} + +// *LEVEL GRAPHICS* + +//palletes for entire game (both title and play screens) +const array pallete = [ + $22,$29,$1A,$0F, $22,$36,$17,$0F, $22,$30,$21,$0F, $22,$27,$17,$0F, //background palette + $22,$1C,$15,$14, $22,$02,$38,$3C, $22,$1C,$15,$14, $22,$02,$38,$3C //sprite palette +] + +const array scorebackground = "P1 Score- P2 Score-" ascii +const array gameover_msg = "G A M E O V E R" ascii +const array p1_win_msg = "P1 Wins!" ascii +const array p2_win_msg = "P2 Wins!" ascii +const array title_msg = "Press Start" ascii + +//attribute table for play screen graphics +const array attribute = [ + %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, + %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, + %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101 +] + + +// *CHARACTER ROM (GRAPHICS)* +segment(chrrom) const array graphics @ $0000 = file("tiles.chr") diff --git a/examples/nes/tiles.chr b/examples/nes/tiles.chr new file mode 100644 index 00000000..2fdacfeb Binary files /dev/null and b/examples/nes/tiles.chr differ