Sprites! Wonderful little things. I have fond memories of getting one on screen on the Commodore 64 back when I was wearing size 9’s, after typing in several hundred lines of code from some tutorial in a microcomputer magazine.
Armed with a few VDP skills – loading patterns, organising artwork into VRAM, setting tile IDs – basic sprite work seems to be pretty easy. Sprite tiles are uploaded to VRAM in the same way as plane A/B patterns, and are again referred to using tile IDs. The structure to describe a sprite’s attributes is a little more complex, as are some of the rules for displaying and sorting sprites, but I’m confident I can wrap the basics up in just a few paragraphs. Sprites can be displayed at any X/Y screen coordinates; they’re not tied to cells like the other planes. They have their own plane, too.
Sprites – The basics
Sprites use the same pattern data as the A/B planes, and can be made up of more than one pattern, too. The VDP supports a grid of up to 4×4 patterns, and will manage their positioning for us to fit together, so it’s possible to have a sprite of up to 32×32 pixels in size. It’s feasible to support larger sizes, but they would have to be positioned manually. All patterns in the sprite must share one palette.
First we need a sprite for testing. I’ve found some great free sprites after some hunting around, and settled on this little monster – he’s a 24×24, 16 colour beast under the Creative Commons license (source in References section). After some tidying up the PNG file in The Gimp, I converted it to indexed mode, 16 colour (optimised palette):
After importing it into Bmp2Tile, set Sprite Output Mode, press the * key to select the whole image, and then export both the tiles and palette. The palette looks garbled in the preview, but it does export correctly; either I don’t understand how to make it show properly, or Windows 7 doesn’t correctly handle DJGPP/Allegro applications.
Loading sprite artwork
Sprites use the same VRAM pattern memory as the A/B planes, so for moving them to VRAM I’ll simply rename the LoadFont subroutine to something more generic like LoadTiles. Now that we’re dealing with more than one art asset, it might be worth figuring out how to organise it all into VRAM. We can modify the preprocessor tricks to do this all for us, and move these addresses to a separate file so they can be layed out neatly. Here’s a quick example of how I’ve arranged the Pixel Font and a few tiles for various sprites, of various sizes:
assetmap.asm:
; ************************************ ; Art asset VRAM mapping ; ************************************ PixelFontVRAM: equ 0x0000 Sprite1VRAM: equ PixelFontVRAM+PixelFontSizeB Sprite2VRAM: equ Sprite1VRAM+Sprite1SizeB Sprite3VRAM: equ Sprite2VRAM+Sprite2SizeB ; ************************************ ; Include all art assets ; ************************************ include 'assets\fonts\pixelfont.asm' include 'assets\sprites\sprite1.asm' include 'assets\sprites\sprite2.asm' include 'assets\sprites\sprite3.asm' ; ************************************ ; Include all palettes ; ************************************ include 'assets\palettes\paletteset1.asm'
pixelfont.asm:
PixelFont: ; Font start address dc.l $01111100 dc.l $11000110 dc.l $10111010 dc.l $10000010 dc.l $10111010 dc.l $10101010 dc.l $11101110 dc.l $00000000 ; ...etc PixelFontEnd ; Font end address PixelFontSizeB: equ (PixelFontEnd-PixelFont) ; Font size in bytes PixelFontSizeW: equ (PixelFontSizeB/2) ; Font size in words PixelFontSizeL: equ (PixelFontSizeB/4) ; Font size in longs PixelFontSizeT: equ (PixelFontSizeB/32) ; Font size in tiles PixelFontTileID: equ (PixelFontVRAM/32) ; ID of first tile
sprite1.asm:
Sprite1: dc.l $11111111 ; Tile: 1 dc.l $10000001 dc.l $10000001 dc.l $10000001 dc.l $10000001 dc.l $10000001 dc.l $10000001 dc.l $11111111 dc.l $11111111 ; Tile: 2 dc.l $10000001 dc.l $10000001 dc.l $10000001 dc.l $10000001 dc.l $10000001 dc.l $10000001 dc.l $11111111 Sprite1End ; Sprite end address Sprite1SizeB: equ (Sprite1End-Sprite1) ; Sprite size in bytes Sprite1SizeW: equ (Sprite1SizeB/2) ; Sprite size in words Sprite1SizeL: equ (Sprite1SizeB/4) ; Sprite size in longs Sprite1SizeT: equ (Sprite1SizeB/32) ; Sprite size in tiles Sprite1TileID: equ (Sprite1VRAM/32) ; ID of first tile
I’ve also moved the palettes to their own file for consistency. Now asset files can be added and removed at will, and it should be simple to keep them organised correctly in VRAM. It’s not an all-round solution, if the game gets big we have neither the space nor the need to fit everything in VRAM at once, so I’ll need to come up with a more dynamic solution if and when the time comes. For now, this is perfect for my needs.
Drawing sprites
Sprites are drawn by filling in details in a sprite attribute table. The VDP’s memory contains an area specifically for this attribute data – at 0xE000 – set in register 5 during intialisation. Each entry is 8 bytes long, and looks a little something like this:
000000YY YYYYYYYY 0000HHVV 0NNNNNNN DPPFFTTT TTTTTTTT 000000XX XXXXXXXX
where:
Y = Y coord (from -128 to screen height + 128) H/V = Sprite grid dimensions, in tiles N = Index of next sprite attribute (a linked list next ptr) D = Draw priority P = Palette index F = Flip bits (vert. and horiz.) T = Index of first tile in sprite X = X coord (from -128 to screen width + 128)
The sprite window’s coordinate system has a 128 pixel border, assumingly to allow sprites to be partially or fully hidden off screen, so for the sprite to be visible in the top-left corner coords of 128,128 must be set. The X and Y coordinates (of the top-left corner of the sprite) are defined in 10 bits each, although I’m unsure as to why they are at opposite ends of the structure. The 4 bits for the grid dimensions define how large the sprite will be, in tiles. It accepts all combinations from 1×1 to 4×4, and the positioning of subsequent tiles will be handled by the VDP for us automatically, as well as flipping for the entire sprite as a whole. I’m unsure what range the draw priority accepts, I’ve left it as zero for now since it’s out of the scope of this article. There are two bits for the H and V flipping (I won’t be using those yet), and the index of the first pattern tile in the sprite.
This leaves us with the N – the index of the next sprite attribute struct. The VDP will draw subsequent sprites by jumping through these next pointers, until it hits 0. This is also used for the drawing order – strangely, the VDP draws front-to-back, unlike most graphics APIs I’ve worked with on other platforms where the drawing is done back-to-front, which seems to make logical sense to me. So with this in mind, expect the first sprite in the linked list to be drawn on top, and the second to be drawn underneath it.
Here’s an example:
SpriteDesc1: dc.w 0x0080 ; Y coord (+ 128) dc.b %00001111 ; Width (bits 0-1) and height (bits 2-3) dc.b 0x00 ; Index of next sprite (linked list) dc.b 0x00 ; H/V flipping (bits 3/4), palette index (bits 5-6), priority (bit 7) dc.b Sprite1TileID ; Index of first tile dc.w 0x0080 ; X coord (+ 128)
Prefixing a value with % allows it to be specified in raw binary, useful for defining the width/height bits. Here I’ve defined a sprite made of a 4×4 grid of tiles. The struct then needs moving to the VDP, using a quick subroutine:
LoadSpriteTables: ; a0 - Sprite data address ; d0 - Number of sprites move.l #vdp_write_sprite_table, vdp_control subq.b #0x1, d0 ; 2 sprites attributes @AttrCopy: move.l (a0)+, vdp_data move.l (a0)+, vdp_data dbra d0, @AttrCopy rts
…which is simply used with:
lea SpriteDesc1, a0 ; Sprite table data move.w #0x1, d0 ; 1 sprite jsr LoadSpriteTables
Providing the 16 tiles have been loaded into VRAM, as well as the correct palette, we should have a big bad monster:
Moving sprites
Since sprites can be positioned at any X/Y coord, part of the point of them is to be able to move them about at runtime, so we’ll need subroutines to modify the X and Y coords. Not too difficult, just write to the correct addresses in the sprite attribute table:
SetSpritePosX: ; Set sprite X position ; d0 (b) - Sprite ID ; d1- X coord clr.l d3 ; Clear d3 move.b d0, d3 ; Move sprite ID to d3 mulu.w #0x8, d3 ; Sprite array offset add.b #0x6, d3 ; X coord offset swap d3 ; Move to upper word add.l #vdp_write_sprite_table, d3 ; Add to sprite attr table move.l d3, vdp_control ; Set dest address move.w d1, vdp_data ; Move X pos to data port rts SetSpritePosY: ; Set sprite Y position ; d0 (b) - Sprite ID ; d1
- Y coord clr.l d3 ; Clear d3 move.b d0, d3 ; Move sprite ID to d3 mulu.w #0x8, d3 ; Sprite array offset swap d3 ; Move to upper word add.l #vdp_write_sprite_table, d3 ; Add to sprite attr table move.l d3, vdp_control ; Set dest address move.w d1, vdp_data ; Move Y pos to data port rts
Used with:
move.w #0x0, d0 ; Sprite ID move.w #0xB0, d1 ; X coord jsr SetSpritePosX ; Set X pos move.w #0xB0, d1 ; Y coord jsr SetSpritePosY ; Set Y pos
Just for good measure, I’ve added another monster friend to demonstrate drawing two sprites, making sure to set the next sprite ID in the linked list, and terminating the second sprite with a 0:
SpriteDescs: dc.w 0x0000 ; Y coord (+ 128) dc.b %00001111 ; Width (bits 0-1) and height (bits 2-3) in tiles dc.b 0x01 ; Index of next sprite (linked list) dc.b 0x00 ; H/V flipping (bits 3/4), palette index (bits 5-6), priority (bit 7) dc.b Sprite1TileID ; Index of first tile dc.w 0x0000 ; X coord (+ 128) dc.w 0x0000 ; Y coord (+ 128) dc.b %00001111 ; Width (bits 0-1) and height (bits 2-3) in tiles dc.b 0x00 ; Index of next sprite (linked list) dc.b 0x20 ; H/V flipping (bits 3/4), palette index (bits 5-6), priority (bit 7) dc.b Sprite2TileID ; Index of first tile dc.w 0x0000 ; X coord (+ 128)
Here’s the finished result:
Check those badasses out.
There’s plenty more I could expand on – draw priorities and sorting, limitations of sprite drawing, subroutines to add and remove sprites at runtime – all in good time.
Matt.
Source code
Assemble with:
asm68k.exe /p spritetest.asm,spritetest.bin
One thing that took me a while to figure out is that if the first tile that’s loaded into VRAM isn’t blank, the console will just use it instead of the normal transparent tile. As a workaround, I made a blank tile that I indexed before the main sprite in VRAM, which seems to work.