/* A platform game with a randomly generated stage. Uses the Famitone2 library for sound and music. It scrolls vertically (horizontal mirroring) and nametable updates are performed offscreen one row at a time with the draw_floor_line() function. The enemies use the same movement logic as the player, just with random inputs. */ #include #include // include NESLIB header #include "neslib.h" // include CC65 NES Header (PPU) #include // BCD arithmetic support #include "bcd.h" //#link "bcd.c" // VRAM update buffer #include "vrambuf.h" //#link "vrambuf.c" // link the pattern table into CHR ROM //#link "chr_generic.s" // famitone2 library //#link "famitone2.s" // music and sfx //#link "music_dangerstreets.s" extern char danger_streets_music_data[]; //#link "demosounds.s" extern char demo_sounds[]; // indices of sound effects (0..3) typedef enum { SND_START, SND_HIT, SND_COIN, SND_JUMP } SFXIndex; ///// DEFINES #define COLS 30 // floor width in tiles #define ROWS 60 // total nametable height in tiles #define MAX_FLOORS 20 // total # of floors in a stage #define GAPSIZE 4 // gap size in tiles #define BOTTOM_FLOOR_Y 2 // offset for bottommost floor #define MAX_ACTORS 8 // max # of moving actors #define SCREEN_Y_BOTTOM 208 // bottom of screen in pixels #define ACTOR_MIN_X 16 // leftmost position of actor #define ACTOR_MAX_X 228 // rightmost position of actor #define ACTOR_SCROLL_UP_Y 110 // min Y position to scroll up #define ACTOR_SCROLL_DOWN_Y 140 // max Y position to scroll down #define JUMP_VELOCITY 18 // Y velocity when jumping // constants for various tiles #define CH_BORDER 0x40 #define CH_FLOOR 0xf4 #define CH_LADDER 0xd4 #define CH_ITEM 0xc4 #define CH_BLANK 0x20 ///// GLOBALS // vertical scroll amount in pixels static int scroll_pixel_yy = 0; // vertical scroll amount in tiles (scroll_pixel_yy / 8) static byte scroll_tile_y = 0; // last screen Y position of player sprite static byte player_screen_y = 0; // score (BCD) static byte score = 0; // screen flash animation (virtual bright) static byte vbright = 4; // random byte between (a ... b-1) // use rand() because rand8() has a cycle of 255 byte rndint(byte a, byte b) { return (rand() % (b-a)) + a; } // return nametable address for tile (x,y) // assuming vertical scrolling (horiz. mirroring) word getntaddr(byte x, byte y) { word addr; if (y < 30) { addr = NTADR_A(x,y); // nametable A } else { addr = NTADR_C(x,y-30); // nametable C } return addr; } // convert nametable address to attribute address word nt2attraddr(word a) { return (a & 0x2c00) | 0x3c0 | ((a >> 4) & 0x38) | ((a >> 2) & 0x07); } /// METASPRITES // define a 2x2 metasprite #define DEF_METASPRITE_2x2(name,code,pal)\ const unsigned char name[]={\ 0, 0, (code)+0, pal, \ 0, 8, (code)+1, pal, \ 8, 0, (code)+2, pal, \ 8, 8, (code)+3, pal, \ 128}; // define a 2x2 metasprite, flipped horizontally #define DEF_METASPRITE_2x2_FLIP(name,code,pal)\ const unsigned char name[]={\ 8, 0, (code)+0, (pal)|OAM_FLIP_H, \ 8, 8, (code)+1, (pal)|OAM_FLIP_H, \ 0, 0, (code)+2, (pal)|OAM_FLIP_H, \ 0, 8, (code)+3, (pal)|OAM_FLIP_H, \ 128}; // right-facing DEF_METASPRITE_2x2(playerRStand, 0xd8, 0); DEF_METASPRITE_2x2(playerRRun1, 0xdc, 0); DEF_METASPRITE_2x2(playerRRun2, 0xe0, 0); DEF_METASPRITE_2x2(playerRRun3, 0xe4, 0); DEF_METASPRITE_2x2(playerRJump, 0xe8, 0); DEF_METASPRITE_2x2(playerRClimb, 0xec, 0); DEF_METASPRITE_2x2(playerRSad, 0xf0, 0); // left-facing DEF_METASPRITE_2x2_FLIP(playerLStand, 0xd8, 0); DEF_METASPRITE_2x2_FLIP(playerLRun1, 0xdc, 0); DEF_METASPRITE_2x2_FLIP(playerLRun2, 0xe0, 0); DEF_METASPRITE_2x2_FLIP(playerLRun3, 0xe4, 0); DEF_METASPRITE_2x2_FLIP(playerLJump, 0xe8, 0); DEF_METASPRITE_2x2_FLIP(playerLClimb, 0xec, 0); DEF_METASPRITE_2x2_FLIP(playerLSad, 0xf0, 0); // rescuee at top of building const unsigned char personToSave[]={ 0, 0, (0xba)+0, 3, 0, 8, (0xba)+2, 0, 8, 0, (0xba)+1, 3, 8, 8, (0xba)+3, 0, 128}; // player run sequence const unsigned char* const playerRunSeq[16] = { playerLRun1, playerLRun2, playerLRun3, playerLRun1, playerLRun2, playerLRun3, playerLRun1, playerLRun2, playerRRun1, playerRRun2, playerRRun3, playerRRun1, playerRRun2, playerRRun3, playerRRun1, playerRRun2, }; ///// GAME LOGIC // struct definition for a single floor typedef struct Floor { byte ypos; // # of tiles from ground int height:4; // # of tiles to next floor int gap:4; // X position of gap int ladder1:4; // X position of first ladder int ladder2:4; // X position of second ladder int objtype:4; // item type (FloorItem) int objpos:4; // X position of object } Floor; // various items the player can pick up typedef enum FloorItem { ITEM_NONE, ITEM_MINE, ITEM_HEART, ITEM_POWER }; // array of floors Floor floors[MAX_FLOORS]; // is this x (pixel) position within the gap ? bool is_in_gap(byte x, byte gap) { if (gap) { byte x1 = gap*16 + 4; return (x > x1 && x < x1+GAPSIZE*8-4); } else { return false; } } // is this ladder at (tile) position x within the gap? bool ladder_in_gap(byte x, byte gap) { return gap && x >= gap && x < gap+GAPSIZE*2; } // create floors at start of game void make_floors() { byte i; byte y = BOTTOM_FLOOR_Y; Floor* prevlev = &floors[0]; for (i=0; iheight = rndint(2,5)*2; do { // only have gaps in higher floors lev->gap = i>=5 ? rndint(0,13) : 0; } while (ladder_in_gap(prevlev->ladder1, lev->gap) || ladder_in_gap(prevlev->ladder2, lev->gap)); do { lev->ladder1 = rndint(1,14); lev->ladder2 = rndint(1,14); } while (ladder_in_gap(lev->ladder1, lev->gap) || ladder_in_gap(lev->ladder2, lev->gap)); if (i > 0) { lev->objtype = rndint(1,4); do { lev->objpos = rndint(1,14); } while (ladder_in_gap(lev->objpos, lev->gap)); } lev->ypos = y; y += lev->height; prevlev = lev; } // top floor is special floors[MAX_FLOORS-1].height = 15; floors[MAX_FLOORS-1].gap = 0; floors[MAX_FLOORS-1].ladder1 = 0; floors[MAX_FLOORS-1].ladder2 = 0; floors[MAX_FLOORS-1].objtype = 0; } // creete actors on floor_index, if slot is empty void create_actors_on_floor(byte floor_index); // draw a nametable line into the frame buffer at // 0 == bottom of stage void draw_floor_line(byte screen_y) { char buf[COLS]; char attrs[8]; byte floor, i; byte y = screen_y; byte rowy; word addr; for (floor=0; floorypos; if (dy >= 254) dy = 0; // if below BOTTOM_Y_FLOOR // is this floor visible on-screen? if (dy < lev->height) { if (dy <= 1) { // iterate through all 32 columns for (i=0; igap) memset(buf+lev->gap*2, 0, GAPSIZE); } else { // clear buffer memset(buf, 0, sizeof(buf)); // draw walls if (floor < MAX_FLOORS-1) { buf[0] = CH_FLOOR+1; // left side buf[COLS-1] = CH_FLOOR; // right side } // draw ladders if (lev->ladder1) { buf[lev->ladder1*2] = CH_LADDER; buf[lev->ladder1*2+1] = CH_LADDER+1; } if (lev->ladder2) { buf[lev->ladder2*2] = CH_LADDER; buf[lev->ladder2*2+1] = CH_LADDER+1; } } // draw object, if it exists if (lev->objtype) { byte ch = lev->objtype*4 + CH_ITEM; if (dy == 2) { buf[lev->objpos*2] = ch+1; // bottom-left buf[lev->objpos*2+1] = ch+3; // bottom-right } else if (dy == 3) { buf[lev->objpos*2] = ch+0; // top-left buf[lev->objpos*2+1] = ch+2; // top-right } } // compute row in name buffer and address rowy = (ROWS-1) - (screen_y%60); addr = getntaddr(1, rowy); // copy attribute table (every 4th row) if ((addr & 0x60) == 0) { byte a; if (dy==1) a = 0x05; // top of attribute block else if (dy==3) a = 0x50; // bottom of attribute block else a = 0x00; // does not intersect attr. block // write entire row of attribute blocks memset(attrs, a, 8); vrambuf_put(nt2attraddr(addr), attrs, 8); } // copy line to screen buffer vrambuf_put(addr, buf, COLS); // create actors on this floor, if needed // TODO: maybe this happens too early? if (dy == 0 && (floor >= 2)) { create_actors_on_floor(floor); } break; } } } // draw entire stage at current scroll position // filling up entire name table void draw_entire_stage() { byte y; for (y=0; y scroll_pixel_yy) draw_floor_line(scroll_tile_y+30); else draw_floor_line(scroll_tile_y-30); } // set scroll variables scroll_pixel_yy = yy; scroll_tile_y = yy >> 3; // divide by 8 // set scroll registers scroll(0, 479 - ((yy + 224) % 480)); } // redraw a floor when object picked up void refresh_floor(byte floor) { byte y = floors[floor].ypos; draw_floor_line(y+2); draw_floor_line(y+3); } ///// ACTORS typedef enum ActorState { INACTIVE, STANDING, WALKING, CLIMBING, JUMPING, FALLING, PACING }; typedef enum ActorType { ACTOR_PLAYER, ACTOR_ENEMY, ACTOR_RESCUE }; typedef struct Actor { word yy; // Y position in pixels (16 bit) byte x; // X position in pixels (8 bit) byte floor; // floor index byte state; // ActorState int name:2; // ActorType (2 bits) int pal:2; // palette color (2 bits) int dir:1; // direction (0=right, 1=left) int onscreen:1; // is actor onscreen? sbyte yvel; // Y velocity (when jumping) sbyte xvel; // X velocity (when jumping) } Actor; Actor actors[MAX_ACTORS]; // all actors // creete actors on floor_index, if slot is empty void create_actors_on_floor(byte floor_index) { byte actor_index = (floor_index % (MAX_ACTORS-1)) + 1; struct Actor* a = &actors[actor_index]; if (!a->onscreen) { Floor *floor = &floors[floor_index]; a->state = STANDING; a->name = ACTOR_ENEMY; a->x = rand8(); a->yy = get_floor_yy(floor_index); a->floor = floor_index; a->onscreen = 1; // rescue person on top of the building if (floor_index == MAX_FLOORS-1) { a->name = ACTOR_RESCUE; a->state = PACING; a->x = 0; a->pal = 1; } } } void draw_actor(byte i) { struct Actor* a = &actors[i]; bool dir; const unsigned char* meta; byte x,y; // sprite variables // get screen Y position of actor int screen_y = SCREEN_Y_BOTTOM - a->yy + scroll_pixel_yy; // is it offscreen? if (screen_y > 192+8 || screen_y < -18) { a->onscreen = 0; return; // offscreen vertically } dir = a->dir; switch (a->state) { case INACTIVE: a->onscreen = 0; return; // inactive, offscreen case STANDING: meta = dir ? playerLStand : playerRStand; break; case WALKING: meta = playerRunSeq[((a->x >> 1) & 7) + (dir?0:8)]; break; case JUMPING: meta = dir ? playerLJump : playerRJump; break; case FALLING: meta = dir ? playerLSad : playerRSad; break; case CLIMBING: meta = (a->yy & 4) ? playerLClimb : playerRClimb; break; case PACING: meta = personToSave; break; } // set sprite values, draw sprite x = a->x; y = screen_y; oam_meta_spr_pal(x, y, a->pal, meta); // is this actor 0? (player sprite) if (i == 0) { player_screen_y = y; // save last screen Y position } a->onscreen = 1; // if we drew the actor, consider it onscreen return; } // draw the scoreboard, right now just two digits void draw_scoreboard() { oam_off = oam_spr(24+0, 24, '0'+(score >> 4), 2, oam_off); oam_off = oam_spr(24+8, 24, '0'+(score & 0xf), 2, oam_off); } // draw all sprites void refresh_sprites() { byte i; // reset sprite index to 0 oam_off = 0; // draw all actors for (i=0; i= MAX_FLOORS) return 0; x = is_ladder_close(player_x, floor->ladder1); if (x) return x; x = is_ladder_close(player_x, floor->ladder2); if (x) return x; return 0; } // put the player on the ladder, and move up or down (floor_adjust) byte mount_ladder(Actor* player, signed char floor_adjust) { byte x = get_closest_ladder(player->x, player->floor + floor_adjust); if (x) { player->x = x + 8; player->state = CLIMBING; player->floor += floor_adjust; return 1; } else return 0; } // should we scroll the screen upward? void check_scroll_up() { if (player_screen_y < ACTOR_SCROLL_UP_Y) { set_scroll_pixel_yy(scroll_pixel_yy + 1); } } // should we scroll the screen downward? void check_scroll_down() { if (player_screen_y > ACTOR_SCROLL_DOWN_Y && scroll_pixel_yy > 0) { set_scroll_pixel_yy(scroll_pixel_yy - 1); } } // actor falls down a floor void fall_down(struct Actor* actor) { actor->floor--; actor->state = FALLING; actor->xvel = 0; actor->yvel = 0; } // move an actor (player or enemies) // joystick - game controller mask // scroll - if true, we should scroll screen (is player) void move_actor(struct Actor* actor, byte joystick, bool scroll) { switch (actor->state) { case STANDING: case WALKING: // left/right has priority over climbing if (joystick & PAD_A) { actor->state = JUMPING; actor->xvel = 0; actor->yvel = JUMP_VELOCITY; if (joystick & PAD_LEFT) actor->xvel = -1; if (joystick & PAD_RIGHT) actor->xvel = 1; // play sound for player if (scroll) sfx_play(SND_JUMP,0); } else if (joystick & PAD_LEFT) { actor->x--; actor->dir = 1; actor->state = WALKING; } else if (joystick & PAD_RIGHT) { actor->x++; actor->dir = 0; actor->state = WALKING; } else if (joystick & PAD_UP) { mount_ladder(actor, 0); // state -> CLIMBING } else if (joystick & PAD_DOWN) { mount_ladder(actor, -1); // state -> CLIMBING, floor -= 1 } else { actor->state = STANDING; } if (scroll) { check_scroll_up(); check_scroll_down(); } break; case CLIMBING: if (joystick & PAD_UP) { if (actor->yy >= get_ceiling_yy(actor->floor)) { actor->floor++; actor->state = STANDING; } else { actor->yy++; } } else if (joystick & PAD_DOWN) { if (actor->yy <= get_floor_yy(actor->floor)) { actor->state = STANDING; } else { actor->yy--; } } if (scroll) { check_scroll_up(); check_scroll_down(); } break; case FALLING: if (scroll) { check_scroll_up(); check_scroll_down(); } case JUMPING: actor->x += actor->xvel; actor->yy += actor->yvel/4; actor->yvel -= 1; if (actor->yy <= get_floor_yy(actor->floor)) { actor->yy = get_floor_yy(actor->floor); actor->state = STANDING; } break; } // don't allow player to travel past left/right edges of screen if (actor->x > ACTOR_MAX_X) actor->x = ACTOR_MAX_X; // we wrapped around right edge if (actor->x < ACTOR_MIN_X) actor->x = ACTOR_MIN_X; // if player lands in a gap, they fall (switch to JUMPING state) if (actor->state <= WALKING && is_in_gap(actor->x, floors[actor->floor].gap)) { fall_down(actor); } } // should we pickup an object? only player does this void pickup_object(Actor* actor) { Floor* floor = &floors[actor->floor]; byte objtype = floor->objtype; // only pick up if there's an object, and if we're walking or standing if (objtype && actor->state <= WALKING) { byte objx = floor->objpos * 16; // is the actor close to the object? if (actor->x >= objx && actor->x < objx+16) { // clear the item from the floor and redraw floor->objtype = 0; refresh_floor(actor->floor); // did we hit a mine? if (objtype == ITEM_MINE) { // we hit a mine, fall down fall_down(actor); sfx_play(SND_HIT,0); vbright = 8; // flash } else { // we picked up an object, add to score score = bcd_add(score, 1); sfx_play(SND_COIN,0); } } } } // read joystick 0 and move the player void move_player() { byte joy = pad_poll(0); move_actor(&actors[0], joy, true); pickup_object(&actors[0]); } // returns absolute value of x byte iabs(int x) { return x >= 0 ? x : -x; } // check to see if actor collides with any non-player actor bool check_collision(Actor* a) { byte i; byte afloor = a->floor; // can't fall through basement if (afloor == 0) return false; // can't fall if already falling if (a->state == FALLING) return false; // iterate through entire list of actors for (i=1; ionscreen && afloor == b->floor && iabs(a->yy - b->yy) < 8 && iabs(a->x - b->x) < 8) { return true; } } return false; } /// const char* RESCUE_TEXT = "Is this a rescue?\n" "I am just hanging out\n" "on top of this building.\n" "Get lost!!!"; // draw a message on the screen void type_message(const char* charptr) { char ch; byte x,y; x = 2; // compute message y position relative to scroll y = ROWS*3 + 39 - scroll_tile_y; // repeat until end of string (0) is read while ((ch = *charptr++)) { while (y >= 60) y -= 60; // compute (y % 60) // newline character? go to start of next line if (ch == '\n') { x = 2; y++; } else { // put character into nametable vrambuf_put(getntaddr(x, y), &ch, 1); x++; } // typewriter sound sfx_play(SND_HIT,0); // flush buffer and wait a few frames vrambuf_flush(); delay(5); } } // reward scene when player reaches roof void rescue_scene() { // make player face to the left actors[0].dir = 1; actors[0].state = STANDING; refresh_sprites(); music_stop(); type_message(RESCUE_TEXT); // wait 2 seconds delay(100); } // game loop void play_scene() { byte i; // initialize actors array memset(actors, 0, sizeof(actors)); actors[0].state = STANDING; actors[0].name = ACTOR_PLAYER; actors[0].pal = 3; actors[0].x = 64; actors[0].floor = 0; actors[0].yy = get_floor_yy(0); // put actor at bottom set_scroll_pixel_yy(0); // draw initial view of level into nametable draw_entire_stage(); // repeat until player reaches the roof while (actors[0].floor != MAX_FLOORS-1) { // flush VRAM buffer (waits next frame) vrambuf_flush(); refresh_sprites(); move_player(); // move all the actors for (i=1; i 4) { pal_bright(--vbright); } } // player reached goal; reward scene rescue_scene(); } /*{pal:"nes",layout:"nes"}*/ const char PALETTE[32] = { 0x03, // background color 0x11,0x30,0x27, 0x0, // ladders and pickups 0x1c,0x20,0x2c, 0x0, // floor blocks 0x00,0x10,0x20, 0x0, 0x06,0x16,0x26, 0x0, 0x16,0x35,0x24, 0x0, // enemy sprites 0x00,0x37,0x25, 0x0, // rescue person 0x0d,0x2d,0x3a, 0x0, 0x0d,0x27,0x2a // player sprites }; // set up PPU void setup_graphics() { ppu_off(); oam_clear(); pal_all(PALETTE); vram_adr(0x2000); vram_fill(CH_BLANK, 0x1000); vrambuf_clear(); set_vram_update(updbuf); ppu_on_all(); } // set up famitone library void setup_sounds() { famitone_init(danger_streets_music_data); sfx_init(demo_sounds); nmi_set_callback(famitone_update); } // main program void main() { setup_sounds(); // init famitone library while (1) { setup_graphics(); // setup PPU, clear screen sfx_play(SND_START,0); // play starting sound make_floors(); // make random level music_play(0); // start the music play_scene(); // play the level } }