2600 101
A tutorial by Kirk Israel
Version 1.1
Introduction
So you want to be an Atari Programmer, make that 2600 sing, dance, do tricks for you? What are you, some kind of masochist?
In case you haven't heard, programming a VCS is not easy. It's a very quirky beast, it has very little memory or other resources to work with, and your program has to hold the hand of the TIA television interface chip for (almost) every damn scanline... demanding a fairly detailed knowledge of what the TV's electron beam is doing as it draws the screen...and what it's doing (or at least for how long) when it's not drawing the screen. Not afraid? "You will be...you will be."
So why do it? There are a number of reasons:
- To connect in a fundamental way with the grandmaster of our misspent childhoods.
- The challenge of it all. Like writing haiku while drunk and on the back of a charging rhino.
- Actually, I hate challenges, but there's something cool about making a game out of such simple little pieces, kind of like Legos when they were all square, with maybe a few wheel pieces thrown in.
- To interact with something you made on a real live TV.
- The chicks.
- Ok, I'm kidding about the chicks.
- Why not program for other, more modern consoles? Despite (or because of) the difficulty of the 2600, there is a community and set of resources for it that I haven't seen for other systems you could potentially homebrew for. (YMMV)
- Why not just make games for Windows, in VB? Or a more powerful language? Or online, in Shockwave or Flash or Java? Yeah, go on, cry to mama you little wuss! But seriously: most successful modern games are big budget productions done by teams of people. Your program is likely to languish in shareware/freeware hell, ignored by millions, and competing for attention with thousands and thousands of other little games. Make a decent classic game, and you will get a lot of attention from the classic game collectors and their friends. You can even sell copies at shows like CVGE and PhillyClassic, and be duly admired in cash form. (If you work out the hourly rate for your programming efforts, you'd probably be better off flipping burgers, but that's not the point.)
UPDATE: I made this tutorial in Autumn 2002. I'm updating in the Spring of 2004, having a 2600 homebrew under my belt: JoustPong! I kept a development journal that you might wish to skim through if you want to know the trials, tribulation, and triumph of making a real 2600 game.
Information Resources
These resources are your friends. Theoretically, if you went through all of the following, you might not need this tutorial! (I hope to make it worth your while anyway.)
- The Stella mailing list. If you're serious about Atari programming, you should join this. This site has an archive going all the way back to October of 1996. More to the point, the people on the list seem pretty friendly to well meaning newbies. (Me being the case in point.) Check out the group's introduction for more details. You might want to consider checking out the new edition of The Dig, a high powered search engine of the site's archives. (A guy named Garon Grainger ran the Original Dig, which was quite an inspiration for me in the early days--now however it is sadly gone missing.)
- If you're not familiar with 6502 Assembly Language (though it seems that many people who get into this hobby are, I'm not one of them) you should check out Assembly In One Step (local mirror), a one (big) page guide to the language.
- The infamous Stella Programmer's Guide. This is the closest thing we have to a bible. All about the TIA chip and the special ways to coax her magic from her. Here I have a local link to an excellent HTML conversion by B. Watson. A little Googling should probably be able to locate it in PDF and other formats.
- AtariAge has become a huge supporter of homebrew efforts. From putting up "in development" pages, to running message board forums, to working with authors to get their games into hardware and working on real-live Ataris, to a great storefront, the site is a huge resource and I consider it THE online resource for Atari...heck, for almost all of classic gaming
2600 101: The Development Environment
There's nothing like getting your feet wet with compiling and playing some real Atari programs to work up your appetite for making your own works. This page will tell you how to do that on a PC running Windows. (There's nothing to stop you from doing this on a Mac, but that's beyond the scope of this tutorial.)
Step One: The Emulation
One cool thing about making Atari games is that you get to play Atari games. (Call it "research".) A number of great emulators are available free for Windows that you play nearly any Atari game ever made. Here are the three I've played with:
- StellaX. I found this one to be the easiest to use with a good Windows frontend, and with very good emulation.
- Z26. Some people consider this to be the canonical emulator...as Manuel Polik puts it, "Something running on the real thing and not with Z26 must be due to a bug on the real thing! :-)" It generally is run from the commandline. or you can try a front end like x26.
- PCAE is another emulator. Seems to have some nice debugging features, but it has a rather unfriendly, engineer-ish interface. On the other hand, it includes a debugger which can be very useful, though very difficult to use. That link is a local mirror, it's webpage has gone missing.
Once you've downloaded your emulator, you should test to see that it works on your system. This can be a very pleasant task for obvious reasons (assuming there aren't any glitches, and there weren't for me), and also will familarize you with the keyboard/joystick setups. StellaX makes this very easy for you, with the ROMs (files that end in .BIN) preinstalled when you unpack the program. For the most part these are homebrew carts. (You can also point StellaX to a different directory from the Options menu, if you want to organize your life differently.)
There are also old Atari ROMs floating around. AtariAge has many of these.
Step Two: The Compilation
An assembler is a program that takes the Assembly source code and makes it into a binary image that an emulator can run. DASM is the assembler of choice. (local mirror of V2.20.07). The file you want is dasm.exe in the DASM\bin\DOS inside the zip.
You are now ready to compile and run your first Atari program. We'll use Chris "Crackers" Cracknell's 2600 Digital Clock (from hell!!!, I'm sure he'd be quick to add.) Here is the source code for it. Either right click and save it as "clock003.asm" or cut and paste the code into notepad and save. (Don't use an editor like Word that will much up the textfile with all sorts of formatting crap.) Incidentally, the ".asm" isn't crucial, just a convention people follow.
Now we're really ready! I'm going to assume you saved it in the same directory as DASM, otherwise make the appropriate path changes when you enter the following line:
dasm clock003.asm -f3 -oclock.bin
The -f3 is crucial--I learned that the hard way, and had to get help from the Stella list. (It has to do with the format of the resulting binary file.) The other parts of this line should be obvious: assemble the file "clock003.asm", put the resulting file in "clock.bin".
If neccesary, move the .bin to the appropriate directory and run it from your emulator. You should see a big friendly 12:00 staring you in the face. And if you wait around, 12:01. (You can set the time using the joystick.)
Great! You've now assembled and ran your first Atari program. You're now mostly ready to assemble any source code that people post to the Stella list...but there's one big catch, and its name is "vcs.h". VCS.H is a file that sets up many constants that the Atari relies on, letting programmers use codes like "WSYNC" rather than ugly hex numbers like "$02". (One of the reasons I picked the clock example was because it made these definitions within the file itself instead of depending on an external VCS.H) This file should sit in the same directory as DASM. Historically there were a few versions of this file floating around, but the current standard can be downloaded from DASM's official homepage, the "Atari 2600 Support Files" Zip file. Here's a local copy of VCS.H.
Step Three
Now might be a good time to check out Adam Trionfo's Changing Atari VCS Graphics- The Easy Way (local mirror, or Google can get you an HTML version.) It's a painless way of learning how to hack the graphics in games like Space Invaders, and you'll be introduced to a few extra tools as well.
2600 101: Into The Breach
Now you should be ready to learn a bit more about what you're doing.
First off, you should consider strongly consider reading through "Assembly In One Step" if you don't know 6502 Assembly, and at least skim through the Stella Programmer's Guide (See our Information Resources section for those.) There's a lot that I'm not going to explain here--there's a lot I don't know enough to explain here!--and you'll likely find your self turning to these resources again and again.
Our Friend The Television
(This section is mostly just a rehash of the first few pages of the Stella Programmer's Guide)
A television works by aiming a stream of electrons at the screen, moving it quickly across the glass in front. (Bear with me here, Mr. Wizard I ain't.) It fires that stream left to right (from the viewer's perspective) across the screen for one "scan line", zips back to the left side, moves down, and does the next scan line. There are 192 visible scan lines on a TV. And your atari program has to tell the TIA (Television Interface Adaptor) what to put on each line, a splitsecond before it starts drawing that line. (Well, you can skip telling TIA what to do every other line to give your program more time to think...it will just make a copy of the line before. More on that later.)
But there's more to this process then that visible picture...thankfully! Otherwise you'd be spending almost all your meager computing power telling TIA what to draw, without the time to make the calculations for the rest of your game. There are four distinct "intervals" of what the electron beam (sounds very scifi, don't it?) is doing...five, in fact, when you realize there's a bit of time before the electron beam starts drawing each individual scanline. (that time is called the "horizontal blank")
So, life in the Atari/Television world is divided into "clock counts", 228 per scanline. Each "Machine Cycle" takes up 3 clock counts...and the actual assembly instructions take 2-6 machine cycles,so you don't have a lot of time to do computations.
Let me do my own version of that Stella Programmer's Guide diagram:
Here's what's going on. First, the CPU has to send the VSYNC. This message gets to the TV and tells it to get ready to start a new "frame", or picture. But you can't just hit it and quit it...the CPU has to wait for 3 whole scanlines for the Television to get the message. Luckily, waiting for a scanline to finish drawing is one of the things the Atari does best...you'll learn that when you "do a WSYNC", the CPU halts, and waits for the current scan line to finish. So an Atari Program turns on the VSYNC, does WSYNCs three times, the turns the VSYNC off. Now the TV's good to go.
Now the Atari enters the time known as the "Vertical Blank". The TV hasn't really begun to draw the picture yet...it won't for another 37 scanlines. In fact, the CPU needs to turn on a special "VBLANK" register that says there's no picture here. In general, there are two strategies for this time. The most obvious strategy is to just do another 37 WSYNCs. A better strategy (we'll learn the details of this in a bit) is to set one of the Atari's timers. That way we can do alot of the game logic we want (move things, check collisions, fiddle with memory, all the jazz) without having to know if a series of instructions fits in a particular scanline or not. We set a calculated amount into a timer, then once our program logic is done logic'ing, we do a "do-nothing" loop until the timer equals zero. Then we turn off VBLANK. And then we move on.
Time to start drawing...almost. For each line, you get about 22 Machine Cycles of "Horizontal Blank" to do some work...usually spent telling the TIA if the various players and missiles and playfields (oy, you'll find out them in a bit) are "on" for the upcoming line and then the electron starts drawing. Now, the Stella Programmer's Guide talks about how you can get extra time to do game calculations by only setting TIA every other line...and programmers who are really slick can do all sorts of logic on the fly, have everything ready to go for the TIA just in time, and still be home in time for dinner.
Now, from what I've seen, most programmers will count the # of scanlines, rather than use the timer trick I mentioned for the vertical blank...that's because of how important it is to tell the TIA "is this object (player, missile, ball) 'on' for this scan line".
So, it's a very different world from most computer programming, where you can just set pixels on the screen and they stay there. One saving grace is that the TIA holds the last values you set, so you don't have to redo every damn thing every damn line. Still, it's pretty challenging.
Once you've done the 192 lines (or so...sometimes the math gets a little crooked) you get another respite, the "overscan" period. This is just like the vertical blank, except its only 30 lines, not 37. So you should set the VBLANK it might make sense to do the timer trick again, and do some more program logic.
Once the overscan is finished, it's time to start over with the VSYNC. (Note that if you've turn on VBLANK at the start of the the overscan, you might as well leave it on throughout the vertical sync, since the vertical blank period needs it too.)
So that was a lot to get through, but believe me, we need to get a handle on this to have any idea what's going on as we start to look at even the simplest Atari programs.
By The Way: all of the above applies only to NTSC televisions, the video standard in the United States (and Canada, Mexico and Japan). There's also a video standard called "PAL" used in Europe and some other places. PAL has 312 scanlines, and refreshes a bit slower. (50 framers per second for PAL, 60 for NTSC). And the colors are a bit different. There are techniques for programming around these differences, but I'm not going to get into that anywhere in this tutorial.
Oh, and the diagram above is a bit of a lie...really, there's some "horizontal blank" time on either side of the visible image, the part you can see isn't really on the right side like that. But in practice, the view in the illustration is good enough.
2600 101: My First Program
Shortly we'll be ready to begin programming. Or rather I'll be ready to begin programming, and you should be at a stage where you're ready to follow along.
But, first, a few words about 6502 (actually 6507, but who's counting) assembly. I really urge you to take a look at "Assembly in One Step", but if you're not going to do that (or you already have and could use a review) here's what you need to know before looking at programs: There are three all important memory things (aka "registers") on this chip: the Accumulator (usually called A) that can hold 8 bits of information, and is involved in all the math the chip does, and X and Y each of which can also hold 1 byte of information. Some of the chip instructions can also access the pitiful 128 bytes of memory you get directly, but this is a bit slower. Also, you can't directly copy the value at a memory location ("address") to another memory location...instead, you need to first put it into one of the registers (putting a value into a register from memory is called "Loading") and then take it out of the register and put it into memory (called "Storing"). 6502 Assembly is then a bunch of these little commands that do tidbits like basic math functions, memory juggling, and program flow control. (At any given moment, the "instruction pointer" is pointing at an address in memory, where the next command to execute is located. It generally goes forward through memory, unless it reaches a branch (a small hop, usually based on checking if a condition is true) or a jump (a potentially bigger jump). You can also "Jump to Subroutine", and then "Return" (which uses this little thing called a stack) but many atari games will avoid doing that, at least in "time critical" parts, since it's slower than just putting the instructions right there, and it uses 2 bytes of memory for each level of jump you do. (Actually, subroutines can greatly add to the structure and readability of a program, but I'm not too good with them, so the examples here won't use them.)
Oh, by the way, you need to kind of know "hex notation", which is base 16 with the digits 0 through F (as opposed to base 10, which goes from 0 through 9. Base 16 is just like Base 10 if you have 6 extra fingers.) And to make matters worse, we usually record negative numbers in something called "two's complement notation"...you have to understand this. That's all I'm going to say about hex here, 'cause I'm lazy.
Andrew Davie points out that another potentially confusing thing you need to learn about are the "addressing modes". Most of the instructions do stuff with memory locations, and most of those memory locations can be expressed in different ways. (Not all instructions support all of the following modes)
mode name | example | what it does |
---|---|---|
Absolute addressing | lda $2000 | load a byte from location hexadecimal 2000 (8192 decimal) |
Zero-page addressing | lda $80 | load a byte from location $80 (128 decimal). This is the first byte of RAM |
Absolute indexed addressing | lda $2000,x | load a byte from location hexadecimal 2000 + x, where x is the 8-bit value in the X register |
Indirect indexed, y | lda (0),y | load a byte from location formed by the two bytes at 0,1 (in low, high format) added to the 8-bit value in the Y register |
Andrew Davie also wrote the following: Registers are 8-bit only. Loading and storing from anywhere is also 8-bit only. Addresses are 8 or 16 bits. 8-bit addresses are also known as 'zero page' because the high-byte of the (imaginary) 16-bit address for these is always 0. Accesssing 16-bit locations in other areas of memory (ie: not zero page) is done through pairs of bytes in low, high format. Indexing is the process of adding the contents of an index register (x or y) to an 8-or-16 bit address, and grabbing a byte from that location - which may also turn out to be either 8 or 16 bits. :)
In case you haven't guessed, the previous paragraphs were a brutally short summary of the 6502 and Assembly language in general. Now here's a similar hatchet job on the 2600 and its TIA (television interface adaptor) chip, a summary of the Stella Player's Guide you should have at least skimmed:
The 2600 has, graphically speaking, 5 "things" to play with: 2 "players", which can be semi-detailed graphical things like men or dragons or tanks or what not, 2 "missiles", each being a square associated with a player, and a "ball", which is a bit like a missile, but associated with the playfield. (Playfield?? You ask. I'm getting to that.) And there's the playfield, 20 pixels across (and as many as you want down) that can be the background or foreground of your game.
Also, of course, the Atari has ways of dealing with joysticks and sounds and what not, but we're not going to tackle all that yet. And there is even a (very small) set of things the TIA does that makes your life easier, like keeping track of what things are overlapping other things, so you know if the bullet has hit a man or if the man has bumped into a wall.
So, remember all that talk about scanlines on Into The Breach? Here's where it really comes into play. Before each scanline is drawn, you have to determine if a missile or ball is "on" for this line, and turn it on if so. You also have to determine if the "player" is on this line, and if so, what line of graphics should be drawn on this line to represent the player. All that's reasonably tough to do quickly...so quickly, we're not going to worry about it for our first test program. (And you may notice that most Atari games seem to have more characters than 2 players, 2 bullets, and an extra thingy...what's happening there is that these graphic "things" are being reused as the program loops through and draws the screen.)
Our first test program is called "Thin Red Line". It got its very start in Nick Bensema's How to Draw a Playfield, though his routine used that JSR (jump to subroutine)/RTS (return from subroutine) stuff, and I've made some other changes as well with an eye towards making as simple a program as possible. (You can follow that link to the original code Nick wrote if you're curious...it is a nice visual display.) I made at least three major mistakes when I first wrote Thin Red Line, 2 of which Thomas Jentzsch helped me correct. (The other one I got on my own).
Sidebar: Two's Complement Notation |
---|
Ok, let me try to explain this. In Two's Complement, the leftmost bit is 1 when the number is negative, otherwise 0. To get the negative of any number, flip all the bits (1 to 0, 0 to 1) and then add 1, ignoring the sign. So with 4 bits, 2 is "0010"; flip that and it's "1101", add 1 you get "1110". Note that if you flip bits and add one again, you get back to the original "0010" It seemed odd to me that the "biggest" value you could get in 4 bytes ("1111") would be 15, but it's -1, but think of a new car's speedometer going backwards. If would go from 0000 to 9999--seems like the biggest number, but really it's "-1". Well, if that (really weird) car's speedometer was in binary, it would roll back to "1111" instead. And the decimal car would then go to 9998, and our binary car goes back to 1110. It's a little weird but it works. |
I'm going to give you the short, uncommented version first, so when you see the commented version that follows you don't get too scared by its immensity:
; thin red line by Kirk Israel processor 6502 include vcs.h org $F000 Start sei cld ldx #$FF txs lda #0 ClearMem sta 0,X dex bne ClearMem lda #$00 sta COLUBK lda #33 sta COLUP0 MainLoop lda #2 sta VSYNC sta WSYNC sta WSYNC sta WSYNC lda #43 sta TIM64T lda #0 sta VSYNC WaitForVblankEnd lda INTIM bne WaitForVblankEnd ldy #191 sta WSYNC sta VBLANK lda #$F0 sta HMM0 sta WSYNC sta HMOVE ScanLoop sta WSYNC lda #2 sta ENAM0 dey bne ScanLoop lda #2 sta WSYNC sta VBLANK ldx #30 OverScanWait sta WSYNC dex bne OverScanWait jmp MainLoop org $FFFC .word Start .word Start
See, was that so bad? Of course you probably can't follow a word of it, so here is the fully explained version. You really should read through it, reading other people's code is a difficult but crucial learning technique, and this is about as heavily commented as you'll ever get.
; thin red line by Kirk Israel ; ; (anything after a ; is treated as a comment and ; ignored by DASM) ; ; First we have to tell DASM that we're ; coding to the 6502: ; processor 6502 ; ; then we have to include the "vcs.h" file ; that includes all the "convenience names" ; for all the special atari memory locations... ; include vcs.h ; ; now tell DASM where in the memory to place ; all the code that follows...$F000 is the preferred ; spot where it goes to make an atari program ; (so "org" isn't a 6502 or atari specific command... ; it's an "assembler directive" that's ; giving directions to the program that's going to ; turn our code into binary bits) ; org $F000 ; ; Notice everything we've done so far is "indented" ; Anything that's not indented, DASM treats as a "label" ; Labels make our lives easier...they say "wherever the ; next bit of code ends up sitting in physical memory, ; remember that location as 'labelname'. That way we ; can give commands lke "JMP labelname" rather than ; "JMP $F012" or what not. ; So we'll call the start of our program "Start". ; Inspired genius, that. Clever students will have ; figured out that since we just told DASM "put the ; next command at $F000", and then "Call the next memory ; location 'Start':, we've implicitly said that ; "Start is $F000" ; Start ; ; The next bit of code is pretty standard. When the Atari ; starts up, all its memory is random scrambled. So the first ; thing we run is "SEI" "CLD" and "TXS". ; Look these up if you want, ; for now know that they're just good things to cleanse ; the palette... sei ;Disable Any Interrupts (hey! comments can go on the side!) cld ; Clear BCD math bit. ldx #$FF ; set X to the top of the memory we have, txs ; ...and set the stack pointer to what X is...i.e. #$FF aka 255 ; ; Now the following is another pretty standard bit of code to start ; your program with..it makes a brief delay when your ; atari program starts, and if you're a hot shot you could consider ; zeroing out only the memory locations you care about, but ; for now we're gonna start at the top of memory, walk our way ; down, and put zeros in all of that. ; ; One thing you may notice is that a lot of atari programming ; involves starting at a number, and counting your way down ; to zero, rather than starting at zero and counting your ; way up. That's because when you're using a Register to ; hold your counter, it's faster/easier to compare that ; value to zero than to compare it to the target value ; you want to stop at. ; ; So X is going to hold the starting memory location ; (top of memory, $#FF)...in fact, it's already set to ; $FF from the previous instruction, so we're not going to ; bother to set it again...you see that kind of shortcut all ; the time in people's code, and sometimes it can be confusing, ; but Atari programs have to be *tight*. The "A"ccumulator is ; going to hold what we put into each memory location (i.e. zero) lda #0 ;Put Zero into A (X is at $FF) ClearMem sta 0,X ;Now, this may not mean what you think... dex ;decrement X (decrease X by one) bne ClearMem ;if the last command resulted in something ;that's "N"ot "Equal" to Zero, branch back ;to "ClearMem" ; ; Ok...a word of explanation about "sta 0,X" ; You might assume that that said "store zero into the memory ; location pointed to by X..." but rather, it's saying ; "store whatever's in the accumulator at the location pointed ; to by (X plus zero)" ; ; So why does the command do that? Why isn't there just a ; "STA X" command? (Go ahead and make the change if you want, ; DASM will give you an unhelpful error message when you go ; to assemble.) Here's one explanation, and it has to do with ; some handwaving I've been doing...memory goes from $0000-$FFFF ; but those first two hex digits represent the "page" you're dealing ; with. $0000-$00FF is the "zero page", $0100-$01FF is the first ; page, etc. A lot of the 6502 commands take up less memory ; when you use the special mode that deals with the zero page, ; where a lot of the action in atari land takes place. ; (some of those values are the special 'named ones' vcs.h names ; for us, that make the Atari's TIA chip do special stuff. ; Most of the rest you can use for your variables.) ; ...sooooo, STA $#nnnn would tell it to grab the next two bytes ; for a full 4 byte address, but this mode only grabs the one ; value from the zero page ; ; ; Now we can finally get into some more interesting ; stuff. First lets make the background black ; (Technically we don't have to do this, since $00=black, ; and we've already set all that memory to zero. ; But one easy experiment might be to try different two ; digit hex values here, and see some different colors ; lda #$00 ;load value into A ("it's a black thing") sta COLUBK ;put the value of A into the background color register ; ; Do the same basic thing for missile zero... ; (except missiles are the same color as their associated ; player, so we're setting the player's color instead ; lda #33 sta COLUP0 ; ; Now we start our main loop ; like most Atari programs, we'll have distinct ; times of Vertical Sync, Vertical Blank, ; Horizontal blank/screen draw, and then Overscan ; ; So every time we return control to Mainloop. ; we're doing another television frame of our humble demo ; And inside mainloop, we'll keep looping through the ; section labeled Scanloop...once for each scanline ; MainLoop ;*********************** VERTICAL SYNC HANDLER ; ; If you read your Stella Programmer's Guide, ; you'll learn that bit "D1" of VSYNC needs to be ; set to 1 to turn on the VSYNC, and then later ; you set the same bit to zero to turn it off. ; bits are numbered from right to left, starting ; with zero...that means VSYNC needs to be set with something ; like 00000010 , or any other pattern where "D1" (i.e. second ; bit from the right) is set to 1. 00000010 in binary ; is two in decimal, so let's just do that: ; lda #2 sta VSYNC ; Sync it up you damn dirty television! ; and that vsync on needs to be held for three scanlines... ; count with me here, sta WSYNC ; one... (our program waited for the first scanline to finish...) sta WSYNC ; two... (btw, it doesn't matter what we put in WSYNC, it could be anything) sta WSYNC ; three... ; We blew off that time of those three scanlines, though theoretically we could have ; done some logic there...but most programs will definately want the vertical blank time ; that follows... ; you might want to do a lot of things in those 37 lines...so many ; things that you might become like Dirty Harry: "Did I use up 36 scanlines, ; or 37? Well, to tell you the truth, in all this excitement, I've kinda lost track ; myself." So here's what we do...The Atari has some Timers built in. You set these ; with a value, and it counts down...then when you're done thinking, you kill time ; until that timer has clicked to zero, and then you move on. ; ; So how much time will those 37 scan lines take? ; Each scanline takes 76 cycles (which are the same thing our clock is geared to) ; 37*76 = 2812 (no matter what Nick Bensema tries to tell us...his "How to Draw ; A Playfield" is good in a lot of other ways though..to quote the comments from ; that: ; We must also subtract the five cycles it will take to set the ; timer, and the three cycles it will take to STA WSYNC to the next ; line. Plus the checking loop is only accurate to six cycles, making ; a total of fourteen cycles we have to waste. ; ; So, we need to burn 2812-14=2798 cycles. Now, there are a couple of different ; timers we can use, and Nick says the one we usually use to make it work out right ; is TIM64T, which ticks down one every 64 cycles. 2798 / 64 = 43.something, ; but we have to play conservative and round down. ; lda #43 ;load 43 (decimal) in the accumulator sta TIM64T ;and store that in the timer lda #0 ;Zero out the VSYNC sta VSYNC ; 'cause that time is over ; ; So RIGHT HERE we can do a ton of game logic, and we don't have ; to worry too much about how many instructions we're doing, ; as long as it's less than 37 scanlines worth (if it's not ; less, your program is a little screwed, and you need to ; write better time-tighter code, or put the code in the ; overscan or elsewhere. ; ;*********************** VERTICAL BLANK WAIT-ER WaitForVblankEnd lda INTIM ;load timer... bne WaitForVblankEnd ;killing time if the timer's not yet zero ldy #191 ;Y is going to hold how many lines we have to do ;...we're going to count scanlines here. theoretically ; since this example is ass simple, we could just repeat ; the timer trick, but often its important to know ; just what scan line we're at. sta WSYNC sta VBLANK ;End the VBLANK period with the zero ;(since we already have a zero in "A"...or else ;the BNE wouldn't have let us get here! Atari ;is full of double use crap like that, and you ;should comment when you do those tricks) ;We do a WSYNC just before that so we don't turn on ;the image in the middle of a line...an error that ;would be visible if the background color wasn't black. ;HMM0 is the "horizontal movement register" for Missile 0 ;we're gonna put in a -1 in the left 4 bits ("left nibble", ; use the geeky term for it)...it's important ;to note that it's the left 4 bits that metters, what's in the ;right just doesn't matter, hence the number is #$X0, where ;X is a digit from 0-F. In two's complement notation, -1 ;is F if we're only dealing with a single byte. ; ; Are you having fun yet? ; lda #$F0 ; -1 in the left nibble, who cares in the right sta HMM0 ; stick that in the missile mover sta WSYNC ;wait for one more line, so we know things line up sta HMOVE ;and activate that movement ;note...for godawful reasons, you must do HMOVE ;right after a damn WSYNC. I might be wasting a scanline ;with this, come to think of it ;*********************** Scan line Loop ScanLoop sta WSYNC ;Wait for the previous line to finish lda #2 ;now sticking a 2 in ENAM0 (i.e. bit D1) will enable Missile 0 sta ENAM0 ;we could've done this just once for the whole program ;since we never turn it off, but i decided to do it again and ;again, since usually we'd have to put smarter logic in ; each horizontal blank ; ;so at some point in here, after 68 clock cycles to be exact, ;TIA will start drawing the line...all the missiles and players ;and what not better be ready...unless we *really* know what ;we're doing. dey ;subtract one off the line counter thingy bne ScanLoop ;and repeat if we're not finished with all the scanlines. lda #2 ;#2 for the VBLANK... sta WSYNC ;Finish this final scanline. sta VBLANK ; Make TIA output invisible for the overscan, ; (and keep it that way for the vsync and vblank) ;***************************** OVERSCAN CALCULATIONS ; ;I'm just gonna count off the 30 lines of the overscan. ;You could do more program code if you wanted to. ldx #30 ;store 30 OverScanWait sta WSYNC dex bne OverScanWait jmp MainLoop ;Continue this loop forver! Back to the code for the vsync etc ; OK, last little bit of crap to take care of. ; there are two special memory locations, $FFFC and $FFFE ; When the atari starts up, a "reset" is done (which has nothing to do with ; the reset switch on the console!) When this happens, the 6502 looks at ; memory location $FFFC (and by extension its neighbor $FFFD, since it's ; seaching for both bytes of a full memory address) and then goes to the ; location pointed to by $FFFC/$FFFD...so our first .word Start tells DASM ; to put the binary data that we labeled "Start" at the location we established ; with org. And then we do it again for $FFFE/$FFFF, which is for a special ; event called a BRK which you don't have to worry about now. org $FFFC .word Start .word Start
Whoo! Now, what three mistakes could I have possibly made? (All of these were corrected in the code you see above.)
- I didn't read the SPG well enough, and kind of missed that WSYNC has to immediately precede HMOVE (Thanks Thomas)
- I didn't read the SPG well enough and was just trying to fiddle with the rightmost bits of HMM0 (Thanks again Thomas)
- I was kind of stupid, and tried to do an HMOVE right after the WSYNC for every scanline, not every tv picture frame! That's one I caught myself when what should've been a nice vertical line was broken up into a bunch of little jumpy bits.
You might want to go ahead and figure out how to change the speed of the line, as well as the color.
PS...it turns out I made FOUR mistakes...the fourth was I didn't do that additional WSYNC before turning off the VBLANK, so when I change the background todifferent colors you only see half of the top line. Thanks, yet again, Thomas.
2600 101: Kernal Clink
OK! We made it through our first program. Our second is going to be darn similar. Baby steps and all that.
The last program was a line, because that was about the easiest thing I could think to make, since we didn't have to do anything on every scanline. (Heck, we didn't even really have to do everything for every frame, except for the HMOVE.) So now we're going to make something just slightly more complex, a moving dot.
Why is a dot more complex than a line? Because we have to figure out when to turn the dot on, and when to turn it off. The code that you do during the scanlines (as opposed to during the Overscan or Vertical Blank) is called the "kernal" code, and it has to be tight, because you have very little time to do program logic. Sometimes, you might need 2 scanlines to do all your thinking, thus making a "two line kernal". (Though in that case, you have to be careful...since a lot of the kernal is setting up the graphical "things", if you change those things while the line is being drawn, weird things might result. Also wonderful things, if you're very careful with the timing... more careful than I've learned to be.)
Anyay, lets dive right into the code. I'll try to focus on commenting mostly on the new stuff, which I'll put in red for your reading convenience. (This means you should copy and paste from the web browser directly, not using "View Source"...and of course you still need the vcs.h file.)
Actually, we'll use one other file: "macro.h" is a file full of convenient "macros", or little bitty programs DASM runs as it puts your program together. It's a lot like vcs.h. The current standard can be downloaded from DASM's official homepage, in the "Atari 2600 Support Files" Zip file. Here's a local copy of macro.h.
; a moving dot by Kirk Israel processor 6502 include vcs.h include macro.h org $F000 ;we start by setting up two "variables" ;this means we tell DASM that when we say ;variablename, we mean this specific memory ;location (we have $80 to $FF to play with) ;we'll use this one to store the vertical position YPosFromBot = $80; ;more on the use of this variable below VisibleMissileLine = $81; Start ;generic start up stuff from macro.h... CLEAN_START lda #$00 sta COLUBK lda #66 ;Lets go for purpley! sta COLUP0 lda #80 sta YPosFromBot ;set Initial Y Position ;NUSIZ0 sets the size and duplication ;of the sprite and missiles --see the Stella ;guide for details lda #$20 sta NUSIZ0 ;Quad Width for now ;VSYNC time MainLoop lda #2 sta VSYNC sta WSYNC sta WSYNC sta WSYNC lda #43 sta TIM64T lda #0 sta VSYNC ;#% is a way of indicating a binary actual number ;(just like #$ starts a hex number and # a decimal number) lda #%00010000 ;put value of 1 in the left nibble (slow move right) sta HMM0 ;set the move for missile 0 WaitForVblankEnd lda INTIM bne WaitForVblankEnd ldy #191 sta WSYNC sta VBLANK sta WSYNC sta HMOVE ;main scanline loop... ScanLoop sta WSYNC ; here the idea is that VisibleMissileLine ; is zero if the line isn't being drawn now, ; otherwise it's however many lines we have to go ; there are more efficient ways of doing this ; we see if this is the line (line # stored in Y) is the ; one that we start the missile on CheckActivateMissile cpy YPosFromBot ;compare Y to the YPosFromBot... bne SkipActivateMissile ;if not equal, skip this... lda #8 ;otherwise say that this should go sta VisibleMissileLine ;on for 8 lines SkipActivateMissile ;turn missile off then see if it's turned on lda #0 sta ENAM0 ; ;if the VisibleMissileLine is non zero, ;we're drawing it ; lda VisibleMissileLine ;load the value of what missile line we're showing beq FinishMissile ;if zero we aren't showing, skip it IsMissileOn lda #2 ;otherwise sta ENAM0 ;showit dec VisibleMissileLine ;and decrement the missile line thing FinishMissile dey ;decrement scanline counter bne ScanLoop ;lather rinse repeat ;overscan same as last time lda #2 sta WSYNC sta VBLANK ldx #30 OverScanWait sta WSYNC dex bne OverScanWait jmp MainLoop org $FFFC .word Start .word Start
2600 101: The Joy of Sticks
So, moving forward...up until now, we've been making non-interactive demos. A well-coded, graphically impressive demo can be a joy to behold, but our demos haven't been well-coded or graphically impressive. Interaction, obviously, is the hallmark of programming the atari. The most common form of input is the beloved atari joystick. (The same 9-pin format was used for many other systems, such as the Commodore 64 and the Colecovision. (With some enhancements.) In fact, you can use a Sega Genesis controller in an Atari, using the center button for firing.
It turns out that the Atari's controller sockets can be used for input or output. (This might bring to mind the idea of making tons of funky little homebrew electronic interfaces controlled by your Atari. Needless is to say this page of tutorial is not going to get in that deep.) In fact, you can set pins individually for input or output.
The Atari joystick has 4 switches; pressing N,S,E, or W activates one switch, pressing diagonally activates two switches. Before trying to read the direction of the controller, you need to have set the correct I/O Port for "input". (Port A is used for the controllers, Port B for the console switches). You can do this explicitly by writing 0's to all the bits of the memory location SWACNT...or, if you do the "generic start up stuff" routing I've been using, it's one of the memory locations that will be zero'd out in that loop.
Reading the direction, then, is pretty easy. The memory location SWCHA is set as follows:
Data Bit | Direction | Player |
D7 | right | P0 |
D6 | left | P0 |
D5 | down | P0 |
D4 | up | P0 |
D3 | right | P1 |
D2 | left | P1 |
D1 | down | P1 |
D0 | up | P1 |
You may also be interested in whether the firebutton is pressed. Finding that out is easy, but with a few small potential gotchas. When the left joystick button is pressed, D7 (the leftmost bit, used for the sign of the number) of the memory location "INPT4" is set. (INPT5 for the other joystick.) The other bits of the locations are set to random values (well not really, but they probably won't be all zeros so you can't use BEQ/BNE--I was burned by that once) So typical code might look like
LDA INPT4 BMI ButtonNotPressed ;do something here because the button *is* pressed ButtonNotPressedThe other gotcha is that the behavior of this is modified by what you set D6 of our old friend VBLANK to...the safest behavior is to make sure D6 of VBLANK is 0...this is easily done if you just set VBLANK to #2 (%#00000010) when you start the vertical blank, since after that is when you'll likely be doing the button checking. (Thanks to Eckhard Stolberg and some other Stella-ites for some advice on the buttons.)
So, for today's program, we'll move yesterday's dot around. Just for kicks, we'll change the screen color when the button is pressed. (The color value will be the vertical position of the, so keep the button held while moving around.)
Again, new stuff is in red, and you might notice the only changes take place during the vertical blank...the kernal is the same as the last lesson's.
; move a dot with the joystick by Kirk Israel processor 6502 include vcs.h include macro.h org $F000 YPosFromBot = $80; VisibleMissileLine = $81; ;generic start up stuff... Start CLEAN_START lda #$00 sta COLUBK ;start with black background lda #66 sta COLUP0 ;Setting some variables... lda #80 sta YPosFromBot ;Initial Y Position lda #$20 sta NUSIZ0 ;Quad Width ;VSYNC time MainLoop lda #2 sta VSYNC sta WSYNC sta WSYNC sta WSYNC lda #43 sta TIM64T lda #0 sta VSYNC ;Main Computations; check down, up, left, right ;general idea is to do a BIT compare to see if ;a certain direction is pressed, and skip the value ;change if we're not moving that way ; ;Not the most efficient code, but gets the job done, ;including diagonal movement ; ; for up and down, we INC or DEC ; the Y Position lda #%00010000 ;Down? bit SWCHA bne SkipMoveDown inc YPosFromBot SkipMoveDown lda #%00100000 ;Up? bit SWCHA bne SkipMoveUp dec YPosFromBot SkipMoveUp ; for left and right, we're gonna ; set the horizontal speed, and then do ; a single HMOVE. We'll use X to hold the ; horizontal speed, then store it in the ; appropriate register ;assum horiz speed will be zero ldx #0 lda #%01000000 ;Left? bit SWCHA bne SkipMoveLeft ldx #$10 ;a 1 in the left nibble means go left SkipMoveLeft lda #%10000000 ;Right? bit SWCHA bne SkipMoveRight ldx #$F0 ;a -1 in the left nibble means go right... SkipMoveRight ;(in 4 bits, using "two's complement ; notation", binary 1111 = decimal -1 ; (which we write there as hex "F" -- ; remember?)) stx HMM0 ;set the move for missile 0 ; while we're at it, change the color of the background ; if the button is pressed (making sure D6 of VBLANK has ; appropriately set above) We'll set the background color ; to the vertical position, since that will be changing ; a lot but we can still control it. lda INPT4 ;read button input bmi ButtonNotPressed ;skip if button not pressed lda YPosFromBot ;must be pressed, get YPos sta COLUBK ;load into bgcolor ButtonNotPressed WaitForVblankEnd lda INTIM bne WaitForVblankEnd ldy #191 sta WSYNC sta VBLANK sta WSYNC sta HMOVE ;main scanline loop... ; ;(this probably ends the "new code" section of today's ; lesson...) ScanLoop sta WSYNC ; here the idea is that VisibleMissileLine ; is zero if the line isn't being drawn now, ; otherwise it's however many lines we have to go CheckActivateMissile cpy YPosFromBot bne SkipActivateMissile lda #8 sta VisibleMissileLine SkipActivateMissile ;turn missile off then see if it's turned on lda #0 sta ENAM0 ; ;if the VisibleMissileLine is non zero, ;we're drawing it ; lda VisibleMissileLine beq FinishMissile IsMissileOn lda #2 sta ENAM0 dec VisibleMissileLine FinishMissile dey bne ScanLoop lda #2 sta WSYNC sta VBLANK ldx #30 OverScanWait sta WSYNC dex bne OverScanWait jmp MainLoop org $FFFC .word Start .word Start
2600 101: Happy Face
So we've come a long way. But I think there's a small chance you'll want to make a game with better graphics than Pong. It's just a theory. So today, we get our "player" graphics mojo working.
Doing player graphics isn't that much more difficult than using the missile we've been using for the last few demos. The trick is that instead of a single on/off boolean value that says if the Atari is drawing the missile, we set a full byte, 8 bits, that make up the graphics for the player for that line. So not only do we have to count the lines and turn the player on or off, but we have to load GRP0 (or GRP1 for the second player) with the correct eight bit value: 1 means the pixel in that location (read left to right, like you'd expect) is on, 0 means it's off. So #%10001101 would have one pixel on, three off, two on, one off, and one on.
You change the color of the player about the same way you do for a missile, by setting COLUMP0 or COLUMP1 (the same register sets the color for a player and its associated missile.) Some of the fancier programs change the color of a player graphic beteen each scanline, given that distinctive horizontal-band colorization effect that the Atari is known for. (At least for people who pay attention to that kind of thing.) Also like Missiles, there are tricks to duplicate the player or make it wider...I'll let you look that up in the Stella guide.
I guess we might as well talk about color, 'cause I've been faking it up to now. (Unlike the rest of this fine tutorial, of course, which is completely unfaked.) B.Watson provides us with this handy chart:
Colors are most easily thought of as a two digit hex number. The left digit is the color, the right bit is the luminosity, or brightness. So to use this chart, find the hex digit for the color column you want, that's the first digit, then pick the luminosity going down, that's the right digit. So $76 is a medium blue. $0E is white (the rightmost bit of the luminosity doesn't matter, so $0E is the same as $0F) $D2 is a dark green. (This is all aproximate of course; different emulators and different TVs will display things differently, and this chart is only for NTSC TVs, not that crazy European PAL stuff)
One somewhat confusing thing is that (usually) Atari graphics are stored upsidedown in the program listing. This is because usually you have a positive number keeping track of how many lines are left to go as you're drawing the player, and this number is decreasing as you go through the scanlines. Combine that with the fact that the memory offset operation adds a number to the base memory location for the graphics, and it usually ends up making more sense to store the things bottom to top. You'll see in today's example.
One member of that tiny group "things the Atari does to make your life easier" is that if you set bit D3 of REFP0 (or REFP1) to 1, it gives you the mirror image of the player, so you don't have to have a seperate copy of the graphics if you want to have a thing moving the other way.
Another even more important thing "to make your life easier" is that the Atari can tell if any 2 of its 6 items (players (P), missiles (M), ball (BL), and playfield (PF)) have collided. There are 15 1-bit latches. (Do the math...the playfield could've hit any of the other 5 object, the ball could've hit any of the other 4 objects (we've already counted the playfield/ball collision) etc.... 5+4+3+2+1 = 15.)
Here is the relevant extra from the Stella guide:
6-bit Address | Address Name | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Function | D7 | D6 |
0 | CXM0P | 1 | 1 | . | . | . | . | . | . | read collision | M0 P1 | M0 P0 |
1 | CXM1P | 1 | 1 | . | . | . | . | . | . | read collision | M1 P0 | M1 P1 |
2 | CXP0FB | 1 | 1 | . | . | . | . | . | . | read collision | P0 PF | P0 BL |
3 | CXP1FB | 1 | 1 | . | . | . | . | . | . | read collision | P1 PF | P1 BL |
4 | CXM0FB | 1 | 1 | . | . | . | . | . | . | read collision | M0 PF | M0 BL |
5 | CXM1FB | 1 | 1 | . | . | . | . | . | . | read collision | M1 PF | M1 BL |
6 | CXBLPF | 1 | . | . | . | . | . | . | . | read collision | BL PF | unused |
7 | CXPPMM | 1 | 1 | . | . | . | . | . | . | read collision | P0 P1 | M0 M1 |
Animation is fairly simple...every time you want to show the next position of the guy's animation, just load the graphic from a different memory location that has the next "frame" of animation.
So, I think we're ready for todays example. We'll move a Happy Face player around the screen with the joystick. The code will be based on last lesson's "moving dot", changes in red. To liven things up, I'll throw the sweeping thin red line back in (but using missile 1 instead of missile 0) Every time it hits the player, the screen background will change to a value reflecting the vertical position of the happy face. We'll use a non-symmetrical happy face, giving a slight 3D effect, as well as letting us tell which way the player is facing.
; move a happy face with the joystick by Kirk Israel ; (with a can't'dodge'em line sweeping across the screen) processor 6502 include vcs.h include macro.h org $F000 YPosFromBot = $80; VisiblePlayerLine = $81; Start CLEAN_START lda #$00 ;start with a black background sta COLUBK lda #$1C ;lets go for bright yellow, the traditional color for happyfaces sta COLUP0 ;Setting some variables... lda #80 sta YPosFromBot ;Initial Y Position ;; Let's set up the sweeping line. as Missile 1 lda #2 sta ENAM1 ;enable it lda #33 sta COLUP1 ;color it lda #$20 sta NUSIZ1 ;make it quadwidth (not so thin, that) lda #$F0 ; -1 in the left nibble sta HMM1 ; of HMM1 sets it to moving ;VSYNC time MainLoop lda #2 sta VSYNC sta WSYNC sta WSYNC sta WSYNC lda #43 sta TIM64T lda #0 sta VSYNC ;Main Computations; check down, up, left, right ;general idea is to do a BIT compare to see if ;a certain direction is pressed, and skip the value ;change if so ; ;Not the most effecient code, but gets the job done, ;including diagonal movement ; ; for up and down, we INC or DEC ; the Y Position lda #%00010000 ;Down? bit SWCHA bne SkipMoveDown inc YPosFromBot SkipMoveDown lda #%00100000 ;Up? bit SWCHA bne SkipMoveUp dec YPosFromBot SkipMoveUp ; for left and right, we're gonna ; set the horizontal speed, and then do ; a single HMOVE. We'll use X to hold the ; horizontal speed, then store it in the ; appropriate register ;assum horiz speed will be zero ldx #0 lda #%01000000 ;Left? bit SWCHA bne SkipMoveLeft ldx #$10 ;a 1 in the left nibble means go left ;; moving left, so we need the mirror image lda #%00001000 ;a 1 in D3 of REFP0 says make it mirror sta REFP0 SkipMoveLeft lda #%10000000 ;Right? bit SWCHA bne SkipMoveRight ldx #$F0 ;a -1 in the left nibble means go right... ;; moving right, cancel any mirrorimage lda #%00000000 sta REFP0 SkipMoveRight stx HMP0 ;set the move for player 0, not the missile like last time... ; see if player and missile collide, and change the background color if so ;just a review...comparisons of numbers always seem a little backwards to me, ;since it's easier to load up the accumulator with the test value, and then ;compare that value to what's in the register we're interested. ;in this case, we want to see if D7 of CXM1P (meaning Player 0 hit ; missile 1) is on. So we put 10000000 into the Accumulator, ;then use BIT to compare it to the value in CXM1P lda #%10000000 bit CXM1P beq NoCollision ;skip if not hitting... lda YPosFromBot ;must be a hit! load in the YPos... sta COLUBK ;and store as the bgcolor NoCollision sta CXCLR ;reset the collision detection for next time WaitForVblankEnd lda INTIM bne WaitForVblankEnd ldy #191 sta WSYNC sta HMOVE sta VBLANK ;main scanline loop... ScanLoop sta WSYNC ; here the idea is that VisiblePlayerLine ; is zero if the line isn't being drawn now, ; otherwise it's however many lines we have to go CheckActivatePlayer cpy YPosFromBot bne SkipActivatePlayer lda #8 sta VisiblePlayerLine SkipActivatePlayer ;set player graphic to all zeros for this line, and then see if ;we need to load it with graphic data lda #0 sta GRP0 ; ;if the VisiblePlayerLine is non zero, ;we're drawing it now! ; ldx VisiblePlayerLine ;check the visible player line... beq FinishPlayer ;skip the drawing if its zero... IsPlayerOn lda BigHeadGraphic-1,X ;otherwise, load the correct line from BigHeadGraphic ;section below... it's off by 1 though, since at zero ;we stop drawing sta GRP0 ;put that line as player graphic dec VisiblePlayerLine ;and decrement the line count FinishPlayer dey bne ScanLoop lda #2 sta WSYNC sta VBLANK ldx #30 OverScanWait sta WSYNC dex bne OverScanWait jmp MainLoop ; here's the actual graphic! If you squint you can see its ; upsidedown smiling self BigHeadGraphic .byte #%00111100 .byte #%01111110 .byte #%11000001 .byte #%10111111 .byte #%11111111 .byte #%11101011 .byte #%01111110 .byte #%00111100 org $FFFC .word Start .word Start
Yikes, with all that joystick testing and graphics crap, our code listings are getting a bit long! Hopefully you're still able to follow along. And next lesson, I'll tell you why this kernal sucks...
2600 101: PlayerBufferStuffer
Congratulations! You've reached the last lesson of 2600 101, and it's a pretty easy one.
Last lesson I said I'd tell you what's wrong with the kernal I presented. If you run that, you might notice that when the happy face is on the left side of the screen, the top line of its head gets chopped off! Why is that? Well, it turns out that even our humble little computations were taking more time than we had...in particular, the CheckActivatePlayer section, where we initialize the variable VisiblePlayerLine with 8, pushes us into visible scanline time, and GRP0 doesn't get set in time
So what to do? This is where the fine art of Atari Kernal Tweaking comes into play. I'm not very good at it, but I'm pretty pleased with the idea I came up...I call it the PlayerBufferStuffertm, patent not pending. If we use a variable to store the player buffer, we can take the entire visible scanline time to do the computations we need for the next line, and then stuff that into the buffer. Then all we have to during the horizontal blank is grab that buffer variable from the last line and stuff it into GRP0.
The technique works pretty well, and can even be expanded for another player or the missiles. It has some disadvantages, like I can't think of it working for multicolored player graphics, but it has one important advantage: even a dumb guy like me can understand it. There are some other tricks that you can use that are game specific as well; for example, if you know one of the players is only going to be on the right side of the screen, you could take your time setting the Register for it, since it would be ok if it bled into the visible scan line.
It turned out to be really easy to make the change from the last example, as you can see by the lack of red below.
; move a happy face with PlayerBufferStuffer processor 6502 include vcs.h include macro.h org $F000 YPosFromBot = $80; VisiblePlayerLine = $81; PlayerBuffer = $82 ;setup an extra variable ;generic start up stuff... Start CLEAN_START lda #$00 ;start with a black background sta COLUBK lda #$1C ;lets go for bright yellow, the traditional color for happyfaces sta COLUP0 ;Setting some variables... lda #80 sta YPosFromBot ;Initial Y Position ;; Let's set up the sweeping line. as Missile 1 lda #2 sta ENAM1 ;enable it lda #33 sta COLUP1 ;color it lda #$20 sta NUSIZ1 ;make it quadwidth (not so thin, that) lda #$F0 ; -1 in the left nibble sta HMM1 ; of HMM1 sets it to moving ;VSYNC time MainLoop lda #2 sta VSYNC sta WSYNC sta WSYNC sta WSYNC lda #43 sta TIM64T lda #0 sta VSYNC ; for up and down, we INC or DEC ; the Y Position lda #%00010000 ;Down? bit SWCHA bne SkipMoveDown inc YPosFromBot SkipMoveDown lda #%00100000 ;Up? bit SWCHA bne SkipMoveUp dec YPosFromBot SkipMoveUp ; for left and right, we're gonna ; set the horizontal speed, and then do ; a single HMOVE. We'll use X to hold the ; horizontal speed, then store it in the ; appropriate register ;assum horiz speed will be zero ldx #0 lda #%01000000 ;Left? bit SWCHA bne SkipMoveLeft ldx #$10 ;a 1 in the left nibble means go left lda #%00001000 ;a 1 in D3 of REFP0 says make it mirror sta REFP0 SkipMoveLeft lda #%10000000 ;Right? bit SWCHA bne SkipMoveRight ldx #$F0 ;a -1 in the left nibble means go right... lda #%00000000 sta REFP0 ;unmirror it SkipMoveRight stx HMP0 ;set the move for player 0, not the missile like last time... ; see if player and missile collide, and change the background color if so lda #%10000000 bit CXM1P beq NoCollision ;skip if not hitting... lda YPosFromBot ;must be a hit! load in the YPos... sta COLUBK ;and store as the bgcolor NoCollision sta CXCLR ;reset the collision detection for next time lda #0 ;zero out the buffer sta PlayerBuffer ;just in case WaitForVblankEnd lda INTIM bne WaitForVblankEnd ldy #191 sta WSYNC sta HMOVE sta VBLANK ;main scanline loop... ScanLoop sta WSYNC lda PlayerBuffer ;buffer was set during last scanline sta GRP0 ;put it as graphics now CheckActivatePlayer cpy YPosFromBot bne SkipActivatePlayer lda #8 sta VisiblePlayerLine SkipActivatePlayer ;set player bufferto all zeros for this line, and then see if ;we need to load it with graphic data lda #0 sta PlayerBuffer ;set buffer, not GRP0 ; ;if the VisiblePlayerLine is non zero, ;we're drawing it next line ; ldx VisiblePlayerLine ;check the visible player line... beq FinishPlayer ;skip the drawing if its zero... IsPlayerOn lda BigHeadGraphic-1,X ;otherwise, load the correct line from BigHeadGraphic ;section below... it's off by 1 though, since at zero ;we stop drawing sta PlayerBuffer ;put that line as player graphic for the next line dec VisiblePlayerLine ;and decrement the line count FinishPlayer dey bne ScanLoop lda #2 sta WSYNC sta VBLANK ldx #30 OverScanWait sta WSYNC dex bne OverScanWait jmp MainLoop BigHeadGraphic .byte #%00111100 .byte #%01111110 .byte #%11000001 .byte #%10111111 .byte #%11111111 .byte #%11101011 .byte #%01111110 .byte #%00111100 org $FFFC .word Start .word Start
Incidentally, while I thought PlayerBufferStuffer was kind of clever, there is a well known better way of doing this called skipdraw. Ask about it on [stella], or look for it at the Dig, or... I'm hoping to assemble 2600 Cookbook page with lots of the collected wisdom of the [stella] list. skipdraw will be described there, along with lots of other neat stuff like playfields, music, smooth movement, etc etc...
You now know just about as much as I do about Atari programming, and I hope this tutorial has given you an easier time of it than I had. Check out the resources I list in the Introduction to continue your 2600 programming career... a whole world of kernal tweaking, graphical tricks, and gameplay ideas awaits you.