Jump to content

Photo

Session 23: Moving Sprites Vertically


41 replies to this topic

#1 Andrew Davie OFFLINE  

Andrew Davie

    Stargunner

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

Posted Mon Nov 17, 2003 8:11 PM

It's been a while since our last session, hasn't it!

This session we're going to have a prelminary look at vertical movement of sprites.

In the previous sessions we have seen that there are two 8-pixel wide sprites, each represented by a single 8-bit register in the TIA itself. The TIA displays the contents of the sprite registers at the same horizontal position on each scanline, corresponding to where on an earlier scanline the RESP0 or RESP1 register was toggled. We explored how to use this knowledge to develop some generic "position at horizontal pixel x" code which greatly simplified the movement of sprites in a horizontal direction.

Instead of having to work with the odd RESPx timing, we have abstracted that aspect of the hardware and now reference the sprite position through a variable in RAM, and our code positions the sprite to the pixel number indicated by this variable.

Let's now have a look at how to position a sprite vertically.

Our examples so far have shown how sprites appear as a vertical strip the entire height of the screen. This is due, of course, to the single byte of sprite data (8 bits = 8 pixels) being duplicated by the TIA (for each sprite) on each scanline. If we change the data held in the TIA sprite graphics registers (ie: in GRP0 or GRP1), then the next time the TIA draws the relevant sprite, we see a change in the shape that the TIA draws on-screen. We still see 8 pixels, directly under the 8 pixels of the same sprite on the previous scanline - but if we've changed the relevant GRPx register then we will see different pixels on (solid) and different pixels off (transparent).

To achieve vertical movement of "a sprite" - and by this, we mean a recognisable shape like a baloon, for example - we need to modify the data that we are writing to the GRPx register. When we're on scanlines where the shape is not visible, then we should be writing 0 to the GRPx register - and when on scanlines where the shape is visible, we should be writing the appropriate line of that shape to the GRPx register. Doing this quickly, and with little RAM or ROM usage, is the trickiest bit. Conceptually, it's quite simple.

There are several ways to tackle the problem of writing the right line of the shape on the right line of the screen, and nothing when the shape isn't on the line we're drawing. Some of them take extra ROM, some require more RAM, and some of them require more cycles per line.

Most kernels keep one of the registers as a "line counter" for use in indexing into tables of data for playfield graphics - so that the correct line of data is placed in the graphics registers for each scanline. The kernels we've created so far also use this line counter to determine when we have done sufficient lines in our kernel. For example...


    ldx #0   ;2

Kernel  lda PF0Table,x   ;4

    sta PF0   ;3

    lda PF1Table,x   ;4

    sta PF1   ;3

    lda PF2Table,x   ;4

    sta PF2   ;3

    sta WSYNC   ;3

    inx   ;2

    cpx #192   ;2

    bne Kernel   ;3(2)


The above code segment shows a loop which iterates the X register from 0 to 192 while it writes three playfield registers on each of the scanlines it 'generates'. We've covered all of this in previous sessions. The numbers after the semicolon (the comment area) indicate the number of cycles that instruction will take (not taking into account possible page-boundary crossing, etc). We can see that this simple symmetrical playfield modification will take at least 31 cycles of our available 76 cycles just to do the three playfield registers on each scanline. That leaves only 45 cycles to do sprites, missiles, ball -- and let's not forget the other three playfield writes if we're doing an asymmetrical playfield.

Clearly, our scanline loop is extremely starved of cycles, and any code we put in there must be extremely efficient. The biggest waste in the code above is the comparison. Remember earlier we indicated that the 6502 has a flags register, and some of these flags are set/cleared automatically after certain operations (on loads and arithmetic operations - including register increments and decrements, the negative and zero flags are automatically set/cleared). From now on we're going to use the 'standard' way of looping and instead of specifically comparing a line count with a desired value (eg: counting up to 192), we'll switch to starting at our top value and decrementing the line counter and branching UNTIL the counter gets to 0. By using our knowledge about the automatic flag setting, we are able to remove the comparison from our loop...


    ldx #192   ;2

Kernel  lda PF0Table,x   ;4

    sta PF0   ;3

    lda PF1Table,x   ;4

    sta PF1   ;3

    lda PF2Table,x   ;4

    sta PF2   ;3

    sta WSYNC   ;3

    dex   ;2

    bne Kernel   ;3(2)



The trick here is that the "dex" instruction will set the Z (zero) flag to 1 if the x register is zero after the instruction has executed, and 0 if it is non-zero. The "bne" instruction stands for "branch if Z is zero" or more memorably "branch if the result was not equal (to zero)". In short, the branch will be taken if the x register is non-zero. Thus we have removed two cycles from our inner scanline loop. But at what cost? Since the loop is now counting "down" instead of "up", our tables will now be accessed upside-down (that is, the first scanline will show data from the bottom of the tables), and our whole playfield will "flip" upside-down. That's fine - the solution for this is to change the tables themselves so they are upside-down, too!

All of that was a bit of a diversion - but it's important to understand how we are accessing our data in an upside-down fashion merely for the purposes of efficiency - in this case, saving us just 2 cycles per scanline. But those 2 cycles are some 2.6% of the time we have, and every little bit counts.

Even with this improvement, we have just 47 cycles left to do everything else. Let's have a look at what we need to add to this to get sprites up and running. Assume we are loading our sprite data from a table, just as with the playfield data. We'd need to add...


    lda Sprite0Data,x   ;4

    sta GRP0   ;3



That's 7 cycles, which is OK - but we find that we have an immovable (we have no ability to change the vertical position) block of sprite data the whole height of the screen - read from the table 'Sprite0Data'. This setup would also require that our sprite data table is 192 lines high.

Let's assume, just for a minute, that Sprite0Data was in RAM. Then we'd have the ability to use this kernel to do the display and have another part of our program draw different shapes into that RAM table (ie: if we were drawing a PacMan sprite, we could have the first 20 'lines' of the table with 0, then the next 16 lines with the shape for the pacman sprite, then the remainder with 0). To move this sprite up or down, we'd simply change where in the RAM table we were drawing the sprite - and when our kernel came to do the display, it wouldn't really care where the sprite was, it would just draw the continuous strip of sprite data from the RAM table, and voila! Vertically moving sprites.

And this is exactly how the Atari home computers manage vertical movement of sprites. They, too, have a single register holding the sprite data - and they, too, modify this register on-the-fly to change the shape of the sprite that is being shown on each scanline. But the difference is that the Atari computers have a bit of hardware which does EXACTLY what our little kernel above does - that is, copy sprite data from a RAM buffer into the hardware sprite register.

The problem for Atari 2600 kernels is that we simply don't have 192 bytes of RAM to spend on a draw buffer/table for each player sprite. In fact, we only have 128 bytes RAM total for our entire program! So it's a nice solution - and certainly one that should be used if you are programming for some cartridge format with ample RAM - because it provides extremely quick (7 cycles) drawing of sprites.

But for normal usage, this technique is not possible or practical.

Unfortunately, the available alternatives are costly - in terms of processing time. The quickest 'generic sprite draw' that I'm aware of at the moment takes 18 cycles. Given our 47 cycles remaining in the scanline, 36 of these would be taken up drawing just two sprites - and that makes asymmetrical playfield, balls and missiles a very problematic task. How can we fit all of these into the remaining 11 cycles of time?

The short answer is: we can't. And this is why many games revert to what is termed a "2 scanline kernel". Instead of trying to fit ALL of the updates into a single scanline, the 2 scanline kernel tries to fit all of the updates into two scanlines - taking advantage of the TIA's persistant state so that registers which have been modifed on one scanline will remain the same until next modified. A typical two scanline kernel will modify the playfield (left side), sprite 0, playfield (right side) on the first scanline, then the playfield (left side), sprite 1, playfield (right side) on the second scanline - and then repeat the process.

The upshot of this is that our sprites have a maximum resoution of two scanlines - that is, we can only modify the shape of a sprite once every two lines - and in fact each sprite is updated on alternate lines. There's a bit of hardware (a graphics delay of 1 scanline) to compensate for this, so that the sprites APPEAR to update on the same scanline. This interesting hardware capability shows clearly that the designers of the '2600 were well aware of the time limitations inherent in trying to update playfield registers, sprites missiles and ball in a single scanline - and that they designed the hardware accordingly to mask this problem.

But we're not concerned with two scanline kernels this session. Please be aware that they are extremely common - and many games extend this concept to multiple-scanline kernels - where different tasks are performed in each scanline, and after n scanlines this process repeats to build up the screen out of 'meta-scanlines'. It's a useful technique to get around the limitations of cycles per line.

Before we continue, let's have a think about what we want a sprite draw to do - it's fine to be able to display a sprite shape anywhere on the screen (we've already touched on the horizontal positioning, and now we're well on the way to understanding how the vertical positioning works) - but sprites typically animate. How can we use the code shown so far to animate our sprites as well?

If we used the Atari computer method - presented above - of using a 'strip' of RAM to represent the table from which data is written to the screen, and modifying the data written to that table, then the problem is fairly simple - we just write different shapes to the table. But if we don't HAVE a RAM table, and we're forced to use a ROM table, then to get different shapes onscreen, we're going to have to use different tables. We can't modify the contents of tables in ROM! But the code above has the table hardwired into the code itself. That is...


    lda Sprite0Data,x

    sta GRP0


The problem here is that the address of the table is hardwired at the time we write our code - and the assembler will happily predetermine where this table is in the ROM, and the code will always fetch the data from the same table. What we really want to do with a sprite routine is not only fetch the data from a table - but also be able to change WHICH table we fetch the data from.

And here is an ideal use for a new addressing mode of the 6502.


    lda (zp),y


In the above code, 'zp' is a zero page two-byte variable which holds a memory address. The 6502 takes the contents of that variable (ie: the address of our table), adds the y register to it, and then uses the resulting address as our location from which to load a byte of data. It's quite an expensive instruction, taking 5 cycles to execute.

But now our code for drawing sprites (in principle) can look like this...


    lda (SpriteTablePtr),y

    sta GRP0


The problem this introduces is that the Y register is used for indexing the data table, whereas we were previously using the X register. There's no way around this - the addressing mode does not work with the X register! So let's change our kernel around a bit, and instead of using the X register to count the scanlines, we'll switch to the Y register...


    ldy #192   ;2

Kernel 



    lda PF0Table,y   ;4

    sta PF0   ;3

    lda PF1Table,y   ;4

    sta PF1   ;3

    lda PF2Table,y   ;4

    sta PF2   ;3



    lda (SpriteTablePtr),y; 5

    sta GRP0; 3



    sta WSYNC   ;3

    dey   ;2

    bne Kernel   ;3(2)



This is a bit better - now (as long as we previously setup the zero page 2-byte variable to point to our table) we are able to display any sprite shape that we desire, using the one bit of code. Here's what you'd need to do to setup your variable to point to the sprite shape data...


    lda #<Sprite0Data

    sta SpriteTablePtr

    lda #>SPrite0Data

    sta SpriteTablePtr+1


Additonally, the variable should be defined in the RAM segment like this...


SpriteTablePtr ds 2


Now let's review all of that and make sure we understand exactly what is happening... We have a zero page variable (2 bytes long) which holds the address of the sprite table containing the shape we want to display. Addresses are 16-bits long, and we've already seen how the 6502 represents 16-bit addresses by a pair of bytes - the low byte followed by the high byte (little-endian order). So into our sprite pointer variable, we are writing this byte-pair. The '>' operator tells the assembler to use the high byte of an address, and the '<' operator tells the assembler to use the low byte of an address. These are standard operators, but there's another way to do it...


    lda #address&0xFF  ; low byte

    sta var

    lda #address/256   ; high byte

    sta var+1


Other ways exist. It doesn't really matter which one you use - the result is the same. We end up with a zero page variable which POINTS to the table which is used to give the data for the shape of the sprite. In fact, the variable points to the very start of the table.

And this is our new problem! As we have earlier seen, if we had a RAM table, then we could move the sprite up and down by drawing it into that table and let our kernel display the whole 'strip' of sprite data. The effect would be that the sprite moved up and down on screen. But because we don't have that much RAM, we must programatically determine on which scanline(s) the sprite data is to be displayed from the table, and which scanline(s) should contain 0-data for the sprite.

Essentially the process consists of comparing the current line-counter (the Y register) with the vertical position required for the sprite. If the counter comparison indicates that the sprite should be visible on the current scanline, then the data is fetched from the table - else a 0 value is used for the sprite data. Rather than stepping through the entire process and deriving the optimum result, we're going to just drop in the method used by nearly all games these days...




        sec                    ; 2  can often be guaranteed, and omitted



         tya                    ; 2

        sbc SpriteEnd          ; 3

        adc #SPRITE_HEIGHT     ; 2

        bcs .MBDraw3           ; 2(3)



        nop                    ; 2

        nop                    ; 2

        sec                    ; 2

        bcs .skipMBDraw3         ; 3



.MBDraw3

        lda (Sprite),y         ; 5

        sta GRP0      ; 3



.skipMBDraw3





Now here things start to get a bit complex! What the above code shows is a sprite draw routine which effectively takes a constant 18 cycles of time to either draw the sprite data from a table (when it's visible), or skip the draw entirely (when it's not visible). There are a few assumptions here...

1) The last drawn line of a sprite is always 0 - thus subsequent lines onscreen do not need to be 'cleared' - the persistant state of the TIA GRP registers will be sufficient to ensure the sprite is not displayed after the sprite is finished drawing

2) A variable 'SpriteEnd' is pre-calculated to indicate the starting line number of the sprite.

3) The sprite is of constant height (here, SPRITE_HEIGHT).

4) The branches in this code are assumed to NOT cross over page boundaries. If they did, then each would incur an additional cycle penalty - and the timing for the scanline would be incorrect.

So, that's a bit much to deal with in one whack - and to be honest you don't really need to understand the intricacies. Basically the code has two different sections - one where the sprite data is drawn from the table, and one where the draw is skipped. Each section is carefully timed so that after they rejoin at the bottom, they have both taken EXACTLY the same number of cycles to execute.

Thomas Jentzsch has presented more optimal code, in the form of his 'skipdraw' routine - and frankly, I've not bothered taking the time to fully understand how it works, either! These sections of code are pretty much guaranteed to work efficiently and correctly, provided you setup the variables properly.

I'd like to invite Thomas to insert his wonderful code here, and explain to all of us exactly how it works!

In the meantime, though we have covered a lot of ground today I hope you will understand the basic principles of vertical sprite movement. In summary...

1) There is no hardware facility to 'move' sprites either horizontally or vertically. To achieve horizontal motion, we need to hit RESPx register at exactly the right horizontal position in a scanline, at which point the appropriate sprite will start drawing. To achieve vertical motion, we need to adjust what data we feed to the GRPx registers, so that the shape we are drawing starts on the appropriate scanline, and scanlines where it is not visible have 0-data.

2) There are precious few cycles available on sanlines, and many of these are taken up by playfield drawing and loop management. Sprite drawing can be done efficiently with large RAM buffers, but most cartridge configurations don't offer this luxury.

3) Drawing animated sprites can be done efficiently by using an indirect zero-page pointer to point to sprite data tables. These tables can then be used as source for the sprite draw.

4) The sprite draw needs to determine, for each scanline, if the sprite would be visible on that line - and either take data from the correct table, or use 0-data.

5) Kernels can be extended to multiple-lines (at the cost of vertical resolution) to allow all the necessary hardware updates to be performed.

Exercises

1) Modify your current sprite drawing code to use a zero-page variable to point to a table of data in your ROM.

2) Create another data table, and use a variable to determine which of the two data tables to display. You might like to have it switch between these tables every second, or perhaps use the joystick button to determine which is displayed. As a hint - remember, you need to setup the zero page pointer to point to the table for your sprite draw to use - so all you need to do is change this pointer, and leave your kernel code alone.

3) THe more difficult task is to attempt to integrate the generic draw (either the code above, or Thomas's code, which should appear shortly) into your kernel. This is worth doing - and waiting for! - because once you have this installed, you'll have a totally generic kernel which can draw a sprite at practically any horizontal and vertical position on the screen and all you have to do is tell it WHERE to appear - and voila!

That should keep you busy. Enjoy!

#2 AtariYoungin OFFLINE  

AtariYoungin

    Moonsweeper

  • 269 posts

Posted Mon Nov 17, 2003 9:54 PM

YOUR BACK :) YAY!

#3 RCorcoran OFFLINE  

RCorcoran

    Dragonstomper

  • 526 posts
  • Location:Glendale, Arizona, USA

Posted Mon Nov 17, 2003 9:59 PM

WOOOHOOO!!!!

Time to do the Snoopy dance! :D

#4 Scuttle130 OFFLINE  

Scuttle130

    Star Raider

  • 73 posts

Posted Tue Nov 18, 2003 6:11 PM

Ahhh.. finally!!! Fights on!!! Andrew has the lead and we're back in the game!!!


Keep it coming ANDREW!!!

#5 Galaga_Freak OFFLINE  

Galaga_Freak

    Chopper Commander

  • 179 posts
  • Location:St. Louis

Posted Sun Jan 18, 2004 3:45 PM

Vert-23.zip: code and bin that shows vertical and horizontal sprite movement

HVPaddle.zip: Same but horizontal movement is controlled by paddle 0 and vertical by paddle 1

Attached Files



#6 Happy_Dude OFFLINE  

Happy_Dude

    River Patroller

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

Posted Fri Apr 16, 2004 1:42 AM

Vert-23.zip: code and bin that shows vertical and horizontal sprite movement

HVPaddle.zip:  Same but horizontal movement is controlled by paddle 0 and vertical by paddle 1

Sorry if this is a bit late but I just noticed this post
You have a part near the end of your code that looks like this
        sta WSYNC
        sta HMOVE
        sta WSYNC
        rts

Ret
	rts
Change it to this
        sta WSYNC
        sta HMOVE
        sta WSYNC
Ret   rts
And you save a byte


p.s That paddle demo is really helping me figure out the paddles :D Thanks

#7 vdub_bobby OFFLINE  

vdub_bobby

    Quadrunner

  • 5,831 posts
  • Boom bam.
  • Location:Seattle, WA

Posted Wed Nov 24, 2004 6:01 PM

Sorry if this is a bit late but I just noticed this post

Ha! I'll show you late!

Anyway, I just read through all the tutorial chapters in the last day or so and I was wondering...

That vert movement code above uses 18 machine cycles, right?

Well, instead of checking every line to see if you need to write the players/missiles/ball to the screen, why not do something like this...
;init vars at some point



Player0DataPtr ds 2

Player0YPos ds 2


; Vertical blank time, baby!



	;code to calculate Y-position of player goes here:



;  code 

;  code 

;  code



	clc  	

	lda Player0Data	;get location of data table

	adc LengthOfTable;make that the end of the table

  	;LengthOfTable >= 383 always



	sec  	

	sbc Player0YPos	;subtract Y-position offset

	sta Player0DataPtr;store in pointer



;etc


; Kernel time, baby!



BeginScanline



	sta WSYNC

	lda (Player0DataPtr),Y;5 machine cycles

  	;couple of notes:

  	;	1) assume using Y to count down scanlines

  	;	2) Player0DataPtr contains the pointer to

  	;    the player data table

	sta GRP0	;3 machine cycles - total of eight!

  	; (am I missing something?)



; rest of Kernel	



	dey

	bne BeginScanline



...

...



Player0Data

	.byte #%00000000

	.byte #%00000000

	...  ;# of lines of zeros=192-height of sprite

	...  ;for sprite able to move from tippy-top of screen

	...  ;to very bottom and no part of sprite allowed to

	...  ;move offscreen (have to check that every frame)

	.byte #%00000000	

	.byte #%00111000;here's the bottom of our sprite!

	.byte #%00111000;more sprite, etc.

	...

	.byte #%00111000;here's the top

	.byte #%00000000	

	.byte #%00000000	

	...  ;more zeros, same # as above

	...

	.byte #%00000000;end of data
I haven't actually implemented this code...so forgive any stupid syntax errors. But would this work?

#8 Cybergoth OFFLINE  

Cybergoth

    Quadrunner

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

Posted Wed Nov 24, 2004 6:40 PM

Hi there!

Yes, something like that certainly works. But it wastes zillions of bytes per sprite, so you'd do that only for very rare and special situations (Like maybe... a crosshair, for a game like maybe... Star Fire :) )

Greetings,
Manuel

#9 vdub_bobby OFFLINE  

vdub_bobby

    Quadrunner

  • 5,831 posts
  • Boom bam.
  • Location:Seattle, WA

Posted Wed Nov 24, 2004 7:08 PM

Yes, something like that certainly works. But it wastes zillions of bytes per sprite, so you'd do that only for very rare and special situations (Like maybe... a crosshair, for a game like maybe... Star Fire)


Thanks. Seems like if you didn't have many animation frames for sprites and wanted to update almost everything during every scanline it might be the way to go.

And there are ways to eliminate lots of the zeros: if you are only going to draw sprites that can move vertically in two-thirds of the screen, then that cuts a third of your zeros. Plus, if you stack the data tables, you can reuse most of the zeros (i.e., trailing zeros for SpriteFrame1 are leading zeros for SpriteFrame2 etc.)

And since at 8 cycles per sprite/missile/ball/playfield register, you could update all eight objects per scanline (64 cycles - am I counting right?) and still have time left over to do something else. Not much time, but a little :). Of course, you'd have to time things so that all the changes take place before the TIA reaches the horiz position for that object.

I mean, 18 cycles for each sprite vs. only 8? Like you said, that's gotta be useful sometime.

#10 Cybergoth OFFLINE  

Cybergoth

    Quadrunner

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

Posted Wed Nov 24, 2004 7:15 PM

Hi there!

I mean, 18 cycles for each sprite vs. only 8?  Like you said, that's gotta be useful <i>sometime</i>.


Sure. Like I said, I'm drawing a crosshair like that. I think Paul does the Marble Craze marble with this and Ben the tanks in Incoming.

Wether some 2600 programming technique is useful or not is always depending on the set of compromises you agree to go with...

Greetings,
Manuel

#11 Andrew Davie OFFLINE  

Andrew Davie

    Stargunner

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

Posted Thu Nov 25, 2004 7:31 AM

And there are ways to eliminate lots of the zeros:  if you are only going to draw sprites that can move vertically in two-thirds of the screen, then that cuts a third of your zeros.  Plus, if you stack the data tables, you can reuse most of the zeros (i.e., trailing zeros for SpriteFrame1 are leading zeros for SpriteFrame2 etc.)


This will hit you with an unpredictable 1-cycle page-boudary crossing hit. Well, it's predictable, but it's also going to be inconvenient -- especially if timing is tight. Like much '2600 programming, the technique is one of many ways to do the same thing differently. The Atari400/800 had a strip of ram for each of the objects, so all you effectively had to do was to draw the object into memory at the right place, and it would appear at the right place on the screen. Given sufficient RAM, there's no reason you couldn't do this...


    lda player0buffer,y

    sta GRP0

    lda player1buffer,y

    sta GRP1


That's just *6* cycles. Even better.

The thing is, these sorts of techniques were not practical in the limitations of 2K or 4K cartridges with 128 bytes of RAM. Given the possibility today of 512K cartridges, and especially lots of RAM, you can see how it would be possible to do amazingly sophisticated kernels with lots of sprites --- just because you could use the above for display and effectively draw sprites some 3 times quicker than possible with small cartridge size limitations.

A handy technique to put in the toolbox, though.

Cheers
A

#12 Aaron OFFLINE  

Aaron

    Chopper Commander

  • 105 posts
  • Location:Baltimore

Posted Fri Nov 26, 2004 1:18 AM

Well, instead of checking every line to see if you need to write the players/missiles/ball to the screen, why not do something like this...
[cut lots of code]

Actually Fall Down does that to draw the players - it was the only way I could find time to draw both sprites and a moving assymetric playfield. The code is only used for the width of a platform though (iirc 8 pixels), so only a little bit of padding is needed between sprites. Outside the platforms I used a normal skipdraw. Works out nicely. :)

Fall Down did have issues with page boundaries as Andrew described (the source is full of weird org statements), but this was mainly a result of some other crazy stuff I did...

#13 vdub_bobby OFFLINE  

vdub_bobby

    Quadrunner

  • 5,831 posts
  • Boom bam.
  • Location:Seattle, WA

Posted Mon Nov 29, 2004 12:14 PM

I have another question....a little more random:

Is it ever practical to use the stack to store sprite image data?

As in: during the vertical blank, push the data on the stack, then during the kernel, pull off your data.

Something like...

;Vertical Blank

;

	

PreparePlayerImageData	

	ldy YPosOffset

	lda Player0DataPtr,Y

	pha

	dey

	bne PreparePlayerImageData



; ...



;Kernel



	pla	;4 machine cycles

	sta GRP0;3 machine cycles



; etc.
The only reason I can think of to do this is if you just can't avoid crossing page boundaries when 'lda abs,Y'-ing your data but you can't afford the extra cycle in the kernel. If I understand this stuff correctly, using the stack this way would effectively move that extra cycle outside of your kernel.

Is this just stupid, or has anyone ever done this?

#14 Thomas Jentzsch OFFLINE  

Thomas Jentzsch

    Thrust, Jammed, SWOOPS!, Boulder Dash, THREE·S, Star Castle

  • 22,693 posts
  • Always left from right here!
  • Location:Düsseldorf, Germany, Europe, Earth

Posted Mon Nov 29, 2004 12:30 PM

Is this just stupid, or has anyone ever done this?

No, it isn't. Though you can easier avoid those penalties by aligning your data correctly.

Actually there are many more reasons:
1. timing (pha or lda tmp are faster than eg. lda(),y))
2. not needing X, Y
3. needing A for something different (ldx tmp)
...

Using sta/lda tmp instead of pha/pla saves one cycle.

#15 vdub_bobby OFFLINE  

vdub_bobby

    Quadrunner

  • 5,831 posts
  • Boom bam.
  • Location:Seattle, WA

Posted Mon Nov 29, 2004 1:12 PM

3. needing A for something different (ldx tmp)
...

Using sta/lda tmp instead of pha/pla saves one cycle.

I guess the situation I was thinking of was when you want to store multiple bytes of data - say, each scanline of a sprite that's 8 lines tall - and you don't want to use 8 variables for that.

And speaking of the stack, can you directly reference the memory in the stack? E.g.,

lda $0100

Thanks.

#16 Thomas Jentzsch OFFLINE  

Thomas Jentzsch

    Thrust, Jammed, SWOOPS!, Boulder Dash, THREE·S, Star Castle

  • 22,693 posts
  • Always left from right here!
  • Location:Düsseldorf, Germany, Europe, Earth

Posted Mon Nov 29, 2004 2:39 PM

And speaking of the stack, can you directly reference the memory in the stack?

Yes, actually stack and RAM are the very same 128 bytes.

So you don't save any variables by using the stack instead. Just maybe a few bytes of ROM.

#17 vdub_bobby OFFLINE  

vdub_bobby

    Quadrunner

  • 5,831 posts
  • Boom bam.
  • Location:Seattle, WA

Posted Mon Nov 29, 2004 4:33 PM

Yes, actually stack and RAM are the very same 128 bytes.

So you don't save any variables by using the stack instead. Just maybe a few bytes of ROM.

Hadn't realized that. Thanks. Not knowing that could have some disastrous consequences!

#18 supercat OFFLINE  

supercat

    Quadrunner

  • 6,401 posts

Posted Fri Jun 3, 2005 1:58 AM

Pitfall II actually uses a different method for vertical motion. Unfortunately, it's not likely to be available to most 2600 programmers, but it's interesting to know about.

The Pitfall II cartridge, rather than using a normal ROM chip, uses a custom chip with special hardware that automatically does the address calculation and table lookup. Software writes some special on-cartridge registers to select a sprite and set its vertical position, and can then simply read a special address to get the proper byte of data for that sprite. This makes possible effects that would be literally impossible with just straight code.

Unfortunately, I don't think anyone makes a cartridge that emulates the Pitfall II hardware, and the fact that the hardware is combined on the same chip as the ROM means it can't very well be used for other games (it might be somewhat interesting to design a cartridge which plugged into a 2600, and which would accept a Pitfall II cartridge that was plugged in, and which would use the Pitfall II cartridge with custom code; I wouldn't be surprised if Activision built such a thing for development. Unfortunately, even if such a thing worked, whatever game one designed would have to use the Pitfall II shapes.

#19 CabaretVoltaire OFFLINE  

CabaretVoltaire

    Space Invader

  • 44 posts

Posted Thu Oct 20, 2005 4:55 AM

What about something like this:



You split the screen up into sectors (say 8 rows of 24 scanlines). Then have a variable "SECTOR_TBL" where each bit is referring to one of these sectors.

For example if SECTOR_TBL is set to #%100000001 then there is a sprite in the top sector and in the bottom sector.

The kernel could look something like:

  lda #128 ;sector
  sta SECTOR;
MainDisplayLoop
  lda SECTOR
  and SECTOR_TBL  ;check if bit is set

  beq SectorContainsSprites   

SectorHasNoSprites
;do whatever you like for 24 scanlines
;decrement some counter
  bne SectorHasNoSprites
  
  jmp EndOfDisplayLoop

SectorContainsSprites
;check if sprites are on this scanline
;decrement some counter
  bne SectorContainsSprites

EndOfDisplayLoop
  lsr SECTOR
  bne MainDisplayLoop


I'm not sure about cheaply setting the SECTOR_TBL variable.
Once it is initially set I think that it would be easy to check if a sprite has left the sector it is in. But I think initially setting it all up would be expensive.

I'm pretty sure that someone has atleast considered a similar system before so thought I'd ask if it's worth pursuing.

It looks good to me as it would leave whole areas of the screen free to process other things..


[edit]

Fudged up a little crappy kernel that displays a sprite using this method. It's very dirty and doesn't display quite right (bottom scanline is cut off??) as I'm very much a beginner.

My implentation is awful but I think the idea is fine. What do the experts think?

The red area is where any sprite checking is taking place. In the grey areas I'm doing nothing at all really.

Attached Files


Edited by CabaretVoltaire, Thu Oct 20, 2005 2:10 PM.


#20 nmoog OFFLINE  

nmoog

    Space Invader

  • 44 posts

Posted Sat May 27, 2006 3:21 AM

Does anyone have any simple code for this Vertical Sprite Movement tutorial? I'm having some trouble figuring out the placement code. The tutorial says:
	ldy #192

Picture

   ;Check if we need to draw code here..
	sec
	tya
	sbc SpriteEnd
	adc #SpriteHeight
	bcs .MBDraw3
				
	nop
	nop
	sec
				
	bcs .skipMBDraw3
				
.MBDraw3
	lda (SpriteTablePtr),y; Load the data from "y"... is this right?
	sta GRP0
				
.skipMBDraw3
; End Vertical drawing
	
	sta WSYNC
	dey		
	bne TopScreen

The placement on the y axis is right, but it is not drawing my data - just solid a block. I think it is because the y value in the drawing bit will be the value of the y scan line, not the line of data to be pointed at. Am i right in thinking this, or does the new addressing mode here work differently or... something...

Thanks!

nmoog

#21 Andrew Davie OFFLINE  

Andrew Davie

    Stargunner

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

Posted Sat May 27, 2006 4:13 AM

Does anyone have any simple code for this Vertical Sprite Movement tutorial? I'm having some trouble figuring out the placement code. The tutorial says:

	ldy #192

Picture

  ;Check if we need to draw code here..
	sec
	tya
	sbc SpriteEnd
	adc #SpriteHeight
	bcs .MBDraw3
				
	nop
	nop
	sec
				
	bcs .skipMBDraw3
				
.MBDraw3
	lda (SpriteTablePtr),y; Load the data from "y"... is this right?
	sta GRP0
				
.skipMBDraw3
; End Vertical drawing
	
	sta WSYNC
	dey		
	bne TopScreen

The placement on the y axis is right, but it is not drawing my data - just solid a block. I think it is because the y value in the drawing bit will be the value of the y scan line, not the line of data to be pointed at. Am i right in thinking this, or does the new addressing mode here work differently or... something...

Thanks!

nmoog



Your assumption is correct. And a way to correct this is to adjust where the sprite data pointer points to, before the scanline loop, so that when the Y register is used as an index, you end up pointing to the correct data after all! When Y is at the first scanline you want to draw on, you want your pointer + Y to point to the first data byte of your sprite shape. In other words, subtract the starting line from the pointer before the loop starts. That should do it.

#22 nmoog OFFLINE  

nmoog

    Space Invader

  • 44 posts

Posted Sat May 27, 2006 6:55 AM

Hey Dragonstomper - thanks so much for these tutorials. Before Thursday I didn't even know there was such a thing as homebrew atari games (though I had thought about it - I chose DHTML as the medium!). I've been reading everything I can about the 6502 and atari programming and in just 2 days I'm already starting to lose friends! For some reason they seem to look down on cancelling on a Saturday night out to stay home trying to make pixels appear on an emulation of a 30 year old console... Pfft, i dunno!

Back to the question... You said "subtract the starting line from the pointer before the loop starts" - Do you mean every frame before the Picture label (or TopLines in the last tutorial) doing something like "SpriteTablePtr = SpriteTablePtr - SpriteEnd". How can I do that with pointers? I tried a bunch of combinations along the lines of:
lda SpriteTablePtr
sbc SpriteEnd
sta SpriteTablePtr
But as you can see, I'm not up to scratch with pointer subtraction!

Will there be any further tutorials? I've been eagerly following along, but I don't quite understand how the horizontal and vertical positioning code can play together. I've been looking at some other code, but as I'm new to this I only understand your kernel at the moment :) I'd really like to get a solid sprite-moving kernel happening.

#23 Thomas Jentzsch OFFLINE  

Thomas Jentzsch

    Thrust, Jammed, SWOOPS!, Boulder Dash, THREE·S, Star Castle

  • 22,693 posts
  • Always left from right here!
  • Location:Düsseldorf, Germany, Europe, Earth

Posted Sat May 27, 2006 7:20 AM

Hey Dragonstomper...

That's just his status. His name is Andrew. :)

...cancelling on a Saturday night out to stay home trying to make pixels appear on an emulation of a 30 year old console... Pfft, i dunno!

That's the right spirit! :thumbsup:

Back to the question... You said "subtract the starting line from the pointer before the loop starts" ...

:idea: Try something like this:
lda	 #<SpriteData+HEIGHT_SPRITE
	sec
	sbc	 yPosSprite   
	sta	 ptrSprite
SpriteData is the label for the sprite graphic data (which has to aligned the end of a page), HEIGHT_SPRITE is a constant depending on your sprite height, yPosSprite is the vertical postion of your sprite (0 at the bottom), ptrSprite is the LSB of the sprite pointer used inside the kernel.

#24 nmoog OFFLINE  

nmoog

    Space Invader

  • 44 posts

Posted Sat May 27, 2006 8:18 AM

That's just his status. His name is Andrew.

Whooops! Sorry Andrew (I didn't think there were any dragons in Tasmania)

I normally wouldn't give in this easily, but I can't figure out how the code Andrew explained would work - I got it working by loading the result from the adc #SpriteHeight line into the x register and using that. but i KNOW that has to be a bad way to do it.

Perhaps you can spot what I am doing wrong here. I had initialised the pointer using the code Andrew provided:
lda #<Sprite0Data; Point to the sprite data
sta SpriteTablePtr
lda #>Sprite0Data
sta SpriteTablePtr+1

But now am trying to do the pointer subtraction suggested, like so:

	lda #<Sprite0Data+SpriteHeight;get pointer to sprite data (plus height of sprite)
	sec					;set carry
	sbc SpriteEnd					; subtract the Y value
	sta SpriteTablePtr			 ; store ptr

Right before launching in to the code given above in my initial post. I still seem to not be pointing at the correct place when it gets to the required scanline. Any ideas?!

Thanks!

#25 nmoog OFFLINE  

nmoog

    Space Invader

  • 44 posts

Posted Sat May 27, 2006 5:42 PM

Well, a bunch of hours later and I cracked it! After re-reading you guys's help I realised I had missed the bit where Thomas had said "which has to aligned the end of a page". I added ORG ($FB00 + KernalHeight) before my sprite data and it worked.

Thanks again!




0 user(s) are browsing this forum

0 members, 0 guests, 0 anonymous users