Jump to content

bsteux

+AtariAge Subscriber
  • Posts

    84
  • Joined

  • Last visited

Everything posted by bsteux

  1. Hi, So here's the sequel to the previous example: bombjack with the bombjack character. I've used the sprites provided by @TIX, which I scaled down to 16 pixels high (with mediocre success) and colorized to 160B. Well, it would need rework, but it's not the main goai. I think I mixed up jumping and dancing sprites... This example shows: - How a rather complicated character state machine can be implemented in C - The use of the new function multisprite_sparse_tiling_collision to detect collisions between bombjack and the platforms. - The use of 16 bits arithmetics (bombjack_ypos is a 16 bit "unsigned short" variable, due to the vertical acceleration that is applied to the character when jumping), and in particular the use of >>8 and <<8 cc7800 optimizations to go from 16 bits to 8 bits and vice versa. - The use of a new feature call overlay. In cc7800 multisprite.h, as in 7800basic, there is the notion of background with, the "multisprite_save" function. I've added another layer, called "overlay", that can also be saved for the next frame. This overlay contains the bombs. It's not static as a background, but it's not as moving as foreground sprites. It's in between, and we don't want to spend time redrawing them every frame, like a background. This is why I've introduced 2 new functions: multisprite_clear_overlay() and multisprite_save_overlay(), which are used when bombjack grabs a bomb. Note that the bombs redraw must be called twice (update_overlay=2 in the code), because multisprite.h is always using double buffering (it's not an option). Note that this feature allows to reduce significantly the use of cpu (down to 10% approx), since we don't have to redraw the bombs every frame. Here is the example source code (example_bombjack2.c in the cc7800 github repository) : #include "prosystem.h" #define MULTISPRITE_OVERLAY #include "multisprite.h" #include "joystick.h" #include "string.h" // Generated by sprites7800 bombjack_tiles.yaml #include "example_bombjack_tiles.c" // Generated by sprites7800 bombjack_sprites.yaml #include "example_bombjack_sprites.c" // Generated by tiles7800 --sparse bombjack_tiles.yaml --varname tilemap_arrangement_A bombjack_arrangement_A.tmx #include "example_bombjack_arrangement_A.c" // Generated by tiles7800 --sparse bombjack_tiles.yaml --varname tilemap_sphinx bombjack_sphinx.tmx #include "example_bombjack_sphinx.c" const char topborder[15] = {26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28}; const char topborder_dl[] = { topborder, 0x60, topborder >> 8, (3 << 5) | (-15 & 0x1f), 0, 0, 0}; const char bottomborder[15] = {30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32}; const char bottomborder_dl[] = { bottomborder, 0x60, bottomborder >> 8, (3 << 5) | (-15 & 0x1f), 0, 0, 0}; #define BOMBS_RIGHT 0 #define BOMBS_UP 1 #define BOMBS_LEFT 2 #define BOMBS_DOWN 3 // Arrangements - x, y of first bomb, nb bombs, direction (0 to 3). const char arrangement_A[] = { 6 + 6 * 8, 3 * 16 - 8, 4, BOMBS_RIGHT, 4 + 13 * 8, 8 + 4 * 16, 4, BOMBS_DOWN, 4, 5 * 16, 4, BOMBS_DOWN, 4 + 2 * 8, 0, 3, BOMBS_RIGHT, 4 + 11 * 8, 11 * 16, 3, BOMBS_LEFT, 5 * 8, 13 * 16, 3, BOMBS_LEFT, 4 + 13 * 8, 0, 3, BOMBS_LEFT}; const char arrangements_nb_series[] = { 7 }; const char *arrangements[] = { arrangement_A }; const signed char arrangement_dx[4] = { 12, 0, -12, 0 }; const signed char arrangement_dy[4] = { 0, -24, 0, 24 }; ramchip char bomb_disposed[24]; ramchip char lighted_bomb; ramchip char explosion_color1, explosion_color2, explosion_color3; ramchip int score; ramchip char update_score; ramchip char display_score_str[5]; #define SPRITES_NB_MAX 24 #define SPRITE_TWO_HUNDRED 0 ramchip char sprite_xpos[SPRITES_NB_MAX], sprite_ypos[SPRITES_NB_MAX], sprite_type[SPRITES_NB_MAX], sprite_state[SPRITES_NB_MAX], sprite_counter1[SPRITES_NB_MAX], sprite_counter2[SPRITES_NB_MAX]; ramchip char sprite_first, sprite_last; void destroy_sprite() { sprite_type[X] = -1; // Removed do { X++; if (X == SPRITES_NB_MAX) X = 0; } while (X != sprite_last && sprite_type[X] == -1); sprite_first = X; } void draw_sprites() { char i, x, y; for (i = sprite_first; i != sprite_last; i++) { if (i == SPRITES_NB_MAX) { i = 0; if (sprite_last == 0) break; } X = i; if (sprite_type[X] != -1) { if (sprite_type[X] == SPRITE_TWO_HUNDRED) { sprite_counter1[X]--; if (sprite_counter1[X] == 0) { destroy_sprite(); } else { x = sprite_xpos[X]; y = sprite_ypos[X]; multisprite_display_sprite_ex(x, y, twohundred, 3, 2, 0); } } } } } void display_score_update() { char display_score_ascii[6]; itoa(score, display_score_ascii, 10); Y = strlen(display_score_ascii); for (X = 0; X != 5 - Y; X++) { display_score_str[X] = 0; // '0' } X = 4; do { display_score_str[X--] = ((display_score_ascii[--Y] - '0') << 1); } while (Y); } void display_arrangement(char a) { char *arrangement = arrangements[X = a]; char i, j, k, nb_series = arrangements_nb_series[X]; for (Y = 0, k = 0, i = 0; i != nb_series; i++) { char x = arrangement[Y++]; char y = arrangement[Y++]; char nb_bombs = arrangement[Y++]; char direction = arrangement[Y++]; _save_y = Y; for (j = 0; j != nb_bombs; k++, j++) { if (!bomb_disposed[X = k]) { multisprite_display_sprite_fast(x, y, bomb, 2, 2); if (k == lighted_bomb) { multisprite_display_sprite_fast(x, y, explosion, 2, 7); } } x += arrangement_dx[X = direction]; y += arrangement_dy[X]; } Y = _save_y; } } // All the bombjack states #define BOMBJACK_LEFT 64 #define BOMBJACK_RIGHT 128 #define BOMBJACK_STILL 1 #define BOMBJACK_STILL_LEFT (BOMBJACK_STILL | BOMBJACK_LEFT) #define BOMBJACK_STILL_RIGHT (BOMBJACK_STILL | BOMBJACK_RIGHT) #define BOMBJACK_JUMPING 2 #define BOMBJACK_JUMPING_LEFT (BOMBJACK_JUMPING | BOMBJACK_LEFT) #define BOMBJACK_JUMPING_RIGHT (BOMBJACK_JUMPING | BOMBJACK_RIGHT) #define BOMBJACK_WALKING 4 #define BOMBJACK_WALKING_LEFT (BOMBJACK_WALKING | BOMBJACK_LEFT) #define BOMBJACK_WALKING_RIGHT (BOMBJACK_WALKING | BOMBJACK_RIGHT) #define BOMBJACK_FALLING 8 #define BOMBJACK_FALLING_LEFT (BOMBJACK_FALLING | BOMBJACK_LEFT) #define BOMBJACK_FALLING_RIGHT (BOMBJACK_FALLING | BOMBJACK_RIGHT) #define BORDER_WIDTH 4 #define PLAYFIELD_HEIGHT 224 #define PLAYFIELD_WIDTH 112 #define SPRITE_HEIGHT 16 #define SPRITE_WIDTH 12 #define PLATFORM_HEIGHT 8 #define GRAVITY 20 ramchip char bombjack_xpos, bombjack_state, bombjack_counter; ramchip char button_pressed, current_arrangement; // ypos and yspeed are 16-bits to allow for fine vertical movement ramchip unsigned short bombjack_yspeed, bombjack_ypos; ramchip char update_overlay; void bombjack_move_left() { char x, top, collision; bombjack_xpos--; if (bombjack_xpos < 3) bombjack_xpos = 3; else { // Test if we bump into a platform with the head x = (bombjack_xpos - BORDER_WIDTH + 1) >> 3; top = (bombjack_ypos >> 8) >> 4; collision = multisprite_sparse_tiling_collision(top, x, x); if (collision != -1 && ((bombjack_ypos >> 8) & 0x0f) < PLATFORM_HEIGHT - 1) { bombjack_xpos++; return; } // Test if we bump into a platform with the foot top = ((bombjack_ypos >> 8) + SPRITE_HEIGHT - 1) >> 4; collision = multisprite_sparse_tiling_collision(top, x, x); if (collision != -1) { bombjack_xpos++; } } } void bombjack_move_right() { char x, top, collision; bombjack_xpos++; if (bombjack_xpos >= PLAYFIELD_WIDTH - SPRITE_WIDTH + 6) bombjack_xpos = PLAYFIELD_WIDTH - SPRITE_WIDTH + 5; else { // Test if we bump into a platform with the head x = (bombjack_xpos - BORDER_WIDTH + SPRITE_WIDTH - 3) >> 3; top = (bombjack_ypos >> 8) >> 4; collision = multisprite_sparse_tiling_collision(top, x, x); if (collision != -1 && ((bombjack_ypos >> 8) & 0x0f) < PLATFORM_HEIGHT - 1) { bombjack_xpos--; return; } // Test if we bump into a platform with the foot top = ((bombjack_ypos >> 8) + SPRITE_HEIGHT - 1) >> 4; collision = multisprite_sparse_tiling_collision(top, x, x); if (collision != -1) { bombjack_xpos--; } } } void bombjack_dispose_bombs(char a) { char *arrangement = arrangements[X = a]; char x2 = bombjack_xpos + 2; char y2 = (bombjack_ypos >> 8) + 1; char i, j, k, nb_series = arrangements_nb_series[X]; for (Y = 0, k = 0, i = 0; i != nb_series; i++) { char x = arrangement[Y++] + 1; char y = arrangement[Y++] + 6; char nb_bombs = arrangement[Y++]; char direction = arrangement[Y++]; for (j = 0; j != nb_bombs; k++, j++) { if (!bomb_disposed[X = k]) { multisprite_compute_box_collision(x, y, 5, 9, x2, y2, 7, 14); if (multisprite_collision_detected) { char add, find_next_lighted_bomb = 0; bomb_disposed[X] = 1; if (X == lighted_bomb) { find_next_lighted_bomb = 1; add = 200; // Display 200 points X = sprite_last++; if (sprite_last == SPRITES_NB_MAX) sprite_last = 0; if (sprite_last != sprite_first) { sprite_xpos[X] = x - 3; sprite_ypos[X] = y - 6; sprite_counter1[X] = 30; // Stay for half a second } else sprite_last = X; } else { add = 100; if (lighted_bomb == -1) { lighted_bomb = X; find_next_lighted_bomb = 1; } } score += add; update_score = 1; // Find next lighted_bomb if (find_next_lighted_bomb) { lighted_bomb++; while (lighted_bomb < 24 && bomb_disposed[X = lighted_bomb]) { lighted_bomb++; } if (lighted_bomb == 24) { lighted_bomb = 0; while (lighted_bomb < 24 && bomb_disposed[X = lighted_bomb]) { lighted_bomb++; } if (lighted_bomb == 24) lighted_bomb = -1; } } update_overlay = 2; // In order to trigger the update of the overlay (bombs display) i = nb_series - 1; // To get out of upper loop break; } } x += arrangement_dx[X = direction]; y += arrangement_dy[X]; } } } void bombjack() { char *gfx, y; char left, right, top, collision; if (bombjack_state & BOMBJACK_FALLING) { if (bombjack_state & BOMBJACK_LEFT) { gfx = bombjack_falling_left; bombjack_move_left(); } else if (bombjack_state & BOMBJACK_RIGHT) { gfx = bombjack_falling_right; bombjack_move_right(); } else { if (bombjack_yspeed == 0) { gfx = bombjack_jumping; } else { gfx = bombjack_falling; } } bombjack_yspeed += GRAVITY; bombjack_ypos += bombjack_yspeed; // Does it land on bottom of the screen ? if (bombjack_ypos >> 8 >= PLAYFIELD_HEIGHT - 16) { bombjack_ypos = (PLAYFIELD_HEIGHT - 16) << 8; bombjack_state = BOMBJACK_STILL; } else { // Does it land on a platform ? left = (bombjack_xpos + (4 - BORDER_WIDTH)) >> 3; right = (bombjack_xpos + (7 - BORDER_WIDTH)) >> 3; top = ((bombjack_ypos >> 8) >> 4) + 1; collision = multisprite_sparse_tiling_collision(top, left, right); if (collision != -1) { y = (top - 1) << 4; bombjack_ypos = y << 8; bombjack_state = BOMBJACK_STILL; } } } else if (bombjack_state & BOMBJACK_JUMPING) { if (bombjack_state & BOMBJACK_LEFT) { gfx = bombjack_jumping_left; bombjack_move_left(); } else if (bombjack_state & BOMBJACK_RIGHT) { gfx = bombjack_jumping_right; bombjack_move_right(); } else gfx = bombjack_jumping; if (bombjack_yspeed < GRAVITY) { bombjack_yspeed = 0; bombjack_state = BOMBJACK_FALLING; } else { bombjack_yspeed -= GRAVITY; // Does it bump into the ceiling ? if (bombjack_ypos < bombjack_yspeed) { bombjack_ypos = 0; bombjack_yspeed = 0; bombjack_state = BOMBJACK_FALLING; } else { bombjack_ypos -= bombjack_yspeed; // Does it bump into a platform ? left = (bombjack_xpos + (4 - BORDER_WIDTH)) >> 3; right = (bombjack_xpos + (7 - BORDER_WIDTH)) >> 3; top = (((bombjack_ypos >> 8) + 7 ) >> 4); collision = multisprite_sparse_tiling_collision(top, left, right); if (collision != -1) { y = (top << 4) + PLATFORM_HEIGHT; // Head below the platform bombjack_ypos = y << 8; // Turn to 16 bits bombjack_state = BOMBJACK_FALLING; bombjack_yspeed = 0; } } } } else { bombjack_yspeed = 0; } if (bombjack_state & BOMBJACK_STILL) { if (bombjack_state == BOMBJACK_STILL_LEFT) { gfx = bombjack_still_left; } else if (bombjack_state == BOMBJACK_STILL_RIGHT) { gfx = bombjack_still_right; } else { gfx = bombjack_still; } } else if (bombjack_state & BOMBJACK_WALKING) { bombjack_counter++; if (bombjack_counter == 8) bombjack_counter = 0; y = bombjack_counter >> 1; // Walking animation if (bombjack_state & BOMBJACK_LEFT) { if (y == 0 || y == 2) { gfx = bombjack_walking_left1; } else if (y == 1) { gfx = bombjack_walking_left2; } else { gfx = bombjack_walking_left3; } bombjack_move_left(); } else { if (y == 0 || y == 2) { gfx = bombjack_walking_right1; } else if (y == 1) { gfx = bombjack_walking_right2; } else { gfx = bombjack_walking_right3; } bombjack_move_right(); } if (bombjack_ypos >> 8 != PLAYFIELD_HEIGHT - 16) { // Does it fall from a platform ? left = (bombjack_xpos + (4 - 4)) >> 3; right = (bombjack_xpos + (7 - 4)) >> 3; top = ((bombjack_ypos >> 8) + 17) >> 4; collision = multisprite_sparse_tiling_collision(top, left, right); if (collision == -1) { bombjack_state = BOMBJACK_FALLING | (bombjack_state & 0xc0); } } } y = bombjack_ypos >> 8; multisprite_display_sprite_ex(bombjack_xpos, y, gfx, 6, 0, 1); bombjack_dispose_bombs(current_arrangement); } void game_init() { for (X = 23; X >= 0; X--) bomb_disposed[X] = 0; bombjack_xpos = (PLAYFIELD_WIDTH - SPRITE_WIDTH) / 2 + BORDER_WIDTH; bombjack_ypos = (PLAYFIELD_WIDTH - 8) << 8; bombjack_yspeed = 0; bombjack_state = BOMBJACK_FALLING; current_arrangement = 0; lighted_bomb = -1; } void joystick_input() { char left_or_right; joystick_update(); if (joystick[0] & JOYSTICK_BUTTON1) { if (!button_pressed) { button_pressed = 1; if (bombjack_state & BOMBJACK_STILL) left_or_right = 0; else left_or_right = bombjack_state & 0xc0; if ((bombjack_state & BOMBJACK_FALLING) || (bombjack_state & BOMBJACK_JUMPING)) { bombjack_state = BOMBJACK_FALLING | left_or_right; bombjack_yspeed = 0; } else { bombjack_state = BOMBJACK_JUMPING | left_or_right; if (!left_or_right && (joystick[0] & JOYSTICK_UP)) { bombjack_yspeed = 1500; } else { bombjack_yspeed = 1000; } } return; }; } else button_pressed = 0; if (joystick[0] & JOYSTICK_LEFT) { if (bombjack_state & BOMBJACK_STILL) { bombjack_state = BOMBJACK_WALKING_LEFT; bombjack_counter = 0; } else if (bombjack_state & BOMBJACK_FALLING) { bombjack_state = BOMBJACK_FALLING_LEFT; } else if (bombjack_state & BOMBJACK_JUMPING) { bombjack_state = BOMBJACK_JUMPING_LEFT; } else if (bombjack_state != BOMBJACK_WALKING_LEFT) bombjack_state = BOMBJACK_STILL; } else if (joystick[0] & JOYSTICK_RIGHT) { if (bombjack_state & BOMBJACK_STILL) { bombjack_state = BOMBJACK_WALKING_RIGHT; bombjack_counter = 0; } else if (bombjack_state & BOMBJACK_FALLING) { bombjack_state = BOMBJACK_FALLING_RIGHT; } else if (bombjack_state & BOMBJACK_JUMPING) { bombjack_state = BOMBJACK_JUMPING_RIGHT; } else if (bombjack_state != BOMBJACK_WALKING_RIGHT) bombjack_state = BOMBJACK_STILL; } else { if (bombjack_state & BOMBJACK_WALKING) { bombjack_state = BOMBJACK_STILL | (bombjack_state & 0xc0); } else { bombjack_state &= ~(BOMBJACK_LEFT | BOMBJACK_RIGHT); } } } void display_init() { char y; multisprite_init(); multisprite_set_charbase(digits); joystick_init(); // Top border in overscan if (_ms_pal_detected) { X = 4; } else { X = 1; } _ms_b0_dll[X] = topborder_dl >> 8; _ms_b1_dll[X] = topborder_dl >> 8; _ms_b0_dll[++X] = topborder_dl; _ms_b1_dll[X] = topborder_dl; // Bottom border in overscan X += (_MS_DLL_ARRAY_SIZE - 1) * 3 + 1; _ms_b0_dll[X] = 0x08; // 9 lines _ms_b1_dll[X] = 0x08; // 9 lines _ms_b0_dll[++X] = bottomborder_dl >> 8; _ms_b1_dll[X] = bottomborder_dl >> 8; _ms_b0_dll[++X] = bottomborder_dl; _ms_b1_dll[X] = bottomborder_dl; // Adjust the bottom of screen to fit the screen size X += 4; if (_ms_pal_detected) { _ms_b0_dll[X] = 0x07; // 8 lines _ms_b1_dll[X] = 0x07; // 8 lines } else { _ms_b0_dll[X] = 0x00; // 1 line _ms_b1_dll[X] = 0x00; // 1 line } // Display the sphinx and platforms multisprite_sparse_tiling(tilemap_sphinx_data_ptrs, 0, 4, 14); multisprite_sparse_tiling(tilemap_arrangement_A_data_ptrs, 0, 4, 14); // Left and right borders for (y = 0; y < PLAYFIELD_HEIGHT; y += 16) { multisprite_display_sprite_fast(0, y, border, 1, 3); multisprite_display_sprite_fast(116, y, border, 1, 3); } multisprite_display_sprite_fast(121, 0, sideone, 8, 0); // Score display score = 0; update_score = 0; display_score_update(); multisprite_display_tiles(120, 1, display_score_str, 5, 3); // Save the background multisprite_save(); update_overlay = 2; // Enemies palette *P0C1 = 0x04; // Dark gray *P0C2 = 0x08; // Medium gray *P0C3 = 0x34; // Red // Bombjack palette *P1C1 = multisprite_color(0x87); // Light blue *P1C2 = multisprite_color(0x3c); // Rose *P1C3 = 0x00; // Black // Bomb palette *P2C1 = multisprite_color(0x34); // Red *P2C2 = multisprite_color(0x1c); // Yellow *P2C3 = 0x0f; // White // Platform palette *P3C1 = multisprite_color(0x24); // Red *P3C2 = multisprite_color(0x28); // Orange *P3C3 = multisprite_color(0x1c); // Yellow // Blue palette *P4C1 = multisprite_color(0x84); // Dark blue // Sphinx palette *P5C1 = multisprite_color(0x12); // Red *P5C2 = multisprite_color(0x15); // Orange *P5C3 = multisprite_color(0x18); // Yellow // Explosion palette explosion_color1 = multisprite_color(0x24); // Red explosion_color2 = multisprite_color(0x28); // Orange explosion_color3 = multisprite_color(0x1c); // Yellow button_pressed = 0; } void explosion_palette_roll() { char roll = explosion_color3; explosion_color3 = explosion_color2; explosion_color2 = explosion_color1; explosion_color1 = roll; *P7C1 = explosion_color1; *P7C2 = explosion_color2; *P7C3 = explosion_color3; } void main() { game_init(); display_init(); // Main loop do { // Display overlay if (update_overlay != 0) { update_overlay--; multisprite_clear_overlay(); display_arrangement(current_arrangement); multisprite_save_overlay(); } joystick_input(); bombjack(); explosion_palette_roll(); draw_sprites(); if (update_score) { display_score_update(); update_score = 0; } multisprite_flip(); } while(1); } The result is quite funny and not far from the actual arcade game dynamics. I'll stop there with bombjack at the moment (maybe will I go back to this when the cc7800 project will have reached the end), and go for R-Type. Next week, I'll try to show how to use sparse tiling to make a 60fps horizontal scrolling shooter. example_bombjack.mp4 bombjack_sprites.yaml example_bombjack.a78
  2. Thank you for the info provided, very useful. The commented C64 code of Bombjack is quite impressive ! What a job it was to hand-code all this in ASM ! It's also true that it's a very good idea to draw/erase bombs inside a bitmap. I haven't developed a bitmap.h header yet with bit-blitting for cc7800, but it's in my plan, so maybe will I revisit this later. At the moment, the bombjack code here is only a way to show how to use sparse tiling and sprites to make a game with cc7800. It's just a tutorial. It's not meant to be the ideal implementation, but at least a simple one, easy to understand and light.
  3. For bombjack, maybe it would be possible to use a 160B background in immediate mode (using a lot of ROM), but it certainly makes things more complicated. I haven't made the computations, but with Bombjack you always have the situation where you have to draw many sprites on the same line (at least 3 bombs + bombjack + enemies + drawing things on the right of the screen (score, etc)). The game logic is not simple : all the enemies are moving at each step (so you have to redraw all the sprites at every frame), and there can be many enemies at the same time, since the game is spawning, spawning, spawning... so you need some CPU left and it must run at 60fps. So better a 160A background with nice palette switching... The result can be next to 160B if well managed. For Zelda, this is a different case : first, you seldom have 5 sprites in a row at the same time, and second, you don't care if it runs at 60fps or not, so you have much, much more CPU available for your logic. Not the same kind of games...
  4. Here is an example of a simple 7800 cart : http://atarihq.com/danb/7800cart/C025513.shtml /CE (Chip enable) is, as Batari stated, generated from a logic (here a simple 74LS138) using A14 and 15.
  5. Now, let's bombjack a little bit. Why bombjack ? Because it's a nice little example of one-screen game, and it's particularly difficult to adapt to the Atari 7800 : it requires to display a nice background with a lot of sprites overlaid on it - 24 bombs + 24 enemies, so lots of CPU/DMA fight. And as it also requires to be colorful, we'll have to deal with the 160A/B modes. It's also a game that was crappy on most 8-bit platform back in the eighties, especially on the NES which had a "mighty Bombjack" far from the arcade game, with crappy backgrounds ! So let's do better. As a first step, here is an example of how to use sparse tiling (again) to display the famous sphinx and the first level (or arrangement of platforms). In the previous examples, we used sparse tiling for scrolling. But we can also use it for a static screen. We'll introduce a new function: multisprite_sparse_tiling. As in the arcade game, the background will be 14x14 tiles big, i.e. 112x224 fat pixels (224x224 in the arcade game, which had a fabulous 256x256 display). As we need to free as much CPU as possible for the sprites, all the tiles will be in 160A, i.e. 3 colors + transparency. Using a screenshot of the arcade game and bit of GIMP, we build the tiles as follow, reducing the palette to 5 different colors and removing duplicated tiles : And then, with the Tiled editor, we reconstitute the whole sphinx : Using sprites7800 and tiles7800, we build the C code for the graphics. But how can we build a 5 colors background using only 160A tiles ? OK. 1 is black so it's the background. So how 4 colors and not 3 per tile ? By using the "background" feature of tiles7800. For instance, the top of the pyramid is defined in bombjack_tiles.yaml as : - name: sky top: 0 left: 0 height: 16 width: 16 holeydma: false palette: sky palette_number: 4 - name: pyramid_top1 top: 0 left: 48 height: 16 width: 16 holeydma: false background: sky palette: sphinx palette_number: 5 Note that pyramid_top1 tells that tiles7800 should use the sky tile as a background tile. Tiles7800 will then overlay a pyramid_top1 tile over a sky tile, mixing the colors. We are using overlaid 160A tiles. This doesn't cost so much, because the sky tileset using the blue palette is not interrupted. There is just an additional tile that will be overlaid on it. We then define another sparse tiling (very sparse in that case) for the platform : Note the the sphinx tiles use 146 out of 256 tiles (for a 4KB memory bank), i.e. 2.5KB + plus 500 bytes for the sparse tiling arrays = 3KB. Without tile compression, the sphinx would take 14x14x2 bytes = 392 tiles, i.e. 6.2KB of ROM. So here, using sparse tiling, the gain is 50% AND we can use more colors and make a nicer background. To finish the example, we just have to write a little C code to draw everything together. Here is the source code for the example : #include "prosystem.h" #include "multisprite.h" // Generated by sprites7800 bombjack_tiles.yaml #include "example_bombjack_tiles.c" // Generated by sprites7800 bombjack_sprites.yaml #include "example_bombjack_sprites.c" // Generated by tiles7800 --sparse bombjack_tiles.yaml --varname tileset_sphinx bombjack_sphinx.tmx #include "example_bombjack_sphinx.c" // Generated by tiles7800 --sparse bombjack_tiles.yaml --varname tilemap_arrangement_A bombjack_arrangement_A.tmx #include "example_bombjack_arrangement_A.c" const char topborder[15] = {6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8}; const char topborder_dl[] = { topborder, 0x60, topborder >> 8, (3 << 5) | (-15 & 0x1f), 0, 0, 0}; const char bottomborder[15] = {10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12}; const char bottomborder_dl[] = { bottomborder, 0x60, bottomborder >> 8, (3 << 5) | (-15 & 0x1f), 0, 0, 0}; // Arrangements - x, y of first bomb, nb bombs, direction (0 to 3). const char arrangement_A[] = { 6 + 6 * 8, 3 * 16 - 8, 4, 0, 4 + 13 * 8, 8 + 4 * 16, 4, 3, 4, 5 * 16, 4, 3, 4 + 2 * 8, 0, 3, 0, 4 + 11 * 8, 11 * 16, 3, 2, 5 * 8, 13 * 16, 3, 2, 4 + 13 * 8, 0, 3, 2}; const char arrangements_nb_series[] = { 7 }; const char *arrangements[] = { arrangement_A }; const signed char arrangement_dx[4] = { 12, 0, -12, 0 }; const signed char arrangement_dy[4] = { 0, -24, 0, 24 }; void display_arrangement(char a) { char *arrangement = arrangements[X = a]; char i, j, nb_series = arrangements_nb_series[X]; for (Y = 0, i = 0; i != nb_series; i++) { char x = arrangement[Y++]; char y = arrangement[Y++]; char nb_bombs = arrangement[Y++]; char direction = arrangement[Y++]; _save_y = Y; for (j = 0; j != nb_bombs; j++) { multisprite_display_sprite_fast(x, y, bomb, 2, 2); x += arrangement_dx[X = direction]; y += arrangement_dy[X]; } Y = _save_y; } } void main() { char y; multisprite_init(); multisprite_set_charbase(platform); // Top border in overscan if (_ms_pal_detected) { X = 4; } else { X = 1; } _ms_b0_dll[X] = topborder_dl >> 8; _ms_b1_dll[X] = topborder_dl >> 8; _ms_b0_dll[++X] = topborder_dl; _ms_b1_dll[X] = topborder_dl; // Bottom border in overscan X += (_MS_DLL_ARRAY_SIZE - 1) * 3 + 1; _ms_b0_dll[X] = 0x08; // 9 lines _ms_b1_dll[X] = 0x08; // 9 lines _ms_b0_dll[++X] = bottomborder_dl >> 8; _ms_b1_dll[X] = bottomborder_dl >> 8; _ms_b0_dll[++X] = bottomborder_dl; _ms_b1_dll[X] = bottomborder_dl; // Adjust the bottom of screen to fit the screen size X += 4; if (_ms_pal_detected) { _ms_b0_dll[X] = 0x07; // 8 lines _ms_b1_dll[X] = 0x07; // 8 lines } else { _ms_b0_dll[X] = 0x00; // 1 line _ms_b1_dll[X] = 0x00; // 1 line } // Left and right borders for (y = 0; y < 224; y += 16) { multisprite_display_sprite_fast(0, y, border, 1, 3); multisprite_display_sprite_fast(116, y, border, 1, 3); } // Display the sphinx and platforms multisprite_sparse_tiling(tilemap_sphinx_data_ptrs, 0, 4, 14); multisprite_sparse_tiling(tilemap_arrangement_A_data_ptrs, 0, 4, 14); multisprite_save(); *P1C2 = multisprite_color(0x3c); // Rose // Bomb palette *P2C1 = multisprite_color(0x34); // Red *P2C2 = multisprite_color(0x1c); // Yellow *P2C3 = 0x0f; // Fire palette *P3C1 = multisprite_color(0x24); // Red *P3C2 = multisprite_color(0x28); // Orange *P3C3 = multisprite_color(0x1c); // Yellow // Blue palette *P4C1 = multisprite_color(0x84); // Dark blue *P4C2 = multisprite_color(0x87); // Light blue *P4C3 = multisprite_color(0xac); // Turquoise // Sphinx palette *P5C1 = multisprite_color(0x12); // Red *P5C2 = multisprite_color(0x15); // Orange *P5C3 = multisprite_color(0x18); // Yellow // Main loop do { // Display the first arrangement display_arrangement(0); multisprite_flip(); } while(1); } Note the use of the new function multisprite_sparse_tiling, which is used twice (one for the bakground, one for the platforms). We also had to draw top and bottom borders in overscan, since the display in multisprite.h is 224 pixels high (14 * 16 pixels), and we NEED a 240 pixels high display. We have to display two 8 pixels high borders on top and on bottom of it. For this, we directly modify the DLLs (Display List Lists), using the _ms_b0_dll and _ms_b1_dll arrays (see the multisprite_init function code in multisprite.h to better understand the standard cc7800 screen DLLs layout). Here, we have to check that we are in PAL or NTSC, because the number of lines in overscan are different in the 2 TV modes, and we have to modify b0 and b1 (for buffer0 and buffer1), the 2 DLLs that are used for double buffering. Here the resulting screenshot for the whole scenery, which looks quite good for a 160A display : Note the white strip that is generated by the -DDEBUG option, which shows that 1/4 of the available CPU is used by displaying the bombs arrangement every frame (24 sprites to display). It's huge, since with the background AND all the sprites, DMA access will severely limit the CPU available for the game logic. We'll have to find a solution for that. We NEED to run at 60fps for bombjack. Next week, I'll introduce the bombjack character. We'll see how to check collisions with the platforms (sprite vs tiles collisions), and how to manage the bombs display so that they don't take so much CPU. bombjack_sprites.yaml bombjack_tiles.tsx bombjack_sphinx.tmx bombjack_tiles.yaml example_bombjack1.a78
  6. Oops! I completely forgot to clear the screen after a game over, which generates a lot of glitches. Here is a fixed version. example_shmup_pokey.a78 example_shmup_vmem.a78 example_shmup.c
  7. Indeed, my plan is to make a one screen example (bombjack like), and then an horizontal scrolling example (R-Type like), and then a bitmap based example (with 3d projection like The Sentinel), and then go back on this to try to finish a game. Let's see. Maybe will I add spawning and additional weapons to this example first to complete it...
  8. Ah yes. It's from the video memory version I guess no ? I probably don't reset correctly the video memory filling after a gameover when I restart the scrolling. I'll check that. Ok course you are welcome to make any game based on this code. It's the goal of it all ! Good to know that it's working on the Dragonfly without signing. Have you tried the pokey version ?
  9. Please tell me if you can run it on the Dragonfly (I guess you'll have to use 7800sign on the binary). If your french Atari friends are from the Paris area, tell them to get in touch !
  10. Have you played Atari today ? Here is a 60 fps shmup for the Atari 7800 (well, not really, it's just the beginning of it, but well... it runs at full speed !). Nothing really new this week, as this is just an integration of the previous developments in order to check that everything fits in... In the end, it's a rather complete basis for developing a shmup : the code manages the FIFOs for bullets, enemies, explosions, the collisions, the music (Pokey & TIA for sound effects) and the scrolling background. And the gameover (I'm proud of this one with the big skull). It just lacks the enemy spawning (I just spawn a single boss) and the level design (it's YOUR work as game developers). The result is that all the stuff takes approximately 70% of CPU available (in NTSC and 60fps), so there is still room for completing the game. On the other hand, it terms of ROM space, it's already very tight. Indeed, this is the first time I'm using cc7800 to make a true supergame (128KB ROM with bankswitching). The memory layout is the following (each bank is 16kB) : - Bank0 (or default bank) is the one that is fixed at 0xC000 => 0xFFF (in the ROM layout, it's indeed the last one. cc7800 manages this for you). It contains most of the code (including RMTPlayer for the Pokey Music) and 4kb of sprites that will be used throughout the game (spaceship, gameover, the bullets, digits for scoring...). In the end, Bank0 is almost full (there is still ~2kb of ROM available, 4kb if you remove POKEY RTMPlayer support). - Bank1 to Bank6 contain the level definitions and are mapped from 0x8000 to 0xbffff on demand (when you jump into a banked function, there is automatic bankswitching). I've only included the definition for 1 level, but the bank is almost full (4kb for the tiles definition, 4kb for the sparse tileset definition (the actual layout of the background), 4kb for the enemy sprites (here only 1 160B big boss sprite), 2.7Kb for the Pokey music and 1.3kb for the enemy management code). There is no room anymore for the spawning of enemies. Solution ? Reduce the music size - nice music but too fat - and reduce the tileset definition by reusing parts of the layout. - Bank7 contains the initialization code (palettes definitions, multisprite library init, etc), which helps to reduce bank0 usage. A splash screen would perfectly fit there... In attachment, you will find : - The latest version of cc7800, with several bankswitching related bug corrected. - 2 tested ROMS : . one with RAM at 0x4000 (with a vertical scrolling in immediate mode), no pokey and a lot of CPU available . the other with Pokey at 0x4000 (with music, but background drawing in indirect mode). Less CPU available, but not that bad indeed. - The C source code of the Shmup game (let's call it Power Shmup 7800) - The yaml and png to be used with sprites7800 and tiles7800 to generate the graphics code. In order to compile the full thing, type in cc7800 directory (from the github repository) : cc7800 -g -v -Iheaders -Iexamples -DMULTISPRITE_USE_VIDEO_MEMORY examples/example_shmup.c to get the the immediate mode vertical scrolling, and cc7800 -g -v -Iheaders -Iexamples -DPOKEY_MUSIC examples/example_shmup.c to get the one with some Music. The 2 options are not compatible yet, since both of the them use the 0x4000 precious memory bank (Concerto doesn't support 0x450 Pokey at the moment). (the -g flag generates the .asm and .lst files, the -v generates a verbose output with the full memory layout. Very very instructive. You need to understand this if you want to go further). You can also compile with -DDEBUG flag to see the CPU available on screen. Next time ? The use of sparse tiling to draw the background of a single screen game. example_shmup.mp4 cc7800-0.2.15-x86_64.msi example_shmup_pokey.a78 example_shmup_vmem.a78 shmup.yaml example_shmup.c
  11. Hi, Today, a new vertical scrolling setup, very optimized : - It uses sparse tiling, and thus can mix empty spaces and 160A with multiple palettes (i.e. it's a 25 colors background scrolling. Well, in my example, I'm using only 2 palettes out of 8 ) - It uses 12kb of RAM at 0x4000 as video memory, copying on the fly the tiles gfx to RAM, which enables the use of immediate mode of Maria. Tiles are copied step by step, in order to smooth the processing time over frames. - It uses DMA Masking as suggested by @RevEng, adapted to double buffering. - Display lists are rolling over, i.e. there is no copy when scrolling (only when putting data into the scrolling buffer) - As such, it consumes the minimum of CPU / DMA during display, possibly enabling 60fps vertical scrolling games (shmups!) - Without a single line of assembly code because it's cc7800 (but you can take the resulting code in your ASM or 7800Basic program if you want). Here is the source code for the example (examples/example_vertical_scrolling_sparse_vmem.c) : #include "prosystem.h" #define VERTICAL_SCROLLING #define MULTISPRITE_USE_VIDEO_MEMORY #include "multisprite.h" // Generated by sprites7800 shmup_tiles.png #include "example_shmup_tiles.c" // Generated by tiles7800 --sparse shmup_tiles.yaml shump.tmx #include "example_shmup_tilesets.c" void main() { char counter = 255; char done = 0; multisprite_init(); multisprite_vscroll_init_sparse_tiles_vmem(tilemap_data_ptrs, blue_objects1); // Grey palette *P0C1 = 0x04; *P0C2 = 0x08; *P0C3 = 0x0c; // Blue palette *P1C1 = multisprite_color(0x84); // Dark blue *P1C2 = multisprite_color(0x88); // Light blue *P1C3 = multisprite_color(0xac); // Turquoise // Main loop do { multisprite_vscroll_buffer_sparse_tiles_vmem_step(); multisprite_vscroll_buffer_sparse_tiles_vmem_step(); // Prepare scrolling data if (multisprite_vscroll_buffer_empty()) { if (!done) { multisprite_vscroll_buffer_sparse_tiles_vmem(counter); if (counter) counter--; else done = 1; } else if (done == 1) { multisprite_vscroll_buffer_sparse_tiles_vmem(255); done = 2; } else done = 3; } while (*MSTAT & 0x80); multisprite_flip(); if (done != 3) multisprite_vertical_scrolling(2); } while(1); } Note the multisprite_vscroll_buffer_sparse_tiles_vmem_step() function, which is called twice, in order to allow for a faster scrolling (2 pixels by 2 pixels). One step copies 1/16th of the tiles gfx from ROM to RAM, so if you want to scroll 2 lines per 2 lines, and as tiles are 16 pixels high, you have to call it twice per frame to smooth the computing/ROM to RAM transfer over the frames. Next week, big time : a (almost) fully functional shmup example, with scrolling, sound & music, explosions, a big boss and a gameover., in 60fps.. This to check that it can fit in a Atari 7800... The target: Summer Carnival '92: Recca on the NES... without the infamous NES blinking ! example_vertical_scrolling_sparse_vmem.mp4 example_vertical_scrolling_sparse_vmem.a78
  12. Hi, Today is about collision detection and more precisely fast pixel accurate collision detection. Collision detection is something very important in video games, but unfortunately the Atari 7800 doesn't provide any hardware support for it. We'll have to deal with the 6502 for that... I've added 2 macros to headers/multisprite.h : multisprite_compute_box_collision, and multisprite_compute_collision. The first one is a simple macro that (as its name implies) checks for a box collision between 2 objects - the classical way on Atari 7800, fast but not accurate -. The second one is pixel accurate collision detection that relies on a pre-computed collision map between sprites. Example : I want to compute the collision between a bullet and a spaceship. In shmup.yaml, I define : sprite_sheets: - image: shmup.png holeydma: 16 sprites: - name: bullet top: 48 left: 0 height: 8 width: 8 palette: fire - name: spaceship top: 48 left: 8 height: 32 width: 24 mode: 160B palette: all collisions: - sprite1: bullet sprite2: spaceship Note the new collisions keyword, that will make sprites7800 generate the collision map between the 2 sprites. When we execute sprites7800 shmup.yaml, it generates a collision map, which is a simple C array : const char collision_bullet_spaceship[78] = {0x03, 0x80, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x0f, 0xe0, 0x0f, 0xe0, 0x0f, 0xe0, 0x3f, 0xf8, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0x7f, 0xfc, 0x3f, 0xf8, 0x3f, 0xf8, 0x1b, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; We can use it in our game code to detect a collision by : multisprite_compute_collision(xbullet, ybullet, 4, 8, spaceship_x, spaceship_y, 12, 24, collision_bullet_spaceship); if (multisprite_collision_detected) { // Explosion } where we provide the position of the 2 possibly colliding objects, their size and the collision map computed by sprites7800. The macro is defined in headers/multisprite.h as : // ~100 cycles max pixel accurate collision detection (60us) #define multisprite_compute_collision(x1, y1, w1, h1, x2, y2, w2, h2, collision_map) {\ _ms_tmp3 = 0; \ _ms_tmp2 = (y1) + ((h1) - 1) - (y2); \ if (_ms_tmp2 >= 0) { \ if ((x1) <= (x2) + ((w2) - 1)) { \ if ((y1) <= (y2) + ((h2) - 1)) { \ _ms_tmp = (x1) + ((w1) - 1) - (x2); \ if (_ms_tmp >= 0) { \ Y = _ms_tmp2 << ((w1 + w2 - 1) / 8); \ while (_ms_tmp >= 8) { \ Y++; \ _ms_tmp -= 8; \ } \ _ms_tmp3 = collision_map[Y] & _ms_bit_extract[X = _ms_tmp]; \ } \ } \ } \ } \ } It's basically 4 if/then (box collision test), followed by a bit test on the collision map. This generates the following assembler code in our example : ; { _ms_tmp3 = 0; _libc_tmp2 = ( ybullet) + (( 8) - 1) - ( spaceship_y); if (_libc_tmp2 >= 0) { if ((xbullet) <= ( spaceship_x) + (( 12) - 1)) { if (( ybullet) <= ( spaceship_y) + (( 24) - 1)) { _libc_tmp = (xbullet) + (( 4) - 1) - ( spaceship_x); if (_libc... LDA #0 ; 2 STA _ms_tmp3 ; 3 LDA main_3_ybullet ; 3 CLC ; 2 ADC #7 ; 2 SEC ; 2 SBC spaceship_y ; 4 STA _libc_tmp2 ; 3 BCC .ifend98 ; 2/3 LDA spaceship_x ; 4 CLC ; 2 ADC #11 ; 2 CMP main_3_xbullet ; 3 BCC .ifend99 ; 2/3 LDA spaceship_y ; 4 CLC ; 2 ADC #23 ; 2 CMP main_3_ybullet ; 3 BCC .ifend100 ; 2/3 LDA main_3_xbullet ; 3 CLC ; 2 ADC #3 ; 2 SEC ; 2 SBC spaceship_x ; 4 STA _libc_tmp ; 3 BCC .ifend101 ; 2/3 LDA _libc_tmp2 ; 3 ASL ; 2 TAY ; 2 .while12 LDA _libc_tmp ; 3 CMP #8 ; 2 BCC .whileend12 ; 2/3 INY ; 2 LDA _libc_tmp ; 3 SEC ; 2 SBC #8 ; 2 STA _libc_tmp ; 3 JMP .while12 ; 3 .whileend12 LDX _libc_tmp ; 3 LDA collision_bullet_spaceship,Y ; 4/5 AND _ms_bit_extract,X ; 4/5 STA _ms_tmp3 ; 3 .ifend101 .ifend100 .ifend99 .ifend98 which is not much more than simple box collision. Its max execution time is 60us, but most of the time it will be less (we start by testing the y position of the 2 objects, and most of the time the bullets are above the spaceship). Note that we have defined a macro and not used a C function in order to benefit from constant parameter optimization (as width and height are constants, the code is simpler than when using variables). Here is the source code for example_collisions, which is almost a game (try to survive this 32 sprites assault) : #include "prosystem.h" #include "multisprite.h" #include "joystick.h" const signed short dx[24] = {300, 289, 259, 212, 149, 77, 0, -77, -150, -212, -259, -289, -300, -289, -259, -212, -149, -77, 0, 77, 149, 212, 259, 289}; const signed short dy[24] = {0, 124, 240, 339, 415, 463, 480, 463, 415, 339, 240, 124, 0, -124, -239, -339, -415, -463, -480, -463, -415, -339, -240, -124}; const char horizontal_pingpong[24] = { 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13 }; const char vertical_pingpong[24] = { 0, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }; // Generated by sprites7800 shmup.yaml holeydma reversed scattered(16,6) char explosion1[96] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x68, 0x00, 0x00, 0x00, 0x62, 0x96, 0xaa, 0xa8, 0x00, ... 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; holeydma reversed scattered(16,1) char bullet[16] = { 0x18, 0x96, 0x7a, 0x7e, 0x7e, 0x6e, 0x9a, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; holeydma reversed scattered(16,6) char spaceship[96] = { 0x00, 0x00, 0x30, 0x80, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x00, 0x00, 0x00, 0x30, 0x80, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x00, 0x00, 0x00, 0x30, 0x80, 0x00, 0x00, 0x00, 0x00, 0xe1, 0x94, 0x00, 0x00, 0x00, 0x00, 0xa1, 0x94, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x94, 0x00, 0x00, 0x00, 0x00, 0xb9, 0xd6, 0x00, 0x00, 0x10, 0x00, 0xba, 0xd6, 0x00, 0x40, 0x13, 0x30, 0xba, 0xd6, 0x80, 0x4c, 0x20, 0x30, 0xa9, 0x96, 0x80, 0x80, 0x20, 0xd2, 0xa9, 0x96, 0x68, 0x80, 0x20, 0xd2, 0xa9, 0x96, 0x68, 0x80, 0x00, 0x5a, 0xaa, 0x5a, 0x5a, 0x00, 0x32, 0x5a, 0xaa, 0x5a, 0x5a, 0x80 }; holeydma reversed scattered(16,6) char spaceship_1[96] = { 0xda, 0x5a, 0x66, 0x59, 0x5a, 0x68, 0xda, 0x9e, 0x6a, 0x5a, 0x6b, 0x68, 0xda, 0x9a, 0x68, 0x52, 0x6a, 0x68, 0xda, 0x9a, 0xa0, 0x50, 0x6a, 0x68, 0xfb, 0x98, 0xe0, 0x60, 0x62, 0xec, 0x00, 0x98, 0x30, 0x80, 0x62, 0x00, 0x00, 0x98, 0x20, 0x40, 0x62, 0x00, 0x00, 0x48, 0x20, 0x40, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; holeydma reversed scattered(16,1) char exhaust1[16] = { 0x3c, 0xbe, 0xaa, 0xaa, 0x98, 0x24, 0x18, 0x10, 0x18, 0x14, 0x04, 0x24, 0x14, 0x10, 0x14, 0x04 }; holeydma reversed scattered(16,1) char exhaust2[16] = { 0x3c, 0xbe, 0xaa, 0xaa, 0x1a, 0x24, 0x08, 0x24, 0x18, 0x10, 0x18, 0x14, 0x04, 0x24, 0x14, 0x10 }; holeydma reversed scattered(16,1) char exhaust3[16] = { 0x3c, 0xbe, 0xaa, 0xaa, 0x9a, 0x20, 0x18, 0x24, 0x08, 0x24, 0x18, 0x10, 0x18, 0x14, 0x04, 0x24 }; holeydma reversed scattered(16,2) char exhauststart[32] = { 0x03, 0xc0, 0x3f, 0xfc, 0xae, 0xee, 0x26, 0x64, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; const char collision_bullet_spaceship[78] = {0x03, 0x80, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x0f, 0xe0, 0x0f, 0xe0, 0x0f, 0xe0, 0x3f, 0xf8, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0x7f, 0xfc, 0x3f, 0xf8, 0x3f, 0xf8, 0x1b, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; #define NB_SMALL_SPRITES 32 ramchip short sp_xpos[NB_SMALL_SPRITES], sp_ypos[NB_SMALL_SPRITES]; ramchip char sp_direction[NB_SMALL_SPRITES]; ramchip char exhaust_state; ramchip char spaceship_x, spaceship_y, spaceship_state, spaceship_state_counter; void main() { char i; multisprite_init(); // Grey palette *P0C1 = 0x04; *P0C2 = 0x08; *P0C3 = 0x0b; // Blue palette *P1C1 = multisprite_color(0x84); // Dark blue *P1C2 = multisprite_color(0x87); // Light blue *P1C3 = multisprite_color(0xac); // Turquoise // Green palette *P2C1 = multisprite_color(0xd4); // Dark green *P2C2 = multisprite_color(0xd8); // Light green *P2C3 = 0x0f; // Fire palette *P3C1 = multisprite_color(0x1c); // Yellow *P3C2 = multisprite_color(0x37); // Orange *P3C3 = multisprite_color(0x43); // Red // Initialize spaceship state spaceship_x = 80 - 6; spaceship_y = 180; exhaust_state = 0; spaceship_state = 0; // Initialize small sprites { char xpos, ypos; for (ypos = 0, xpos = 0, i = 0, X = 0; X != NB_SMALL_SPRITES; xpos++, ypos++, X++) { sp_xpos[X] = xpos << 8; sp_ypos[X] = ypos << 8; sp_direction[X] = i++; if (i == 24) i = 0; } } // Main loop do { multisprite_flip(); joystick_update(); if (spaceship_state < 2) { if (joystick[0] & JOYSTICK_LEFT) { if (spaceship_x) spaceship_x--; } else if (joystick[0] & JOYSTICK_RIGHT) { if (spaceship_x < 160 - 13) spaceship_x++; } if (joystick[0] & JOYSTICK_UP) { if (spaceship_y) spaceship_y--; exhaust_state++; if (exhaust_state == 13) exhaust_state = 10; } else { exhaust_state = 0; if (joystick[0] & JOYSTICK_DOWN) { if (spaceship_y < 224 - 33) spaceship_y++; } } } char draw_spaceship; if (spaceship_state == 0) { draw_spaceship = 1; } else if (spaceship_state == 1) { // Blinking returning spaceship draw_spaceship = spaceship_state_counter & 8; spaceship_state_counter--; if (spaceship_state_counter == 0) { spaceship_state = 0; } } else if (spaceship_state == 2) { char *gfx, x, y; if (spaceship_state_counter >= 16) { gfx = explosion1; } else if (spaceship_state_counter >= 12) { gfx = explosion2; } else if (spaceship_state_counter >= 8) { gfx = explosion3; } else if (spaceship_state_counter >= 4) { gfx = explosion4; } else gfx = explosion5; x = spaceship_x - 6; y = spaceship_y - 12; if (y < 0) y = 0; else if (y >= 224 - 48) y = 224 - 49; multisprite_display_big_sprite(x, y, gfx, 6, 3, 3, 0); spaceship_state_counter--; if (spaceship_state_counter == 0) { spaceship_state = 1; spaceship_state_counter = 100; } draw_spaceship = 0; } if (draw_spaceship) { multisprite_display_big_sprite(spaceship_x, spaceship_y, spaceship, 6, 0, 2, 1); // Draw exhaust if (exhaust_state > 0 && spaceship_y < 224 - 25 - 16) { char x, y; y = spaceship_y + 24; if (exhaust_state < 10) { x = spaceship_x + 2; multisprite_display_small_sprite_ex(x, y, exhauststart, 2, 3, 0); } else { char *gfxptr; i = exhaust_state - 10; gfxptr = exhaust1 + i; x = spaceship_x + 4; multisprite_display_sprite_ex(x, y, gfxptr, 1, 3, 0); } } } for (i = 0; i != NB_SMALL_SPRITES; i++) { char xbullet, ybullet; X = i; Y = sp_direction[X]; sp_xpos[X] += dx[Y]; sp_ypos[X] += dy[Y]; xbullet = sp_xpos[X] >> 8; ybullet = sp_ypos[X] >> 8; if ((xbullet < 5 && (dx[Y] >> 8) < 0) || (xbullet >= 150 && (dx[Y] >> 8) >= 0)) { sp_direction[X] = horizontal_pingpong[Y]; } if ((ybullet < 5 && (dy[Y] >> 8) < 0) || (ybullet >= MS_YMAX - 20 && (dy[Y] >> 8) >= 0)) { sp_direction[X] = vertical_pingpong[Y]; } multisprite_display_small_sprite_ex(xbullet, ybullet, bullet, 1, 3, 0); if (spaceship_state == 0) { multisprite_compute_collision(xbullet, ybullet, 4, 8, spaceship_x, spaceship_y, 12, 24, collision_bullet_spaceship); if (multisprite_collision_detected) { // Explosion spaceship_state = 2; spaceship_state_counter = 20; } } } } while(1); } Next will be an example of using RAM to make a faster scrolling than using ROM, paving the way to 60fps shmups... example_collisions.mp4 example_collisions.a78
  13. Here is the next contribution to cc7800 : sparse tiling integrated with vertical scrolling. The idea is that you can easily design some nice levels with Tiled (Usually 20 tiles large and max 256 in height), generate the code for the tiles with sprites7800, generate the sparse tiles data with tiles7800, and integrate it into your scrolling code. This yields a quite efficient way of making backgrounds, not too expensive (it's sparse tiling, so you can use 160A with holes and as many palettes as you want), and using only ROM (as this version uses the indirect mode of Maria). In my example, the 256 high shmup level consumes 4kb for the tiles graphics, and around 4kb for the sparse tiles data. You can imagine making a "super game" with 7 levels, each occupying a 16kb ROM bank (4kb for the sprites, 4kb for the sparse tiles (level definition), 4kb for the tiles graphics data (backdoung), and 4kb for the music, the specific code and the enemy spawning tables). The code for this example (examples/example_vertical_scrolling_sparse.c), using only 2 palettes (blue and grey) : #include "prosystem.h" #define VERTICAL_SCROLLING #include "multisprite.h" // Generated by sprites7800 shmup_tiles.png reversed scattered(16,32) char blue_objects1[512] = { 0x00, 0x50, 0x74, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x55, 0x40, 0x00, 0x74, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x55, 0x54, 0xf8, 0x00, 0x00, 0xa5, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x50, 0x00, 0x00, 0x15, 0x55, 0x55, .... 0x0e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x50, 0x0f, 0x50, 0x0f, 0x50, 0x0e, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x10, 0x09, 0x20, 0x09, 0x20, 0x0d, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0e, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x10, 0x09, 0x10, 0x09, 0x20 }; // Generated by tiles7800 --sparse shmup_tiles.yaml shmup.tmx #include "example_shmup_tiles.c" void main() { char counter = 255; char done = 0; multisprite_init(); multisprite_set_charbase(blue_objects1); multisprite_vscroll_init_sparse_tiles(tilemap_data_ptrs); // Grey palette *P0C1 = 0x04; *P0C2 = 0x08; *P0C3 = 0x0c; // Blue palette *P1C1 = multisprite_color(0x84); // Dark blue *P1C2 = multisprite_color(0x88); // Light blue *P1C3 = multisprite_color(0xac); // Turquoise // Main loop do { // Prepare scrolling data if (multisprite_vscroll_buffer_empty()) { if (!done) { multisprite_vscroll_buffer_sparse_tiles(counter); if (counter) counter--; else done = 1; } else if (done == 1) { multisprite_vscroll_buffer_sparse_tiles(255); done = 2; } else done = 3; } while (*MSTAT & 0x80); multisprite_flip(); if (done != 3) multisprite_vertical_scrolling(1); } while(1); } You will find in attachment the latest versions of sprites7800 (with collision map generation, for the next demo), tiles7800 (with 256 high sparse tiling code generation), the shmup project, and the compiled 32kb ROM of the example. Next will be a demo for pixel perfect collision, and next next a vertical scrolling using RAM and immediate mode (instead of the ROM and Maria indirect mode here). And maybe next next next a "kinda" shmup game with everything together (vertical scrolling, a controllable spaceship, some bullets and a bad guy to get shot at). example_vertical_scrolling_sparse.mp4 sprites7800.exe tiles7800.exe shmup.tiled-project shmup_tiles.yaml shmup.tsx shmup.tmx example_vertical_scrolling_sparse_tiling.a78
  14. From what I've read on the web (I've neither tested the game nor looked at the code), it's not considered a bad port. Double dragon is even cited as one of the best games on the Atari 7800 (well, there aren't many to be honest) https://neovideohunter.wordpress.com/2018/01/13/atari-7800-review-double-dragon-activision-1989/. From what I've seen, it looks like it was programmed by seasoned guys (given the specificity of the Atari 7800 and the lack of tools at this time). The upper part of the screen is in 320 mode, and it's using 160A sparse tiling mode for the background (with many palettes). Not so bad. The characters are ugly. They should have been using 160B mode, but they probably couldn't afford it (probably need to reuse the same graphics with different palettes due to the limited ROM available). And in contrary to the NES, a 2 players game is available ! From a technical point of view, it makes sense. Is it really that bad at playing ?
  15. No shmup without a good explosion, which requires big sprite display. And big displays is one of the nice features of Maria ! So here are the explosions (examples/example_explosions.c) using the new multisprite_display_big_sprite macro (which optimizes the big sprite display by chaining the display list entries) : #include "prosystem.h" #include "multisprite.h" #define NB_EXPLOSIONS 5 ramchip char xpos[NB_EXPLOSIONS], ypos[NB_EXPLOSIONS]; // Generated with sprites7800 explosion.yaml holeydma reversed scattered(16,6) char explosion1[96] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ... 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; void main() { char i, j = 0, k, l = 0, x, y; multisprite_init(); *P0C1 = multisprite_color(0x1c); // Yellow *P0C2 = multisprite_color(0x37); // Orange *P0C3 = multisprite_color(0x43); // Red // Initialize explosions x = 0; y = 0; for (X = 0; X != NB_EXPLOSIONS; X++) { xpos[X] = x; ypos[X] = y; x += 160 / NB_EXPLOSIONS; y += 224 / NB_EXPLOSIONS; } // Main loop do { multisprite_flip(); char *gfx = explosion1; for (X = j; X != 0; X--) gfx += 18; k = j; for (i = 0; i != NB_EXPLOSIONS; i++) { x = xpos[i]; y = ypos[i]; multisprite_display_big_sprite(x, y, gfx, 6, 0, 3, 0); gfx += 18; k++; if (k == NB_EXPLOSIONS) { k = 0; gfx = explosion1; } } l++; if (l == 5) { j++; if (j == NB_EXPLOSIONS) j = 0; l = 0; } } while(1); } Next will come the integration of sparse tiling with vertical scrolling, in order to make nice shmups and run & guns. And then pixel-perfect collision computation, which is also required for a good shmup... Maybe (if I've got enough time) will I turn the sparse tiling into bitmapped display (immediate mode) in RAM - at the moment it only uses ROM -, in order to save DMA/CPU for more bullets on screen... Let's see... example_explosions.mp4 explosion.yaml
  16. Yes, that's very odd. Maybe is this related to the NTSC bit in the .a78 output generated by cc7800 (I'm using PAL/NTSC auto detection during execution) ? Any insight from DF designers ?
  17. Good news. It's working. So signing ROMs is required on DragonFly even on PAL Atari 7800... I thought it was required only on NTSC units...
  18. I confirm. It's a PAL Atari 7800.
  19. Ah OK. But isn't the signature only required on NTSC Atari 7800? The case here is on a Atari 7800 from Poland, so theoretically a PAL system. I will check with 7800sign if it works.
  20. I've been reported that the .out files produced by cc7800 (indeed .a78 files) don't run on the Dragon Fly. I can't check this : I'm a concerto guy. Can a Dragon Fly proud owner (or even better, designer) try to figure out what I'm doing wrong when I generate this .a78 ? 7800header output is : embedded game name : a.out rom size : 32768 cart format (v3) : linear ram@4000 cart format (v4) : linear ram@4000 controllers : 7800joy1 7800joy2 peripherals : none tv format : NTSC but the Dragon Fly states it's an "Invalid File". Does it need to be signed or something ? In attachment, the .out file (which runs on a7800), the .a dasm file generated by cc7800 (which includes the .a78 header generation) and the .lst file. Thank you in advance for having a look at it ! a.zip
  21. A new macro was added to multisprite.h : multisprite_display_small_sprite, in order to support the optimized display of sprites of small height (8 lines) with 16 lines high display zones. When the y position of a small sprite is below 8 in a height 16 display zone, it's possible to avoid the second display list entry (generally, a 16 lines sprite requires 2 display list entries, except when it's aligned with the display zone), yielding an average gain of 25% of DMA/CPU/DLL memory. With this optimization, we can get 64 missiles running around at 60fps and 128 missiles at 30fps, and less DMA glitches. Here is the full example : #include "prosystem.h" #include "multisprite.h" char i, xpos, ypos; #define NB_SMALL_SPRITES 128 ramchip short sp_xpos[NB_SMALL_SPRITES], sp_ypos[NB_SMALL_SPRITES]; ramchip char sp_direction[NB_SMALL_SPRITES]; const signed short dx[24] = {300, 289, 259, 212, 149, 77, 0, -77, -150, -212, -259, -289, -300, -289, -259, -212, -149, -77, 0, 77, 149, 212, 259, 289}; const signed short dy[24] = {0, 124, 240, 339, 415, 463, 480, 463, 415, 339, 240, 124, 0, -124, -239, -339, -415, -463, -480, -463, -415, -339, -240, -124}; const char horizontal_pingpong[24] = { 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13 }; const char vertical_pingpong[24] = { 0, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }; // Generated with sprites7800 missile.yaml holeydma reversed scattered(16,1) char missile[16] = { 0x18, 0x96, 0x7a, 0x7e, 0x7e, 0x6e, 0x9a, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; void main() { multisprite_init(); *P0C1 = multisprite_color(0x1c); // Yellow *P0C2 = multisprite_color(0x37); // Orange *P0C3 = multisprite_color(0x43); // Red // Initialize small sprites for (ypos = 0, xpos = 0, i = 0, X = 0; X != NB_SMALL_SPRITES; xpos++, ypos++, X++) { sp_xpos[X] = xpos << 8; sp_ypos[X] = ypos << 8; sp_direction[X] = i++; if (i == 24) i = 0; } // Main loop do { multisprite_flip(); for (i = 0; i != NB_SMALL_SPRITES; i++) { X = i; Y = sp_direction[X]; sp_xpos[X] += dx[Y]; sp_ypos[X] += dy[Y]; xpos = sp_xpos[X] >> 8; ypos = sp_ypos[X] >> 8; if ((xpos < 5 && (dx[Y] >> 8) < 0) || (xpos >= 150 && (dx[Y] >> 8) >= 0)) { sp_direction[X] = horizontal_pingpong[Y]; } if ((ypos < 5 && (dy[Y] >> 8) < 0) || (ypos >= MS_YMAX - 20 && (dy[Y] >> 8) >= 0)) { sp_direction[X] = vertical_pingpong[Y]; } multisprite_display_small_sprite(xpos, ypos, missile, 1, 0); } } while(1); } You will also find in this post the latest installer for cc7800 and a new sprites7800 window executable that supports small sprites and big sprites. Next example will be a big sprite display using the macro multisprite_display_big_sprite. example_small_sprites.mp4 missile.yaml cc7800-0.2.12-x86_64.msi sprites7800.exe
  22. Yes, still ways to go. Indeed, cc7800 is fourfold : - It's a C compiler for 6502 that is efficient enough to produce Atari console game code (i.e. DASM quality-like. It's not possible to use cc65 for instance to make an "action" game on 2600 or 7800, since the code would be too big and too slow. I've tried and this was totally unsucessful). This is cc6502 (written from scratch in Rust), which is I think operational and well tested. It doesn't support the full C language (no structures in particular, but structure support is so badly handled by the 6502...), but is really optimized 6502 code oriented through the possible use of "special" X and Y indexing variables. cc6502 itself requires some documentation to explicit all the limitations and to show how to write code in order to get the most of the 6502. Frankly, given all the tests and programs I've already written with it, it's quite mature today. - It's a mapper for Atari 7800, to support the specific Atari 7800 memory layout and bankswitching. This is actually cc7800, adding keywords to the C language : bankX, scattered, holeydma, ramchip... This is working but may be not complete. Requires also some explanations. - It's a set of tools (tools7800) to generate C code for the sprites, tiles... - And to make it usable to create games, a set of headers (conio.h, multisprite.h, sparse_tiling.h, etc) and examples, with many macros that I'm still currently developing. This also requires documentation before being fully useful... It's going on. My goal is to have a complete package for the 4th of april 2024, the date of anniversary of the first release. This will never be a package for beginners. This will always require prior C embedded development experience.
  23. This article is partially incorrect (Maria has tile compression, handles smooth scrolling with ease (no intensive software routine required at all) and can display up to 25 colors per zone), but is correct I think in its conclusions : the limited bandwidth due to the bus sharing between Maria and the CPU is quite limiting, and the sharing of bandwidth between background and foreground graphics favors games with black or sparse background... I agree that SMS and NES with their separate video bus / memory can generally be considered more powerful than Atari 7800, but well... frankly ? IMO, I still find the Atari 7800 more "attractive", probably due to its direct link to the Atari 2600, its programming model which is indeed quite fun, and also because it didn't get the attention it deserved in its time... There are still so many things to do with it, it's still an open field...
  24. Using bitmaps saves on sprite usage (wrt indirect access / tiling), but definitely eats into the foreground sprite count. Indeed, Maria doesn't distinguish between background and foreground, and even doesn't know about sprites. It just displays objects in order, either in immediate mode (aka sprite or bitmap mode), on in indirect mode (aka tiled mode), until it has finished or there is no DMA cycles left on the line... Completely different from the NES, but very flexible and keen to display big "sprites", bitmaps, or many small sprites at your will... The main drawback (to me) is the 6502 speed bottleneck to fill the display lists, and the memory layout which is very special and limiting (except with bankset bankswitching, which requires to handle the HALT line on cart).
  25. You can definitely use bitmapped BG (aka immediate mode), which is cheaper DMA-wise than tiled backgrounds (aka character more). In the case of dynamic/varied BG, a bitmapped BG may consume some / a lot of RAM (and thus generally requires 16KB RAM mapped at 0x4000), and ROM to RAM copying, while using tiling is easier (it's a form of compression, an indirect graphics addressing performed by MARIA at the expense of DMA/CPU time) and generally requires less RAM / ROM. Bitmapped BG is definitely required if background must be full of 160B, 320B or 320C graphics...
×
×
  • Create New...