Stardust

The Finnish team Bloodhouse burst onto the Amiga scene in 1993 with the release of their Asteroids inspired game Stardust. The game itself was great fun, but the tunnel sequence blew everyone away at the time.

This analysis was done based on the demo version that appeared on coverdisks. I chose this version because I think it's the best looking tunnel! Sadly this tunnel never appeared in the full game, and the other difference is the demo did not feature the energy meter in the bottom right corner.

Bloodhouse used dual-playfield mode for the tunnel sequence, which allowed 2 playfields to overlap each other and move independently. Although the Amiga can handle 8 colours in each playfield, Bloodhouse went with 3 bitplanes (7 colours plus transparent) for the foreground and 2 bitplanes for the background. This was most likely chosen to keep the game running at the full frame rate of 50Hz as there are some huge rocks drawn at certain times. I'm guessing the game couldn't quite keep up with 6 bitplanes at all times.

The sprite layer retrieves graphics from the following spritesheet:

The game overwrites the score and energy sprites as points increase of the energy decreases. There are many more player sprites but I've only included the in-game sprite above due to laziness :)

The screen is split into 3 vertical bands. The top band contains 8 sprites, all of which are 3 colours + transparent. The player ship comes next made up of 4 sprites, each of which is made by attaching 2 together to allow 15 colours + transparent. Finally the bottom section contains the life counter (and energy meter in the full game) also made up of 2 attached sprites.

The sprite numbers are shown above each sprite so you can see how they are used.

You may have spotted a potential problem. If the player ship goes right to the top or bottom of the screen, there would be more than 8 sprites on a line and glitches would occur. The game got around this problem by preventing the player ship from entering those areas.

The background is a 6 frame animation made up of only 4 colours heavily dithered. Due to the animation speed, you don't even notice the dithering. The full animation is 448x384 pixels, which is much larger than the Amiga's screen size of 288x255 pixels:

The team only had to store the top half of the animation in memory thanks to the Amiga's modulo register and the copper, which changes the modulo to a negative value half way down the screen. This switch means the Amiga fetches the bitplane data from the previous line each time, drawing it upside down once it's drawn the top half.

The actual animation frames were ripped from a savestate with Maptapper and look like this in memory:

To see all the layers that make up the tunnel, it's easiest to view a video of the action:

Copperlist

Here's the copperlist, which is surprisingly small. The game has to skip 18 bytes between each row of the display due to the fact the game window is much smaller than the animation:

Chip Addr: Copper instructions ; Comments -------------------------------------------------- $00000AC6: 008E 2590 ; DIWSTRT = 0x2590 $00000ACA: 0090 30B0 ; DIWSTOP = 0x30B0 $00000ACE: 009C 8010 ; INTREQ = 0x8010 $00000AD2: 0100 0000 ; BPLCON0 = 0x0000 $00000AD6: 0104 0024 ; BPLCON2 = 0x0024 $00000ADA: 0092 0038 ; DDFSTRT = 0x0038 $00000ADE: 0094 00C8 ; DDFSTOP = 0x00C8 $00000AE2: 0108 0004 ; BPL1MOD = 0x0004 $00000AE6: 010A 0012 ; BPL2MOD = 0x0012 ; Skip 18 bytes between each background line

Now the game sets up the palette:

$00000AEA: 0180 0114 ; COLOR00 = 0x0114 $00000AEE: 0182 0FFF ; COLOR01 = 0x0FFF $00000AF2: 0184 0FF1 ; COLOR02 = 0x0FF1 $00000AF6: 0186 0FA1 ; COLOR03 = 0x0FA1 $00000AFA: 0188 0AAB ; COLOR04 = 0x0AAB $00000AFE: 018A 0889 ; COLOR05 = 0x0889 $00000B02: 018C 0547 ; COLOR06 = 0x0547 $00000B06: 018E 0415 ; COLOR07 = 0x0415 $00000B0A: 0180 0114 ; COLOR00 = 0x0114 $00000B0E: 0192 011C ; COLOR09 = 0x011C $00000B12: 0194 0119 ; COLOR10 = 0x0119 $00000B16: 0196 0117 ; COLOR11 = 0x0117 ; Colours 12-16 not used $00000B1A: 01A2 0500 ; COLOR17 = 0x0500 $00000B1E: 01A4 0911 ; COLOR18 = 0x0911 $00000B22: 01A6 0F22 ; COLOR19 = 0x0F22 ; Colour 20 not used at the top $00000B26: 01AA 0511 ; COLOR21 = 0x0511 $00000B2A: 01AC 0911 ; COLOR22 = 0x0911 $00000B2E: 01AE 0F22 ; COLOR23 = 0x0F22 ; Colour 24 not used at the top $00000B32: 01B2 0777 ; COLOR25 = 0x0777 $00000B36: 01B4 0BBB ; COLOR26 = 0x0BBB $00000B3A: 01B6 0FFF ; COLOR27 = 0x0FFF ; Colour 28 not used at the top $00000B3E: 01BA 0777 ; COLOR29 = 0x0777 $00000B42: 01BC 0BBB ; COLOR30 = 0x0BBB $00000B46: 01BE 0FFF ; COLOR31 = 0x0FFF

All 8 sprites are set to empty data:

$00000B4A: 0120 0000 0122 0000 ; SPR0PT = 0x00000000 $00000B52: 0140 0000 ; SPR0POS = 0x0000 $00000B56: 0142 0000 ; SPR0CTL = 0x0000 $00000B5A: 0124 0000 0126 0000 ; SPR1PT = 0x00000000 $00000B62: 0148 0000 ; SPR1POS = 0x0000 $00000B66: 014A 0000 ; SPR1CTL = 0x0000 $00000B6A: 0128 0000 012A 0000 ; SPR2PT = 0x00000000 $00000B72: 0150 0000 ; SPR2POS = 0x0000 $00000B76: 0152 0000 ; SPR2CTL = 0x0000 $00000B7A: 012C 0000 012E 0000 ; SPR3PT = 0x00000000 $00000B82: 0158 0000 ; SPR3POS = 0x0000 $00000B86: 015A 0000 ; SPR3CTL = 0x0000 $00000B8A: 0130 0000 0132 0000 ; SPR4PT = 0x00000000 $00000B92: 0160 0000 ; SPR4POS = 0x0000 $00000B96: 0162 0000 ; SPR4CTL = 0x0000 $00000B9A: 0134 0000 0136 0000 ; SPR5PT = 0x00000000 $00000BA2: 0168 0000 ; SPR5POS = 0x0000 $00000BA6: 016A 0000 ; SPR5CTL = 0x0000 $00000BAA: 0138 0000 013A 0000 ; SPR6PT = 0x00000000 $00000BB2: 0170 0000 ; SPR6POS = 0x0000 $00000BB6: 0172 0000 ; SPR6CTL = 0x0000 $00000BBA: 013C 0000 013E 0000 ; SPR7PT = 0x00000000 $00000BC2: 0178 0000 ; SPR7POS = 0x0000 $00000BC6: 017A 0000 ; SPR7CTL = 0x0000

The copper waits until the top of the screen, and sets the sprite pointers to the score and distance remaining graphics:

$00000BCA: 2C01 FFFE ; Wait for vpos >= 0x2C and hpos >= 0x00 $00000BCE: 0120 0006 0122 1DEC ; SPR0PT = 0x00061DEC $00000BD6: 0140 31AB ; SPR0POS = 0x31AB $00000BDA: 0142 4700 ; SPR0CTL = 0x4700 $00000BDE: 0124 0006 0126 1E48 ; SPR1PT = 0x00061E48 $00000BE6: 0148 31B5 ; SPR1POS = 0x31B5 $00000BEA: 014A 4700 ; SPR1CTL = 0x4700 $00000BEE: 0128 0006 012A 1CD8 ; SPR2PT = 0x00061CD8 $00000BF6: 0150 31BF ; SPR2POS = 0x31BF $00000BFA: 0152 4700 ; SPR2CTL = 0x4700 $00000BFE: 012C 0006 012E 1CD8 ; SPR3PT = 0x00061CD8 $00000C06: 0158 31C9 ; SPR3POS = 0x31C9 $00000C0A: 015A 4700 ; SPR3CTL = 0x4700 $00000C0E: 0130 0006 0132 2370 ; SPR4PT = 0x00062370 $00000C16: 0160 3850 ; SPR4POS = 0x3850 $00000C1A: 0162 4300 ; SPR4CTL = 0x4300 $00000C1E: 0134 0006 0136 23A0 ; SPR5PT = 0x000623A0 $00000C26: 0168 3858 ; SPR5POS = 0x3858 $00000C2A: 016A 4300 ; SPR5CTL = 0x4300 $00000C2E: 0138 0006 013A 23D0 ; SPR6PT = 0x000623D0 $00000C36: 0170 3860 ; SPR6POS = 0x3860 $00000C3A: 0172 4300 ; SPR6CTL = 0x4300 $00000C3E: 013C 0006 013E 2400 ; SPR7PT = 0x00062400 $00000C46: 0178 3868 ; SPR7POS = 0x3868 $00000C4A: 017A 4300 ; SPR7CTL = 0x4300

The correct pixel indentation is set, and the bitplane pointers are setup to display the correct foreground and background frames:

$00000C4E: 2F01 FFFE ; Wait for vpos >= 0x2F and hpos >= 0x00 $00000C52: 0102 00F0 ; BPLCON1 = 0x00F0 $00000C56: 00E0 0006 00E2 3382 ; BPL1PT = 0x00063382 $00000C5E: 00E8 0006 00EA 6172 ; BPL3PT = 0x00066172 $00000C66: 00F0 0006 00F2 8F62 ; BPL5PT = 0x00068F62 $00000C6E: 00E4 0001 00E6 D0E8 ; BPL2PT = 0x0001D0E8 $00000C76: 00EC 0001 00EE FAE8 ; BPL4PT = 0x0001FAE8

At line $30, the display is turned on. This sets up a dual playfield screen with 3 foreground planes and 2 background planes:

$00000C7E: 3001 FFFE ; Wait for vpos >= 0x30 and hpos >= 0x00 $00000C82: 0100 5600 ; BPLCON0 = 0x5600

Once the top status panel is displayed, the sprite pointers are reset to display the player ship, which is also made up from sprites. The player ship sprites are all attached (sprites 0 and 1 overlay each other, 2 to 3, 4 to 5, and 6 to 7), so they appear as 16 colours. This gives the player ship a maximum size of 64 pixels wide:

$00000C86: 4801 FFFE ; Wait for vpos >= 0x48 and hpos >= 0x00 $00000C8A: 0120 0002 0122 580E ; SPR0PT = 0x0002580E $00000C92: 0140 FF7F ; SPR0POS = 0xFF7F $00000C96: 0142 1783 ; SPR0CTL = 0x1783 $00000C9A: 0124 0002 0126 5876 ; SPR1PT = 0x00025876 $00000CA2: 0148 FF7F ; SPR1POS = 0xFF7F $00000CA6: 014A 1783 ; SPR1CTL = 0x1783 $00000CAA: 0128 0002 012A 58DE ; SPR2PT = 0x000258DE $00000CB2: 0150 FB87 ; SPR2POS = 0xFB87 $00000CB6: 0152 1483 ; SPR2CTL = 0x1483 $00000CBA: 012C 0002 012E 594A ; SPR3PT = 0x0002594A $00000CC2: 0158 FB87 ; SPR3POS = 0xFB87 $00000CC6: 015A 1483 ; SPR3CTL = 0x1483 $00000CCA: 0130 0002 0132 59B6 ; SPR4PT = 0x000259B6 $00000CD2: 0160 FB8F ; SPR4POS = 0xFB8F $00000CD6: 0162 1483 ; SPR4CTL = 0x1483 $00000CDA: 0134 0002 0136 5A22 ; SPR5PT = 0x00025A22 $00000CE2: 0168 FB8F ; SPR5POS = 0xFB8F $00000CE6: 016A 1483 ; SPR5CTL = 0x1483 $00000CEA: 0138 0002 013A 5A8E ; SPR6PT = 0x00025A8E $00000CF2: 0170 FF97 ; SPR6POS = 0xFF97 $00000CF6: 0172 1783 ; SPR6CTL = 0x1783 $00000CFA: 013C 0002 013E 5AF6 ; SPR7PT = 0x00025AF6 $00000D02: 0178 FF97 ; SPR7POS = 0xFF97 $00000D06: 017A 1783 ; SPR7CTL = 0x1783

The colours for the player ship are also altered from the palette used at the top for the score and distance:

$00000D0A: 01A0 000C ; COLOR16 = 0x000C $00000D0E: 01A2 0222 ; COLOR17 = 0x0222 $00000D12: 01A4 0555 ; COLOR18 = 0x0555 $00000D16: 01A6 0777 ; COLOR19 = 0x0777 $00000D1A: 01A8 0999 ; COLOR20 = 0x0999 $00000D1E: 01AA 0BBB ; COLOR21 = 0x0BBB $00000D22: 01AC 0DDD ; COLOR22 = 0x0DDD $00000D26: 01AE 0FFF ; COLOR23 = 0x0FFF $00000D2A: 01B0 0000 ; COLOR24 = 0x0000 $00000D2E: 01B2 0600 ; COLOR25 = 0x0600 $00000D32: 01B4 0910 ; COLOR26 = 0x0910 $00000D36: 01B6 0C30 ; COLOR27 = 0x0C30 $00000D3A: 01B8 0D60 ; COLOR28 = 0x0D60 $00000D3E: 01BA 0EA0 ; COLOR29 = 0x0EA0 $00000D42: 01BC 0FF0 ; COLOR30 = 0x0FF0 $00000D46: 01BE 0FF0 ; COLOR31 = 0x0FF0

Now the game waits for the middle of the tunnel to be displayed. In this case, it's at vertical position $6f. Once reached, the modulo is set to $ffa2 which is -94. Once the Amiga has displayed the visible part of the tunnel, it takes that value off and also subtracts the full width of the tunnel. This positions the pointer to the previous line of the tunnel, effectively drawing it upside down.

$00000D4A: 6F01 FFFE ; Wait for vpos >= 0x6F and hpos >= 0x00 $00000D4E: 010A FFA2 ; BPL2MOD = 0xFFA2

Now the game waits for the bottom part of the screen (which is row $11a) so the game has to wait for line $ff, then $1a due to the 8 bit nature of the wait command. The palette is then altered for the life counter sprites at the bottom:

$00000D52: FFDF FFFE ; Wait for vpos >= 0xFF and hpos >= 0xDE $00000D56: 1A09 FFFE ; Wait for vpos >= 0x1A and hpos >= 0x08 $00000D5A: 01A0 0004 ; COLOR16 = 0x0004 $00000D5E: 01A2 0600 ; COLOR17 = 0x0600 $00000D62: 01A4 0910 ; COLOR18 = 0x0910 $00000D66: 01A6 0C30 ; COLOR19 = 0x0C30 $00000D6A: 01A8 0D60 ; COLOR20 = 0x0D60 $00000D6E: 01AA 0EA0 ; COLOR21 = 0x0EA0 $00000D72: 01AC 0FF0 ; COLOR22 = 0x0FF0 $00000D76: 01AE 0FFD ; COLOR23 = 0x0FFD $00000D7A: 01B0 0229 ; COLOR24 = 0x0229 $00000D7E: 01B2 0333 ; COLOR25 = 0x0333 $00000D82: 01B4 0555 ; COLOR26 = 0x0555 $00000D86: 01B6 0777 ; COLOR27 = 0x0777 $00000D8A: 01B8 0999 ; COLOR28 = 0x0999 $00000D8E: 01BA 0BBB ; COLOR29 = 0x0BBB $00000D92: 01BC 0DDD ; COLOR30 = 0x0DDD $00000D96: 01BE 0FFF ; COLOR31 = 0x0FFF

Finally the game sets up the sprite pointers for the life counter:

$00000D9A: 1B09 FFFE ; Wait for vpos >= 0x1B and hpos >= 0x08 $00000D9E: 0120 0006 0122 2070 ; SPR0PT = 0x00062070 $00000DA6: 0140 1C50 ; SPR0POS = 0x1C50 $00000DAA: 0142 2C06 ; SPR0CTL = 0x2C06 $00000DAE: 0124 0006 0126 20B0 ; SPR1PT = 0x000620B0 $00000DB6: 0148 1C50 ; SPR1POS = 0x1C50 $00000DBA: 014A 2C86 ; SPR1CTL = 0x2C86 $00000DBE: 0128 0006 012A 20F0 ; SPR2PT = 0x000620F0 $00000DC6: 0150 2058 ; SPR2POS = 0x2058 $00000DCA: 0152 2706 ; SPR2CTL = 0x2706 $00000DCE: 012C 0006 012E 2230 ; SPR3PT = 0x00062230 $00000DD6: 0158 2058 ; SPR3POS = 0x2058 $00000DDA: 015A 2786 ; SPR3CTL = 0x2786 $00000DDE: 2F01 FFFE ; Wait for vpos >= 0x2F and hpos >= 0x00 $00000DE2: 0100 0000 ; BPLCON0 = 0x0000 $00000DE6: ffff fffe ; End of copperlist