; ------------------------------------------------------------------------
; * Subject: Re: About SHIPS.ASM (was: Re: [stella] Jim's heart
; * From: Piero Cavina
; * Date: Wed, 17 Jun 1998 16:36:36 +0200
; ------------------------------------------------------------------------
; See original message post for details
; http://www.biglist.com/lists/stella/archives/9806/msg00088.html
; The Lifeboat game.
; by Kurt Woloch. Started on June 10th, 1997
; |
; Wow, did it take one year? ;-) ---------|
; Modified by Piero Cavina June 17h 1998
; Based on the Game&Watch "Lifeboat" by Nintendo.
; I came onto the idea to write this game when I saw that the game mentioned
; above was sold for $81 at eBay.
; Further pushing of this game idea occured through the film "Titanic".
; I was considering earlier to write a sound demo playing "My heart will go
; on" on the 2600.
; Finally, this game is dedicated to Jim who passed away on June 4th, as far
; as I know. I'm afraid he won't be around anymore to optimize this code,
; but I'll do my best to make it attractive in spite of that.
; His heart will go on in all of us...
;
; The objective of this game is to save as many ship-wrecked persons as you
; can by moving around with the lifeboat. The shipwrecked persons appear
; on the ship and jump down to land on the lifeboat, while the ship is
; slowly sinking - as the Titanic did way back then.
; Now, there's only room for four people maximum in the boat. To empty it
; again, you have to move it to the left or right of the screen, where your
; passengers will jump out of the boat, onto land. Now I know this has
; nothing to do with the Titanic, where there was no water around, but who
; cares...
;
; This game is based on the demo How to Draw A Playfield
; by Nick Bensema 9:23PM 3/2/97, but I don't know how much of this remained
; intact.
;
; OK, let's go to the code itself now. Sorry for my heavy commenting, which
; I know has been offended before on Stellalist, but if you don't want to
; read that, just skip it, or close the file!
;
; OK, now for the programming flow. Usually, it goes like this:
;
; Clear memory and registers
; Set up variables
; Loop:
; Do the vertical blank
; Do game calculations
; Draw screen
; Do more calculations during overscan
; Wait for next vertical blank
; End Loop.
;
; This source code is for compiling with DASM. Frankly, I don't care about
; any other compilers...
;
processor 6502
include vcs.h
; The vhs.h file here is a bit special. In fact, I didn't take this one
; anywhere from the list, but took the equates from a listing Distella
; generated...
org $F000
; OK, so this means the code starts at $F000, although in fact the first
; digit is more or less regardless, since I won't refer to any memory
; locations in the ROM by their address, but only by label.
; Now, here come the equates for the variables I use by now:
SinkCount = $80 ;This counts the frames until the ship goes down again by
;one line.
SinkLevel = $81 ;This counts the lines remaining from the ship display.
;At this time, I haven't got any men in here. I'll change that sometime...
Temp = $82
SoundCount = $83
DecayCount = $84
PlayfieldY = $90
SinkLinesL = $85
SinkLinesH = $86
;Now, some equates for colors. Since I plan to do a PAL version of this,
; they should be easy to alter.
White = $07
Red = $43
Pink = $47
Yellow = $17
Brown = $20
Skyblue = $96
Waterblue = $84
Shadowblue = $90
Start
; Now, we set up the CPU.
SEI ; Disable interrupts, if there are any.
CLD ; Clear BCD math bit.
LDX #$FF
TXS ; Set stack to beginning.
; Clear up the whole memory
; Since X is already loaded to 0xFF, our task becomes simply to count
; everything off.
;
LDA #0
INCLR1 STA 0,X
DEX
BNE INCLR1
; The above routine does not clear location 0, which is VSYNC. We will
; take care of that later.
;
; At this point in the code we would set up things like the data
; direction registers for the joysticks and such.
; In fact, I don't have the joystick in here yet. There's much more to
; come...
;
JSR GameInit
;
; Here is a representation of our program flow.
;
MainLoop
JSR VerticalBlank ;Execute the vertical blank.
JSR CheckSwitches ;Check console switches.
JSR GameCalc ;Do calculations during Vblank
JSR DrawScreen ;Draw the screen
JSR OverScan ;Do more calculations during overscan
JMP MainLoop ;Continue forever.
;
; It is important to maintain a stable screen, and this routine
; does some important and mysterious things. Actually, the only
; mysterious part is VSYNC. All VBLANK does is blank the TIA's
; output so that no graphics are drawn; otherwise the screen
; scans normally. It is VSYNC which tells the TV to pack its
; bags and move to the other corner of the screen.
;
; Fortunately, Nick's program sets VBLANK at the beginning of the
; overscan period, which usually precedes this subroutine, so
; it is not changed here - and I kept that here.
;
VerticalBlank
LDX #0
LDA #2
STA WSYNC
STA WSYNC
STA WSYNC
STA VSYNC ;Begin vertical sync.
STA WSYNC ; First line of VSYNC
STA WSYNC ; Second line of VSYNC.
;
; But before we finish off the third line of VSYNC, why don't we
; use this time to set the timer? This will save us a few cycles
; which would be more useful in the overscan area.
;
; To insure that we begin to draw the screen at the proper time,
; we must set the timer to go off just slightly before the end of
; the vertical blank space, so that we can WSYNC up to the ACTUAL
; end of the vertical blank space. Of course, the scanline we're
; going to omit is the same scanline we were about to waste VSYNCing,
; so it all evens out.
;
; Atari says we have to have 37 scanlines of VBLANK time. Since
; each scanline uses 76 cycles, that makes 37*76=2888 cycles.
; 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. 2888-14=2876.
; Aaargh, oops! Of course Nick's calculation is wrong here.
; 2888-14 is, of course, 2874. But who cares?
;
;
; We almost always use TIM64T for this, since the math just won't
; work out with the other intervals. 2880/64=44.something.
;
LDA #44
STA TIM64T
;
; And now's as good a time as any to clear the collision latches.
;
LDA #0
STA CXCLR
;
; Now we can end the VSYNC period.
;
STA WSYNC ; Third line of VSYNC.
STA VSYNC ; (0)
;
; At this point in time the screen is scanning normally, but
; the TIA's output is suppressed. It will begin again once
; 0 is written back into VBLANK.
;
RTS
;
; Checking the game switches is relatively simple. Theoretically,
; some of it could be slipped between WSYNCs in the VBlank code.
; But we're going for clarity here.
;
; Here, we check for the reset switch. Also, we set the background color,
; though this is a bit dirty, but who cares... Nick did it here too.
;
CheckSwitches
LDA SWCHB
AND #1
BNE NoReset
JSR GameInit
NoReset
LDA #Skyblue
STA COLUBK ; Background will be cyan.
RTS
;
; Minimal game calculations, just to get the ball rolling.
;
GameCalc
;
; OK, here we do the sinking. We'll decrease the sink count until it's 0,
; then we'll decrease the value that's left for the ship scanlines.
DEC SinkCount
BNE NoSinkNow
LDA #50
STA SinkCount
DEC SinkLevel
BNE NoSinkNow
LDA #112 ; ship-only height
STA SinkLevel
NoSinkNow
;Now we took care for making the ship sink, we play a little tune in here.
; What will James Horner think about this? If he ever hears THAT...
;Here we reduce the decay by 1. If it's zero, we pull everything back up,
; start the next note and increase the note counter.
DEC DecayCount
BNE DecayNotZero
;If the current note holds 0, no new note is set.
LDX SoundCount
LDA Sound0Freq,X
BEQ NoNote0
STA AUDF0
NoNote0
LDA Sound1Freq,X
BEQ NoNote1
STA AUDF1
NoNote1
LDA #16
STA DecayCount
LDA #15
STA AUDV0
STA AUDV1
INC SoundCount
RTS
;Now we take care of decay. The old note decays one beat before the new
; note is played, so we set the volume only if we don't encounter a zero
; in the current note, which means the note will be held.
; Note that this happens only if no new notes were set.
DecayNotZero
LDX SoundCount
LDA Sound0Freq,X
BEQ NoDecay0
LDA DecayCount
STA AUDV0
NoDecay0
LDA Sound1Freq,X
BEQ NoDecay1
LDA DecayCount
STA AUDV1
NoDecay1
RTS
;
; DrawScreen first waits for the timer, then for the next scanline.
;
DrawScreen
LDA INTIM
BNE DrawScreen ; Whew!
STA WSYNC
STA VBLANK ;End the VBLANK period with a zero.
;
; Now the actual playfield is drawn. As stated earlier, I use a reflected
; playfield here. Unlike Nick, I don't set the "score" bit here. In fact,
; I haven't seen much games where this is used.
;
LDA #1 ;Set the reflected playfield.
STA CTRLPF
; Now, in principle, the first eight scanlines should contain a 6-digit
; score counter, but I didn't include that here.
; So at the moment, when the ship is fully visible, there are 48 empty
; scanlines above it. This is reflected by the graphics data, containing
; 48 empty blocks. That's wasting 48 ROM bytes! Well, maybe I can improve on
; this...
; However, as the ship is sinking, we'll have to draw additional scanlines.
; We do this as follows: We put the remaining visible scanlines to X.
; Then we compare X to 164 (the total scanline number we do until the
; bottom of the ship) and do some WSYNC's until it's equal.
LDA #160 ;ship+sky height
SEC
SBC SinkLevel
TAX
SinkingLines
STA WSYNC
DEX
BNE SinkingLines
SinkingDone
; Initialize some display variables.
; Now we can start to draw the ship itself.
; For this purpose, we load the current ship line into y.
; Then we increment y every line until we reached the maximum lines.
LDA SinkLevel
AND #%00000011
STA SinkLinesL
LDA SinkLevel
LSR
LSR
STA SinkLinesH
BEQ DoneShip
LDY #0 ;We start at the first line
ShipLoop
STA WSYNC
LDA Forecolor,Y
STA COLUPF
LDA PF1Graphics,Y
STA PF1
LDA PF2Graphics,Y
STA PF2
STA WSYNC
STA WSYNC
STA WSYNC
INY
CPY SinkLinesH
BNE ShipLoop
DoneShip
LDA SinkCount
LSR
ORA #Waterblue
STA COLUPF
STA COLUBK
TAY
LDX SinkLinesL
DEX
BMI DoneShip1
STA WSYNC
DEY
DEY
STY COLUPF
STY COLUBK
DEX
BMI DoneShip1
STA WSYNC
DEY
DEY
STY COLUPF
STY COLUBK
DEX
BMI DoneShip1
STA WSYNC
DEY
DEY
STY COLUPF
STY COLUBK
DoneShip1
; Now, we have to draw the water, so we set the foreground to blue
; and do another 36 scanlines. (Did I count that right?)
STA WSYNC
LDA #Shadowblue
STA COLUPF
LDA #Waterblue
STA COLUBK
LDY #37
waterloop1
STA WSYNC
DEY
BNE waterloop1
;
; Clear all registers here to prevent any possible bleeding.
;
LDA #2
STA WSYNC ;Finish this scanline.
STA VBLANK ; Make TIA output invisible,
; Now we need to worry about it bleeding when we turn
; the TIA output back on.
; Y is still zero.
STY COLUBK
STY PF0
STY PF1
STY PF2
STY GRP0
STY GRP1
STY ENAM0
STY ENAM1
STY ENABL
RTS
;
; For the Overscan routine, one might take the time to process such
; things as collisions. I, however, would rather waste a bunch of
; scanlines, since I haven't drawn any players yet.
;
OverScan ;We've got 30 scanlines to kill.
LDX #30
KillLines
STA WSYNC
DEX
BNE KillLines
RTS
;
; GameInit could conceivably be called when the Select key is pressed,
; or some other event.
; Well, here's a little flaw. I'd rather put this at the BEGINNING of the
; code, but I'll leave it here for now. At the moment, this is only called
; when powering up the system (or starting the emulator).
; Here we'll set the ship to its full height (not sunk yet) and the sinking
; counter to its normal value.
;
GameInit
LDA #112 ;Full ship height is 116 scanlines+at least 48 above.
; ^^^ 112?
STA SinkLevel
LDA #50 ;Ship goes down one scanline every 50 frames.
STA SinkCount
LDA #14 ;Bring audio volume near maximum
STA AUDV0
STA AUDV1
LDA #12 ;Store distortion value 12 for Channel 0 (vocal)
STA AUDC0
LDA #1 ;and value 1 for Channel 1 (bass). Sorry I couldn't
STA AUDC1 ;do the other instruments, but the 2600 only has got
LDA #16 ;2 channels!
STA DecayCount ;Do another note every 16 frames.
RTS
org $FD00
; This is the sound data.
; Data for Channel 1 (Distortion 12 - Vocal voice)
; A 0 means that the note is held on...
Sound0Freq
.byte 16,0,0,16,16,0,16,0,17,0,16,0,0,0,16,0 ;Every night in my dreams, I
.byte 17,0,16,0,0,0,14,0,12,0,0,0,14,0,0,0 ;see you, I feel you.
.byte 16,0,0,16,16,0,16,0,17,0,16,0,0,0,14,0 ;That is how I know you go
.byte 22,0,0,0,0,0,0,0,0,0,0,0,24,22,19,17 ;on.
.byte 16,0,0,16,16,0,16,0,17,0,16,0,0,0,16,0 ;Far across the distance, and
.byte 17,0,16,0,0,0,14,0,12,0,0,0,14,0,0,0 ;spaces between us,
.byte 16,0,0,16,16,0,16,0,17,0,16,0,0,0,14,0 ;you have come to show you go
.byte 22,0,0,0,0,0,0,0,0,0,0,0,24,22,19,17 ;on.
.byte 16,0,0,0,0,0,0,0,14,0,0,0,0,0,22,0 ;Near, far, where
.byte 10,0,0,0,11,0,12,14,14,0,0,0,12,0,11,0 ;ever you are, I be-
.byte 12,0,0,0,14,0,16,0,17,0,16,0,0,0,17,0 ;lieve that the heart does go
.byte 19,0,0,0,19,0,0,0,19,0,0,0,24,22,19,17 ;on.
.byte 16,0,0,0,0,0,0,0,14,0,0,0,0,0,22,0 ;Once more, you
.byte 10,0,0,0,11,0,12,14,14,0,0,0,12,0,11,0 ;open the door, and you're
.byte 12,0,0,0,14,0,16,0,17,0,16,0,0,0,16,0 ;here in my heart and my
.byte 17,0,16,0,0,0,14,0,12,0,0,0,14,0,0,0 ;heart will go on and...
;And now, the bass line (Distortion: 1)
Sound1Freq
.byte 13,0,0,17,13,0,0,0,17,0,0,23,17,0,0,0
.byte 19,0,0,27,19,0,0,0,17,0,0,23,17,0,0,0
.byte 13,0,0,17,13,0,0,0,17,0,0,23,17,0,0,0
.byte 19,0,0,27,19,0,0,27,19,0,0,27,19,0,17,0
.byte 13,0,0,17,13,0,0,0,17,0,0,23,17,0,0,0
.byte 19,0,0,27,19,0,0,0,17,0,0,23,17,0,0,0
.byte 13,0,0,17,13,0,0,0,17,0,0,23,17,0,0,0
.byte 19,0,0,27,19,0,0,27,19,0,0,27,19,0,17,0
.byte 15,0,0,20,15,0,0,0,17,0,0,23,17,0,0,0
.byte 19,0,0,27,19,0,0,0,17,0,0,23,17,0,17,0
.byte 15,0,0,20,15,0,15,0,17,0,0,23,17,0,17,0
.byte 19,0,0,27,19,0,0,27,19,0,0,27,19,0,17,0
.byte 15,0,0,20,15,0,0,0,17,0,0,23,17,0,0,0
.byte 19,0,0,27,19,0,0,0,17,0,0,23,17,0,17,0
.byte 15,0,0,20,15,0,15,0,17,0,0,23,17,0,17,0
.byte 19,0,0,27,19,0,19,0,17,0,0,23,17,0,17,0
;
; Graphics are placed so that the extra cycle in the PFData,X indexes
; is NEVER taken, by making sure it never has to index across a page
; boundary. This way our cycle count holds true.
;
org $FF00
;
; This is the tricky part of drawing a playfield: actually
; drawing it. Well, the display routine and all that binary
; math was a bit tricky, too, but still, listen up.
;
; By the way, this game works with a REFLECTED playfield.
;
; In PF0 and PF2, the most significant bit (bit 7) is on the RIGHT
; side. In PF1, the most significant bit is on the LEFT side. This
; means that relative to PF0 and PF2, PF1 has a reversed bit order.
;
; PF0 | PF1 | PF2
; 4 5 6 7|7 6 5 4 3 2 1 0|0 1 2 3 4 5 6 7
;PFData0 ; 4 5 6 7
; .byte $00,$f0,$00,$A0,$A0,$E0,$A0,$A0
;PFData1 ; 7 6 5 4 3 2 1 0
PF1Graphics
.byte $00,$00,$00,$03,$03,$13,$13,$1F
.byte $1F,$1F,$1F,$7F,$7F,$7F,$7F,$5B
.byte $7F,$7F,$7F,$2B,$3F,$3F,$3F,$15
.byte $1F,$1F,$1F,$0F
;PFData2 ; 0 1 2 3 4 5 6 7
PF2Graphics
.byte $80,$80,$80,$C0,$C0,$C3,$F3,$F3
.byte $F3,$FF,$FF,$FF,$FF,$FF,$FF,$B6
.byte $FF,$FF,$FF,$B6,$FF,$FF,$FF,$B6
.byte $FF,$FF,$FF,$FF
; .byte $00,$FF,$00,$EE,$A2,$A2,$A2,$E2
;PFLColor ; Ship color stripes
Forecolor
.byte $07,$07,$07,$43,$43,$47,$47,$47
.byte $17,$17,$17,$43,$07,$43,$20,$20
.byte $20,$20,$20,$20,$20,$20,$20,$20
.byte $20,$20,$20,$20
;PFRColor ; Background color stripes (every 8 scanlines!)
; .byte $80,$80,$80,$80,$80,$80,$80,$80
; .byte $80,$81,$82,$83,$84,$85,$86,$87
; .byte $77,$76,$75,$74,$73,$72,$71,$70
; .byte $70,$70,$70,$70,$70,$70,$70,$70
org $FFFC
.word Start
.word Start