By Andrew Davie (adapted by Duane Alan Hahn, a.k.a. Random Terrain)
As an Amazon Associate I earn from qualifying purchases.
Page Table of Contents
Original Session
It's time to begin our understanding of sprites.
What are sprites? By now, sprites are well-known in the gaming industry. They are small, independently movable objects which are drawn by hardware anywhere over the top of playfield graphics. The Atari 2600 was the first console to introduce general-purpose sprites—back in the day they were called 'player missile graphics'. It was the Commodore 64 which introduced the term 'sprites', which we know and love.
The Atari 2600 has two 'players', two 'missiles' and a 'ball'—all of these are sprites, and each has various parameters which can be adjusted by the programmer (position, size, color, shape, etc). We're going to concentrate, this session, on the 'players' and how they work.
Player graphics have much finer resolution than playfield graphics. Each player is 8 pixels wide, and each pixel in a player is just a single TIA color-clock in width. In other words, the pixels in player graphics are a quarter of the width of the pixels in playfield graphics. The graphics of each player are controlled by a single 8-bit TIA register. The register for player 0 (the first player) is GRP0 (standing for 'Graphics, Player 0') and the register for the second player is GRP1. When you write data to either of these registers you change the visuals of the relevant player sprite being drawn on the screen.
Just like playfield graphics, the player graphics registers only hold a single 'line' of data. If you do not modify the data on-the-fly (that is, changing it every scanline), then the TIA just displays the same data on every scanline. So kernels using sprite graphics typically modify these player graphics registers constantly.
Surprisingly, though player sprites can be (effectively) positioned anywhere on the screen, they do NOT have position registers. Most more modern machines (Nintendo, C64, etc.) provided an x,y coordinate which was used to position a sprite on the screen. The Atari 2600 is a much more primitive beast.
The horizontal position of a player sprite is controlled by writing to a 'reset position' register (RESP0 for sprite 0 and RESP1 for sprite 1). When you write to these registers, you cause the hardware to begin drawing the relevant sprite … immediately! This is very strange and a bit hard to get used to at first. To move a sprite horizontally to any x-position on a scanline, one has to make sure that the RESP0 write happens just before the position on the scanline at which you want the sprite to appear. Since the 6502 is running at 1/3 of the clock speed of the TIA, this makes it incredibly difficult to write to RESP0 at exactly the right time. For every cycle of 6502 time, three pixels (cycles of TIA time) pass. So it's only possible to position sprites (through RESPx writes) with an accuracy of 1 6502 clock period, or in other words three TIA pixels.
To facilitate fine-positioning of sprites, the TIA has additional registers which allow the sprite to be adjusted in position by a few pixels. We are not going to cover that this session—but instead we'll have a look at how sprite graphics are written, how the course RESPx registers are used, and how sprite colors are controlled. Fine positioning of sprites is an art in itself, and many solutions have been proposed on the [stella] list. We'll get to that in a session or two, but for now, let's stick with the basics.
The sample kernel shows a fully working sprite demo.
There are very few additions from our earlier playfield demos…
lda #$56
sta COLUP0
lda #$67
sta COLUP1
In our initialization (before the main frame loop) the above code is initializing the colors of the two player sprites. These are random purplish colors. You may also change the color on-the-fly by rewriting it every scanline. Remember, though—you only have 76 cycles per scanline—so there's only so much you can cram into a single line before you run out of 'space'.
MiddleLines
SLEEP 20
sta RESP0
SLEEP 10
sta RESP1
stx GRP0 ; modify sprite 0 shape
stx GRP1
sta WSYNC
inx
cpx #184
bne MiddleLines
The above code sample is the 'guts' of our sprite demo. It doesn't do a lot of new stuff. You should already be familiar with the SLEEP macro—it just causes a delay of a certain number of 6502 cycles. The purpose of the SLEEP macros here is to delay to a position somewhere in the middle of the scanline—you may play with the values and see the effect on the positioning of the sprites.
Immediately after each SLEEP, there's a write to RESPx for each of the player sprites. This causes the TIA to begin drawing the appropriate player sprite immediately. And what will it draw?
stx GRP0 ; modify sprite 0 shape
stx GRP1
Since, in this kernel, the x register is counting the scanline number, that is also the value written to both of the graphics registers (GRPx) for the player sprites. So the graphics we see will change on each scanline, and it will represent a visual image of the scanline counter. This should be pretty evident by the image below:
Here's the sample kernel:
That's pretty much all there is to getting sprites up and running. There are a few interesting things we need to cover in the coming sessions, including sprite size, sprite repeating, priorities, buffered sprite drawing, drawing specific images/shapes and lots of other stuff. But now you have the basics, and you should be able to do some experimenting with what you see here.
See you next time!
The answer to each question below is hidden in a black box that has a green border. Hover your mouse pointer over the box (or tap it if you're using something like an iPhone) to reveal the answer.
1.Modify the kernel so that the color of the sprite is changed every scanline. How many cycles does this add to your kernel? How many cycles total is each of your lines taking now?
It takes 3 cycles per write to a color register (eg: stx COLUP1), but it takes two or more additional cycles if you want to load a specific color. The variation in time depends on the addressing mode you use to load the color (eg: an immediate value = 2 cycles, but loading indirectly through a zero page pointer to a memory location, indexed by the y register, would take 6 cycles!).
lda #34 ; 2
sta COLUP1 ; 3
lda (colour),y ; 6
sta COLUP1 ; 3
2.Instead of using the scanline to write the shape of the sprite, load the shape from a table. Can you think how it would be possible to draw (say) a Mario-shaped sprite anywhere on the screen? This is tricky, so we'll devote a session or more to vertical positioning.
This really is too tricky to answer here. Future sessions will cover this problem thoroughly, as its fundamental to drawing sprites in your game.
3.What happens when you use more than 76 cycles on a line—how will this code misbehave?
Remember that the TIA and the TV beam are in synch. The timing is such that precisely 76 cycles of 6502 time, or 228 cycles of TIA time, correspond to *exactly* one scanline on the TV. Currently we've been using "sta WSYNC" to synchronize our kernel to the start of every scanline. This isn't necessary IF our code makes sure that our kernel lines take EXACTLY 76 cycles to execute.
But since the above code DOES use "sta WSYNC", a 3 cycle instruction, we really only have 73 cycles per line available for other processing. If we exceed these 73 cycles, then that pushes the "sta WSYNC" past the point at which it's on the current scanline and onto the point where it's really on the NEXT scanline. And if it happens on the NEXT scanline, it will operate as expected (and that, as we know, is by halting the 6502 until the start of the NEXT scanline).
So essentially, if our code exceeds 76 cycles, then each scanline will actually be two scanlines deep! And instead of sending, say, 262 scanlines per frame, we'd be sending 524. Most TVs cannot cope with this and they will, as noted, 'roll'. I just wanted you to understand WHY.
4.The picture shows sprites over the 'border' areas at top and bottom, yet the code which draws sprites is only active for the middle section. Why is this happening? How would you prevent it?
A good lesson in how the TIA works. The TIA registers hold whatever you put into them, until you next put something in to them. So after our last write to the sprite registers, the TIA keeps displaying the same shape for sprites, on each scanline, until we write again. So what we're really seeing in those border areas is the last write (which is actually at the bottom of the changing shape area of sprites) repeated on the bottom, and then on the top again, until we start writing sprite shapes again.
The solution is to write 0 to GRP0 and GRP1 when we've finished drawing our sprites—and, of course, on initialization of the system.
5.Move the SLEEP and RESPx code outside the middle loop—place this code BEFORE the loop. What differences would you expect to see? Is the result surprising?
Barring minor timing changes which will cause the positions to shift slightly, the effect I was trying to show was that it is not necessary to rewrite the RESPx registers every scanline. You only need to position your sprites once each, and they will remain in that position until you reposition them. By moving the reposition outside the loop, we've freed up extra cycles in the kernel code for each scanline.
Positioning sprites to any arbitrary horizontal position is quite complex, and usually takes at least one whole scanline to do in a generic fashion. This is why games which use multiple sprites rarely allow those sprites to cross over each other, and also the reason why you see distinct 'bands' of sprites in other games—the gaps between the bands is where the horizontal movement code is doing its stuff.
Other Assembly Language Tutorials
Be sure to check out the other assembly language tutorials and the general programming pages on this web site.
Amazon: Atari 2600 Programming (#ad)
Amazon: 6502 Assembly Language Programming (#ad)
Atari 2600 Programming for Newbies (#ad)
|
|
|
Session 2: Television Display Basics
Sessions 3 & 6: The TIA and the 6502
Session 5: Memory Architecture
Session 7: The TV and our Kernel
Session 9: 6502 and DASM - Assembling the Basics
Session 14: Playfield Weirdness
Session 15: Playfield Continued
Session 16: Letting the Assembler do the Work
Sessions 17 & 18: Asymmetrical Playfields (Parts 1 & 2)
Session 20: Asymmetrical Playfields (Part 3)
Session 21: Sprites
Session 22: Sprites, Horizontal Positioning (Part 1)
Session 22: Sprites, Horizontal Positioning (Part 2)
Session 23: Moving Sprites Vertically
Session 25: Advanced Timeslicing
Disclaimer
View this page and any external web sites at your own risk. I am not responsible for any possible spiritual, emotional, physical, financial or any other damage to you, your friends, family, ancestors, or descendants in the past, present, or future, living or dead, in this dimension or any other.
Use any example programs at your own risk. I am not responsible if they blow up your computer or melt your Atari 2600. Use assembly language at your own risk. I am not responsible if assembly language makes you cry or gives you brain damage.