Jump to content
IGNORED

Racing The Beam!


wavemotion

Recommended Posts

Racing The Beam!

 

I’ve been working through Andrew’s fine tutorials here on AA for the past couple of weeks and was really struggling. The 6502 Assembly wasn’t the problem – having been a software engineer now for 20 years I’m finding the 6502 to be reasonably straightforward. But the TIA was so very difficult to wrap my head around and I didn’t really ‘get’ it until last night. I finally took a break from reading and re-reading Andrew’s tutorial to take a look at some of the source code that exists for the 2600 (from various homebrew authors that released code) and Kirk’s wonderfully straightforward Atari 2600 101 tutorial and then the light went on. I don’t think either tutorial would have done the trick by itself nor any single example of code, but the combination of the three caused something to click. My main problem was understanding both how and why the programmer must control game logic as the scanlines are being generated – performing all the key decisions before it came time for the TIA to actually draw the line… and also actually be able to predict how far along the scanline has been drawn to know exactly when to enable the Player, Missile, etc. to show properly (i.e. knowing when to kick off the display of a player sprite will allow the programmer to position this at a desired location horizontally on a line). It also became clear after seeing Kirk’s tutorial that a fair portion of the game logic takes place in the overscan and vertical blank period (all simple kernels up to that point simply did a series of WSYNC – but it seems in reality no game would ever waste all those cycles like that). This overscan and vertical blank period is great for reading joysticks, paddles, collision detection and other game logic that can be done before the next frame is drawn. I also finally understood that the player and missile graphics can be enabled for any scanline vertically – and that the careful reuse of those objects is important to creating games with more visible items than, say, pong or tank.

 

So, I finally felt comfortable writing my first kernel – based partially on Kirk’s joystick program. I made a kernel that displayed both missiles – Missile1 was controlled by the joystick and could roam the screen freely – bounded at the top and the bottom but allowed to pass from side-to-side. The Missile2 was stationary and was repeated 11 times vertically to prove I could “reuse” Missile2 and show multiple secondary objects on screen. I added collision detection so that when Missile1 touches any Missile2 it will change the screen color using a lookup table that will set the background to a different color depending on which of the 11 Missile2 blocks where hit (it took me almost no time to figure out how to determine which of the 11 Missile2 blocks were hit – now that I understood the TIA better, my normal assembly skills started to be more, um, useful!).

 

So, I got it all working… but noticed that when the Missile1 (which is controlled by the joystick) gets very near to the left side of the screen, part of the missile (which I setup as a square block) was chopped off. I couldn’t figure it out until I realized that I was performing too much logic before enabling the missile – the TIA had advanced just past the left side of the visible area so by the time I enabled the missile, it was already too late. I had lost the race with the beam!! – but was able to rework my code so that most of the logic came before the start of the next line – leaving only the simple decision as to turn on the missile(s). This was a huge learning experience for me and while I won’t pretend to be able to write a game next week, I think I may be over a significant hurdle. Of course I’m running out of cycles to do a lot more work – but I’ve already seen some hints as to how people avoid this – by processing different things on even/odd scanlines, using routines like skipdraw, etc. Hopefully that will be the next stage for me.

 

There was one thing in my (admittedly) simple program I didn’t quite understand. I added a TIM64T timing loop to the overscan so I could do some collision detection in those precious cycles. I did this by calculating:

 

30 scan lines x 76 cycles per line = 2280 cycles

 

Subtract off approx 14 cycles for the logic to setup the timer and I get 2266 cycles. Divided by the timer period of 64 and I get 35.41. I tried to program the timer for 35 and follow the expiration of the timer by a WSYNC (to finish the last line) but noticed Z26 reported one too few scanlines (261 as opposed to 262 which is what I expected). I increased the timer to 36 and it works fine.

 

; Start overscan here...

lda #2

sta WSYNC

sta VBLANK

 

lda #36 ; tbd – should be 35?

sta TIM64T

 

; Add more processing code here such as collision detection…

 

OverScanWait

lda INTIM

bne OverScanWait

sta WSYNC

 

jmp MainLoop

 

I was at a loss to explain why I needed 36 and not 35. If I add 5 NOPs before the loading of the TIM64T register, then it works fine with 35. So I’m thinking that the fractional .41 cycles is what’s causing my problem – if I understand the math right, 0.41 x 64 = 26.24 machine cycles that need to be chewed up. 26.24 machine cycles is 78 pixel clocks which is more than a single WSYNC will wait? Anyway, I’m confused here – anyone know what might be going on? I’ve got it to work, but I’d like to know why it works ;)

 

If I replace the above logic with 30 WSYNCs instead, it works out fine but of course that isn’t a viable solution in the end since I want to do some real logic processing during the overscan period and would rather use the timer approach.

 

Thanks much in advance!

Link to comment
Share on other sites

If you don't mind a suggestion from one hobbyist to another, use Stella during development if you're not already. Stella's debugger (hit the '~' key while you're running your program) is EXCELLENT for understanding what's happening with your code and when. It tracks the exact scanline you're on, shows you how many pixels clocks vs. CPU clocks have elapsed, allows you to peek inside the TIA to see what's going on, and even shows you a complete dump of RAM along with which locations have changed.

 

Stepping through a program in Stella's debugger can help you quickly spot where and how you're missing your deadline. If you pay enough attention, you can even see effects like partial playfield renders, or sprites being updated while displaying. Very cool. and very useful. :cool:

Link to comment
Share on other sites

  • 1 month later...

It sounds like you have figured out all the important stuff for yourself, which is always the best way to learn. It is always good to have more people in the 2600 scene, and I am looking forward to seeing your first programming efforts.

 

Chris

Link to comment
Share on other sites

Racing the beam is fun. One thing I've found, though, is that sometimes rather than worrying about trying to predict TMR64T values and cycle timings, it's easier to simply make a reasonably-close guess and be prepared to tweak things as needed. If my first attempt at a tMR64T value comes up 4 scan lines short, I'll increase the value by 5 and try again. Usually doesn't take many attempts to get such things right.

 

A few other things:

 

-1- I personally like to load TMR64T with about 125+the desired amount of delay. This makes it possible, when I'm doing a "big think", to do something like:

thinkloop47:
 bit INTIM
 bmi nokernel47
 jsr kernel
nokernel47:  

My kernel then waits for INTIM to count down to 125. Provided that I do the kernel check at least once every 128 cycles I don't have to worry about how long my 'big think' is going to take. If for some reason I don't have anything to "think" about, I can just call the kernel early.

 

Sometimes, for good measure, I'll include a check in my kernel to ensure that INTIM hasn't counted down to 125 before I got there. If it has, I may put up an error screen showing the two bytes on top of the stack. Those two bytes will point to the JSR kernel instruction that happened late.

 

In some cases, you may want to include certain game or animation logic within the kernel itself to allow it to run properly even during a "big think". Strat-O-Gems Deluxe does this. When a set of gems explodes, the pattern of gems on screen is stored to EEPROM (if one is present). This takes about 10 frames, but since the kernel is running the exploding-gem animation during that time it's completely invisible to the player.

 

-2- You'll certainly need a STA WSYNC sometime between when you enter the kernel and when you first do something that must occur at a fixed spot in the frame. It's best to include the STA WSYNC in your loop before the INTIM check. This will help you avoid certain nasty boundary contitions.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...