When you have finished this tutorial, you will have written a small game.Disclaimer 1:
I have tested and made sure that all the example code will assemble and run properly on several emulators. However, I cannot guarantee the same results that I have had. You take full responsibility when you use my code. Nintendo and SNES are registered trademarks of Nintendo.Disclaimer 2:
This tutorial is written by Aceman2000. You can redistribute this file, but you have to leave this section unchanged. The original version can be found at Vintagedev. All other rights reserved.
Things you have to know before you start:
.include "header.inc" .include "initsnes.asm" .bank 0 slot 0 .org 0 .section "Vblank" ;-------------------------------------- VBlank: rti ;-------------------------------------- .ends .bank 0 slot 0 .org 0 .section "Main" ;-------------------------------------- Start: InitSNES forever: wai jmp forever ;-------------------------------------- .endsThis is my standard "Empty Code". If you'd like to test it, add these lines just after InitSNES:
sep #$30 ; get 8-bit registers stz $2121 ; write to CGRAM from $0 lda #%11101111 ; this is ldx #%00111111 ; a green color sta $2122 ; write it stx $2122 ; to CGRAM lda #%00001111 ; turn on screen sta $2100 ; hereIf you get a green screen, then it's all right! (delete these lines)
.bank 1 slot 0 ; We'll use bank 1 .org 0 .section "Tiledata" .include "tiles.inc" ; If you are using your own tiles, replace this .ends
rep #%00010000 ;16 bit xy sep #%00100000 ;8 bit ab ;See this? We take every byte from the palette, and put it to CGRAM ldx #$0000 - lda UntitledPalette.l,x sta $2122 inx cpx #8 bne - ;I'll explain this later ;We'll have two palettes, only one color is needed for the second: lda #33 ;The color we need is the 33rd sta $2121 lda.l Palette2 sta $2122 lda.l Palette2+1 sta $2122Note: -,--,---,+,++,+++ are special labels. The bne - branches to the nearest "-" backwards.
ldx #UntitledData ; Address lda #:UntitledData ; of UntitledData ldy #(15*16*2) ; length of data stx $4302 ; write sta $4304 ; address sty $4305 ; and length lda #%00000001 ; set this mode (transferring words) sta $4300 lda #$18 ; $211[89]: VRAM data write sta $4301 ; set destination ldy #$0000 ; Write to VRAM from $0000 sty $2116 lda #%00000001 ; start DMA, channel 0 sta $420BOkay, we're done with this. So, here it comes:
(this is not code) What to do: X|X|X Legend: -+-+- X: first empty tiles, then OX X|X|X |-+: Lines -+-+- X|X|XWe'll use the background (tile 0) for X, |-+ are tiles 2,4 and 6 respectively (16x16 tiles count as two.
lda #%10000000 ; VRAM writing mode
sta $2115
ldx #$4000 ; write to vram
stx $2116 ; from $4000
;ugly code starts here - it writes the # shape I mentioned before.
.rept 2
;X|X|X
.rept 2
ldx #$0000 ; tile 0 ( )
stx $2118
ldx #$0002 ; tile 2 (|)
stx $2118
.endr
ldx #$0000
stx $2118
;first line finished, add BG's
.rept 27
stx $2118 ; X=0
.endr
;beginning of 2nd line
;-+-+-
.rept 2
ldx #$0004 ; tile 4 (-)
stx $2118
ldx #$0006 ; tile 6 (+)
stx $2118
.endr
ldx #$0004 ; tile 4 (-)
stx $2118
ldx #$0000
.rept 27
stx $2118
.endr
.endr
.rept 2
ldx #$0000 ; tile 0 ( )
stx $2118
ldx #$0002 ; tile 2 (|)
stx $2118
.endr
After I wrote this, I realized that I could have used a table, then copy data from there,ldx #$6000 ; BG2 will start here stx $2116 ldx #$000C ; And will contain 1 tile stx $2118Note: BG2 uses colors 32 and 33 when in mode 0.
;set up the screen lda #%00110000 ; 16x16 tiles, mode 0 sta $2105 ; screen mode register lda #%01000000 ; data starts from $4000 sta $2107 ; for BG1 lda #%01100000 ; and $6000 sta $2108 ; for BG2 stz $210B ; BG1 and BG2 use the $0000 tiles lda #%00000011 ; enable bg1&2 sta $212C ;The PPU doesn't process the top line, so we scroll down 1 line. rep #$20 ; 16bit a lda #$07FF ; this is -1 for BG1 sep #$20 ; 8bit a sta $210E ; BG1 vert scroll xba sta $210E rep #$20 ; 16bit a lda #$FFFF ; this is -1 for BG2 sep #$20 ; 8bit a sta $2110 ; BG2 vert scroll xba sta $2110 lda #%00001111 ; enable screen, set brightness to 15 sta $2100 lda #%10000001 ; enable NMI and joypads sta $4200Phew, we're done! If you haven't done so, run wla, and test your program. You should see a
VBlank: Get controller input If it's the same as last time, rti Else, get the input (We have a delete key, an X placer key, and an O placer key, and up/down/left/right) If delete key, delete all data, then rti If X or O, then put the corresponding tile according to the cursor's location If up/down/left/right, move the cursor (and do not let it run out if the 3x3), then rti rtiThe SNES is very nice, it shadows $7E0000-$7E1FFF to bank $00 (see here), which means we can write $ABCD instead of $7EABCD,
The O's and X's: they are a 3x3 tile draft Our info in the RAM Info for the SNES in VRAM X|X|X $0000|$0001|$0002 $4000|$4002|$4004 (27 empty tiles here) -+-+- -----+-----+----- -----+-----+----- (here, too) X|X|X => $0003|$0004|$0005 => $4040|$4042|$4044 (and so on) -+-+- -----+-----+----- -----+-----+----- X|X|X $0006|$0007|$0008 $4080|$4082|$4084Cursor position:
We will store this in a straightforward format, like this: (0,0)|(1,0)|(2,0) Legend: -----+-----+----- (X,Y) (0,1)|(1,1)|(2,1) -----+-----+----- (0,2)|(1,2)|(2,2) This is just two bytes. We have plenty of space, so skip a little, and put them here: X: $0100 Y: $0101Controller input:
This is easy, just one byte: Place it to $0200, we will use $0201 as a temp valueRemember I said we'll put everything to the VBlank except two things? Here they come... :)
Our data
(0,0)|(1,0)|(2,0) ( 0, -1)|(-32, -1)|(-64, -1) Legend:
-----+-----+----- ---------+---------+--------- (X,Y)
(0,1)|(1,1)|(2,1) ( 0,-33)|(-32,-33)|(-64,-33)
-----+-----+----- ---------+---------+---------
(0,2)|(1,2)|(2,2) ( 0,-65)|(-32,-65)|(-64,-65)
Remember: the PPU doesn't process line 0.
Here's a formula: SNEScoord=-(32*Ourcoord) (if y coord, decrement by 1)
To calculate 32*Ourcoord, we have to shift left the data
5 times (X*2*2*2*2*2=X*32)
So, NOW we can code. Put this code after the first two .include lines:
.macro ConvertX ; Data in: our coord in A ; Data out: SNES scroll data in C (the 16 bit A) .rept 5 asl a ; multiply A by 32 .endr rep #%00100000 ; 16 bit A eor #$FFFF ; this will do A=1-A inc a ; A=A+1 sep #%00100000 ; 8 bit A .endm .macro ConvertY ; Data in: our coord in A ; Data out: SNES scroll data in C (the 16 bit A) .rept 5 asl a ; multiply A by 32 .endr rep #%00100000 ; 16 bit A eor #$FFFF ; this will do A=1-A sep #%00100000 ; 8 bit A .endmPut this code after wai but before jmp forever - we'll get fresh data this way.
rep #%00100000 ; get 16 bit A lda #$0000 ; empty it sep #%00100000 ; 8 bit A lda $0100 ; get our X coord ConvertX ; WLA needs a space before a macro name sta $210F ; BG2 horz scroll xba sta $210F ; write 16 bits ;now repeat it, but change $0100 to $0101, and $210F to $2110 rep #%00100000 ; get 16 bit A lda #$0000 ; empty it sep #%00100000 ; 8 bit A lda $0101 ; get our Y coord ConvertY ; WLA needs a space before a macro name sta $2110 ; BG2 vert scroll xba sta $2110 ; write 16 bits2.) Remember that the XO have to be copied to VRAM? This will be the second thing we do.
;put this after everything .bank 2 slot 0 .org 0 .section "Conversiontable" VRAMtable: .db $00,$02,$04,$40,$42,$44,$80,$82,$84 .ends ;write this after the conversion routine, just before jmp forever ldx #$0000 ; reset our counter - rep #%00100000 ; 16 bit A lda #$0000 ; empty it sep #%00100000 ; 8 bit a lda VRAMtable.l,x ; this is a long indexed address, nice :) rep #%00100000 clc adc #$4000 ; add $4000 to the value sta $2116 ; write to VRAM from here lda #$0000 ; reset A while it's still 16 bit sep #%00100000 ; 8 bit A lda $0000,x ; get the corresponding tile from RAM ; VRAM data write mode is still %10000000 sta $2118 ; write stz $2119 ; this is the hi-byte inx cpx #9 ; finished? bne - ; no, go backGood! Now if we write to the RAM, the SNES will obey, and do what we want!
What I did What I could have done
VBlank: VBlank:
Get controller input, Get controller input
if finished, rti if finished, jmp label
label:
do some conversions
rti rti
forever: forever:
wai wai
VBLANK OCCURS HERE
do some conversions
jmp forever jmp forever
When my program runs it's this order: wai-controllerinput-rti-conversion-loop.
When the other program runs it's this: wai-controllerinput-conversion-rti-loop.
If you leave out the rti, it's the same.
VBlank: lda $4212 ; get joypad status and #%00000001 ; if joy is not ready bne VBlank ; wait lda $4219 ; read joypad (BYSTudlr) sta $0201 ; store it cmp $0200 ; compare it with the previous bne + ; if not equal, go rti ; if it's equal, then return + sta $0200 ; store and #%00010000 ; get the start button ; this will be the delete key beq + ; if it's 0, we don't have to delete ldx #$0000 - stz $0000,x ; delete addresses $0000 to $0008 inx cpx #$09 ; this is 9. Guess why (homework :) ) bne - stz $0100 ; delete the scroll stz $0101 ; data also + lda $0201 ; get back the temp value and #%11000000 ; Care only about B and Y beq + ; if empty, skip this ; so, B or Y is pressed. Let's say B is O, ; and Y is X. cmp #%11000000 ; both are pressed? beq + ; then don't do anything cmp #%10000000 ; B? bne ++ ; no, try Y ; B is pressed, write an O ($08) ; we have to tell the cursor position, ; and calculate an address from that ; Formula: Address=3*Y+X lda $0101 ; get Y sta $0202 ; put it to a temp value clc adc $0202 ; multiply by 3 - an easy way adc $0202 ; A*3=A+A+A :) adc $0100 ; add X ; Now A contains our address ldx #$0000 ; be on the safe side tax lda #$08 sta $0000,x ; put $08 to the good address jmp + ; done with this ++ ; now for Y cmp #%01000000 ; Y? bne + ; no, jump forward (this should not happen) ; Y is pressed, write an X ($0A) lda $0101 ; get Y sta $0202 ; put it to a temp value clc adc $0202 ; multiply by 3 - an easy way adc $0202 ; A*3=A+A+A :) adc $0100 ; add X ; Now A contains our address ldx #$0000 ; be on the safe side tax lda #$0A sta $0000,x ; put $0A to the good address + ; finished putting tiles ; cursor moving comes now lda $0201 ; get control and #%00001111 ; care about directions sta $0201 ; store this cmp #%00001000 ; up? bne + ; if not, skip lda $0101 ; get scroll Y cmp #$00 ; if on the top, beq + ; don't do anything dec $0101 ; sub 1 from Y + lda $0201 ; get control cmp #%00000100 ; down? bne + ; if not, skip lda $0101 cmp #$02 ; if on the bottom, beq + ; don't do anything inc $0101 ; add 1 to Y + lda $0201 ; get control cmp #%00000010 ; left? bne + ; if not, skip lda $0100 cmp #$00 ; if on the left, beq + ; don't do anything dec $0100 ; sub 1 from X + lda $0201 ; get control cmp #%00000001 ; right? bne + ; if not, skip lda $0100 cmp #$02 ; if on the right, beq + ; don't do anything inc $0100 ; add 1 to X + rti ; F|NisH3D!