Jump to content

Photo

Session 21: Sprites


17 replies to this topic

#1 Andrew Davie OFFLINE  

Andrew Davie

    Stargunner

  • 1,583 posts
  • Dr.Boo
  • Location:Tasmania

Posted Wed Aug 20, 2003 9:59 PM

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, independantly 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, colour, 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 colour-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 colours 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 initialisation (before the main frame loop) the above code is initialising the colours of the two player sprites. These are random purplish colours. You may also change the colour 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 attached image.

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.

Exercises

[1] Modify the kernel so that the colour 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?

[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.

[3] What happens when you use more than 76 cycles on a line - how will this code misbehave?

See you next time!

Attached Thumbnails

  • kernel21.png

Attached Files



#2 Happy_Dude OFFLINE  

Happy_Dude

    River Patroller

  • 4,212 posts
  • Forum Slacker
  • Location:Sydney, Australia

Posted Wed Aug 20, 2003 11:13 PM

[1] Modify the kernel so that the colour 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?

You'd need to add "stx COLUP0 and stx COLUP1" to the middle lines
They take 3 cycles each adding 6 cycles to each line.
Each line would then be 55 cycles in length except for the last one
which would be 54 because the branch is not taken ;)

[3] What happens when you use more than 76 cycles on a line - how will this code misbehave?

The screen rolls.

#3 Andrew Davie OFFLINE  

Andrew Davie

    Stargunner

  • Topic Starter
  • 1,583 posts
  • Dr.Boo
  • Location:Tasmania

Posted Thu Aug 21, 2003 6:25 PM

More exercises...

[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?

[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?

#4 Dones OFFLINE  

Dones

    River Patroller

  • 2,273 posts

Posted Sat Aug 23, 2003 10:49 PM

[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?


The sprites spill over the border because we forgot to clean the GRP0/1 registers. The TIA gives priority to the sprites so that they are drawn on top of the playfield. I guess we'll learn to play with this as we plow ahead. To fix this we have to clear the GRP registers before drawing the top and bottom playfield bars.


[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?


In my case I moved the SLEEP and RESPx code after the setting of the PFx registers and just before the MiddleLines label. The player graphics shifted a little bit to the right. I guess this happened because some processing time was spent clearing the PFx registers.


This might be a bit obvious but I am going to ask anyway:

Stella Programmer's guide says:  

RESP0 (RESP1, RESM0, RESM1, RESBL)
These addresses are used to reset players, missiles and the ball. The object will begin its serial graphics at the time of a horizontal line at which the reset address occurs. No data bits are used


So storing a value in RESPx is only to set it off? Can I use any value for this then?

#5 AtariYoungin OFFLINE  

AtariYoungin

    Moonsweeper

  • 269 posts

Posted Sat Aug 23, 2003 11:13 PM

pretty sure thats how it works(in response to the last question). Believe its called a strobe register, so sending and value will trigger it.

#6 Andrew Davie OFFLINE  

Andrew Davie

    Stargunner

  • Topic Starter
  • 1,583 posts
  • Dr.Boo
  • Location:Tasmania

Posted Thu Aug 28, 2003 12:14 AM

[1] Modify the kernel so that the colour 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 colour register (eg: stx COLUP1), but it takes two or more additional cycles if you want to load a specific colour. The variation in time depends on the addressing mode you use to load the colour (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 synchronise 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 initialisation 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.

#7 antron OFFLINE  

antron

    Chopper Commander

  • 196 posts

Posted Mon Dec 1, 2003 2:20 PM

I noticed that the ball can be streched out 2, 4 or 8 pixels wide.
Is it possible to stretch it out 4, and then turn it off after three have been drawn, to make it just three wide?

#8 Thomas Jentzsch OFFLINE  

Thomas Jentzsch

    Thrust, Jammed, SWOOPS!, Boulder Dash

  • 18,909 posts
  • Always left from right here!
  • Location:Düsseldorf, Germany

Posted Mon Dec 1, 2003 2:24 PM

Is it possible to stretch it out 4, and then turn it off after three have been drawn, to make it just three wide?

Theoretically maybe, practically no.

#9 Ze_ro OFFLINE  

Ze_ro

    Quadrunner

  • 8,511 posts
  • Welcome Back!

Posted Fri Apr 23, 2004 2:24 PM

[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?  


The solution is to write 0 to GRP0 and GRP1 when we've finished drawing our sprites - and, of course, on initialisation of the system.


A lazier method might be to just set the playfield priority (by setting the 3rd bit of CTRLPF at the same time we choose a mirrored playfield) so that the playfield overlaps the sprites.

However, I guess this could cause problems later on with collision detection, since the sprite will constantly be colliding with the playfield, and collisions with other objects could then happen "underneath" the playfield.... but by the time that this is important, I imagine we'll only have GRP0/1 set for short periods anyways, rather than all the time.

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.


Is it possible to do the RESPx writes during VBLANK'ing or overscan? Or even during VSYNC? I don't think I've seen any code that actually does anything during VSYNC... is this a "special" time where the TIA should be left alone or is it fair game?

--Zero

#10 Ze_ro OFFLINE  

Ze_ro

    Quadrunner

  • 8,511 posts
  • Welcome Back!

Posted Fri Apr 23, 2004 4:23 PM

I hope I'm not jumping ahead, but I'm a little curious about the VDELPx registers... in the Stella guide, it says they are supposed to be used to delay the display of a sprite (or the ball, in the case of VDELBL) by one line... the idea being that you can use this to get scanline resolution even if you're only updating the screen every second line (ie, in order to gain an extra 76 cycles, we only update on odd scanlines, but we need to start a sprite on an even scanline on occaision, so we write a 2 into VDELP0 during VBLANK to force it to draw one scanline later). This makes complete sense to me.

My question is about what happens if you toggle this on and off on a per-scanline basis instead of per-frame (which is what I think they had in mind). From the "Vertical Delay" circuit they show on page 30, it seems that there are two seperate registers for each player, with one being for the delayed version of the player (Although I am unclear as to how this second register gets loaded... is the TIA always keeping a delayed copy whether I need it or not?), and a switch saying which should be displayed on that line.

So, if I write into GRP0, and then set VDELP0, it will delay that GRP0 data until the next line. However, if I change GRP0 during the horizontal blank, the TIA would then be holding two seperate copies of sprite data (for a single sprite!), and would display whichever was specified by VDELP0. If I time it right, can I change VDELP0 and hit RESP0 to get both bytes of sprite data to appear on the same line in different positions? Could I fiddle with COLUP0 to convince it to draw each in a different color? (ie, instead of trying to hit RESP0, just draw both sprites at the same position with two different colors in order to get 2-color sprites).

Even if this is possible, I'm guessing that it would be extremely tricky to time it properly to keep this "double sprite" going for more than one scanline, let alone adding a second sprite or missiles/balls. There's probably much more efficient ways to multiplex sprites, but this one seemed to stand out to me.

--Zero

#11 khryssun OFFLINE  

khryssun

    Dragonstomper

  • 835 posts
  • Location:lost somewhere on earth

Posted Sat May 1, 2004 12:04 PM

Andrew Davie wrote

[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.


Well, it's not Mario, but he's famous ;)

Posted Image




               processor 6502 

               include "vcs.h" 

               include "macro.h" 

                
;-------------------------------------------------------------
;-                    Start of code here                     -
;-------------------------------------------------------------



               SEG 

               ORG $F000 



Reset 


;-------------------------------------------------------------
;-   Clear RAM, TIA registers and Set Stack Pointer to #$FF  -         
;-------------------------------------------------------------

        

               SEI

               CLD	

               LDX #$FF

               TXS

               LDA #0

Clear_Mem

               STA 0,X

               DEX

               BNE Clear_Mem	



               LDA #$80        ; Blue

               STA COLUBK      ; Background Color

               LDA #$1E        ; Yellow     

               STA COLUP0      ; Player 0 Color

  
;-------------------------------------------------------------
;-                    Start to Build Frame                   -
;-------------------------------------------------------------    



Start_Frame 



         

      ; Start of Vertical Blank 



               LDA #2 

               STA VSYNC 



               STA WSYNC 

               STA WSYNC 

               STA WSYNC        ; 3 scanlines of VSYNC



               LDA #0 

               STA VSYNC            

                

       

      ; Start of Vertival Blank

      ; Count 37 Scanlines



               LDA  #43         ; 2 cycles

               STA  TIM64T      ; 3 cycles


;>>>> Free space for code starts here : 2793 cycles Free

               

               

       
;>>>> Free space for code ends here



Wait_VBLANK_End

               LDA INTIM                ; 3 cycles

               BNE Wait_VBLANK_End      ; 3 cycles

               STA WSYNC        ;3 cycles  Total Amount = 19 cycles

                                ; 2812-19 = 2793; 2793/64 = 43.64 (TIM64T)



               LDA #0 

               STA VBLANK      ; Enable TIA Output

                                

      

      ; Display 192 Scanlines with Player 0

        	

               SLEEP 36         ; Player X = +/- Middle Screen

               STA RESP0        ; Set Player 0 (X)  

               LDY #192         ; 192 Scanlines

               LDX #0

Picture  

               CPY #110         ; Position Y reached ?

               BPL No_Drawing   ; No = Continue

               CPX #14          ; 14 Lines of Sprite Datas Drawn ?

               BEQ No_Drawing   ; Yes = Stop

               LDA Sprite_Data,X                        

               STA GRP0                               

               INX

No_Drawing                        

               STA WSYNC        

               DEY              ; 192 Scanlines drawn ?

               BNE Picture      ; No = Continue

                


;-------------------------------------------------------------
;-                       Frame Ends Here                     -
;-------------------------------------------------------------

      

               LDA #%00000010 

               STA VBLANK       ; Disable TIA Output 





      ; 30 Scanlines of Overscan

        

               LDX #30 

Overscan        

               STA WSYNC 

               DEX 

               BNE Overscan 



               JMP Start_Frame  ; Build Next Frame 




;-------------------------------------------------------------
;-                         Demo Datas                        -
;-------------------------------------------------------------



Sprite_Data

               .byte #%00011000

               .byte #%00111100

               .byte #%01111110

               .byte #%01101110

               .byte #%11111111

               .byte #%11111000

               .byte #%11100000

               .byte #%11111000

               .byte #%11111111

               .byte #%01111110

               .byte #%01111110

               .byte #%00111100

               .byte #%00011000

               .byte #%00000000


;-------------------------------------------------------------
;-                     Set Interrup Vectors                  -
;-------------------------------------------------------------





               ORG $FFFA 



Interrupt_Vectors 



               .word Reset      ; NMI 

               .word Reset      ; RESET 

               .word Reset      ; IRQ 



       END 



#12 Serguei2 OFFLINE  

Serguei2

    Moonsweeper

  • 413 posts
  • Location:Canada

Posted Sat May 28, 2005 7:46 AM

Hi khryssun

I got '1 unresolved symbol' message when I compiled your code.

What's wrong?

#13 Cybergoth OFFLINE  

Cybergoth

    Quadrunner

  • 8,629 posts
  • This is Sparta!
  • Location:Bavaria

Posted Sat May 28, 2005 12:25 PM

Hi there!

Depends on the symbol. If it's NO_ILLEGAL_OPCODES then it's Ok.

Greetings,
Manuel

#14 Serguei2 OFFLINE  

Serguei2

    Moonsweeper

  • 413 posts
  • Location:Canada

Posted Sat May 28, 2005 2:11 PM

Hi Cybergoth


I see no NO_ILLEGAL_OPCODES in the khryssun's code. So why I still get the error?

And dasm does not show the error when I compile my programs.


Here the whole message when I compile khryssun's Pac-Man:

unresolved symbol list
NO_ILLEGAL_OPCODES
1 unresolved symbol



#15 Cybergoth OFFLINE  

Cybergoth

    Quadrunner

  • 8,629 posts
  • This is Sparta!
  • Location:Bavaria

Posted Sat May 28, 2005 2:33 PM

Hi there!

As I already said, that one is Ok. It comes from macro.h

Greetings,
Manuel

#16 Serguei2 OFFLINE  

Serguei2

    Moonsweeper

  • 413 posts
  • Location:Canada

Posted Sat May 28, 2005 3:43 PM

Thanks very much

#17 Sheldon Sims OFFLINE  

Sheldon Sims

    Chopper Commander

  • 111 posts
  • Location:San Fernando, Trinidad, West Indies

Posted Fri Aug 26, 2005 5:28 PM

[1] Modify the kernel so that the colour 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?


MultiColor.png

Changing the colors every scanline added 6 cycles to my kernel. The lines in my MiddleLines section are taking 51 cycles now.


[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.


LuigiMario.png

[3] What happens when you use more than 76 cycles on a line - how will this code misbehave?


Mario and Luigi were stretched, Mario is caught in the left/right border, and the screen flickers.

Edited by Sheldon Sims, Fri Aug 26, 2005 5:32 PM.


#18 Sheldon Sims OFFLINE  

Sheldon Sims

    Chopper Commander

  • 111 posts
  • Location:San Fernando, Trinidad, West Indies

Posted Fri Aug 26, 2005 6:16 PM

[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?


The sprites spill over the border because we forgot to clean the GRP0/1 registers. The TIA gives priority to the sprites so that they are drawn on top of the playfield. I guess we'll learn to play with this as we plow ahead. To fix this we have to clear the GRP registers before drawing the top and bottom playfield bars.


I just added the following code after my MiddleLines section and before my Bottom8Lines section:

   ldy #0; load 0 into accumulator
    sty GRP0; clear graphics in sprite 0
    sty GRP1; clear graphics in sprite 1

Since the GRP0 and GRP1 registers retain their settings until they are written to again, there is no need to repeat these changes for the Top8Lines section.

I guess I could have used lda and sta to avoid the unnecessary use of another register (Y).

Exercise21_4.png

[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?


In my case I moved the SLEEP and RESPx code after the setting of the PFx registers and just before the MiddleLines label. The player graphics shifted a little bit to the right. I guess this happened because some processing time was spent clearing the PFx registers.


Same here. My graphics just shifted to the right a little with this change.

Exercise21_5.png

Edited by Sheldon Sims, Fri Aug 26, 2005 6:29 PM.





0 user(s) are browsing this forum

0 members, 0 guests, 0 anonymous users