SNES programming tutorial. Example 7.
I made a simple Pong demo to show sprite collisions.
I made this with SPEZ version 2. Although SPEZ version 3 is out, it has different sprite code (see example in the SPEZ folder).
Well… I was trying to keep it simple, but I decided to use some of the more complicated code I have previously written. Copied to the library.asm file from some of the EasySNES files. OAM_Spr(copies one sprite to the buffer), OAM_Meta_Spr (copies multiple sprites to the buffer), oam_clear (clears the buffer), Map_Offset (gets an address from a specific x/y coordinate in a map). I did change the return from these functions from RTL to RTS, because all of our code is in the same bank.
I will discuss these functions further below.
Check_Collision is new. I will discuss that a bit later.
Let’s talk about the process of making this. I made a circle gradient in GIMP for the background, and converted to indexed 4 color (with dithering). Sized 256×192 (it won’t cover the entire screen).
Saved as a PNG. Imported to M1TE.
Then I drew some numbers for BG3, and filled a little on the top and bottom.
Clicked the priority checkbox for this map.
Saved all the maps and tiles and palette. Pretty much the same as previous examples of loading a background.
Now I opened SPEZ (my sprite editor) and drew some simple box shapes for the ball and paddle. Saved them as metasprites.asm and saved their tiles (chr) and palette.
Everything is .incbin -ed in the main.asm file. We are loading everything just like the previous examples, with DMAs to the VRAM. One difference is that I wrote a macro for DMAs to the VRAM. This made the code a little easier to read and write. Let’s look at an example…
DMA_VRAM $700, Map1, $6000
This is the DMA_VRAM macro definition…
.macro DMA_VRAM length, src_addr, dst_addr
;dst is address in the VRAM
;a should be 8 bit, xy should be 16 bit
stx $2116 ; vram address
sta $4300 ; transfer mode, 2 registers 1 write
; $2118 and $2119 are a pair Low/High
lda #$18 ; $2118
sta $4301 ; destination, vram data
stx $4302 ; source
sta $4304 ; bank
stx $4305 ; length
sta $420b ; start dma, channel 0
So where it says length, the macro will insert the $700 bytes (not $800, because the screen is only 224 pixels high, so I’m not filling the entire 256 pixel high map). Where it says src_addr, it replaces it with Map1. Where it says dst_addr, it replaces it with VRAM address $6000. All that code could be written in one line.
DMA_VRAM $700, Map1, $6000
Doesn’t this look nicer though? Simple. Elegant. Easy to read. Macros are your friends.
Everything between InfiniteLoop and, somewhere below that, jmp InfiniteLoop is the game loop. Every frame we wait till v-blank. Copy the OAM_BUFFER to the OAM. Print the score to the top of the screen. Read the controllers. Move the paddles if up or down are pressed.
lda pad1 and #KEY_UP beq @not_up @up: A8 lda paddle1_y cmp #$20 ;max up beq @not_up ;too far up bcc @not_up dec paddle1_y dec paddle1_y dec paddle2_y dec paddle2_y @not_up:
This code is moving both paddles, because this is just example code. You could modify it, so that controller2 moves the paddle on the right. Copy this whole thing, and replace pad1 with pad2, and only move paddle2. Also change the label names, so you don’t have duplicates.
We are only moving the ball while it is “active”. Press START to make it active, and choose a random direction to go (based on a frame counter).
ball_x_speed and ball_y_speed are the directions of the ball. Either 1 or -1 ($ff). Every frame we are adding the speed variable to the position variable. If speed is 1, we add 1 and it moves it to the right 1 pixel.
If the ball is active, it moves up/down until it reaches the ceiling or floor.
;bounce off ceilings
;bounce off floor
lda #$ff ; -1
It moves left/right until it reaches the end of the room. But we want it to bounce off the paddles, so we need to check collisions with hitboxes. I wrote this a long time ago (modified slightly). It’s the Check_Collision function in the library.asm file.
So we need the dimensions and location of the 4 sides of both boxes. That’s 8 numbers, that I copy to these variables…
obj1x, obj1w, obj1y, obj1h
obj2x, obj2w, obj2y, obj2h
x = left side of sprite object
w = width (minus 1), added to x to get the right side
y = top side of the sprite object
h = height (minus 1) , added to y to get the bottom side
I defined some of these with constants at the top of main.asm
BALL_SIZE = 7
PADDLE_W = 7
PADDLE_H = 47
Of course, the x and y values are changing. Those are defined as variables in the zero page (direct page).
I copy these to the obj1 obj2 stuff, and then call Check_Collision, which sets the “collision” variable to 0 or 1. If collision is true, we bounce the ball. This collision check is for 8 bit positions only, and assumes that no object goes off the screen at all. The code won’t work right at the very edges of the screen.
Here’s what the collision code is doing, under the hood, in some optimized ASM.
if((obj1_right >= obj2_left) && (obj2_right >= obj1_left) && (obj1_bottom >= obj2_top) && (obj2_bottom >= obj1_top)) return 1; else return 0;
Every frame I DMA the OAM buffer. Then I clear it with Clear_OAM and then rebuild it by writing to either OAM_Spr or OAM_Meta_Spr. The metasprites were made with SPEZ, and exported to the Sprites/metasprites.asm file. It’s a list all the sprites needed to make a metasprite.
The OAM_Meta_Spr function works like this.
Copy the x position to spr_x, the y position to spr_y, and then load A and X with the address of the metasprite data, and call our function. Remember ^ is for bank number. Like this.
lda #.loword(Meta_00) ;left paddle
And this will automatically put all the data in the OAM_BUFFER at the correct x and y positions. It also adjusts the high table bit shifting and keeps track of exactly how many sprites have been added (sprid).
*spr_x is 9 bits (uses 2 bytes). If the sprite never leaves the screen, just leave the upper byte of spr_x as zero. If you pass it more than 9 bits, it will ignore the extra bits.
The ball uses another function, OAM_Spr. This is for putting 1 sprite in the OAM BUFFER. You have to provide all the details of the sprite. Pass the x position to spr_x, the y position to spr_y, the tile # to spr_c, the attributes to spr_a, and set the size with spr_sz. spr_sz needs to be either 0 (small) or 2 (large). Then jsr OAM_Spr.
stz spr_sz ;8×8
If you are placing multiple balls on screen, all using the same palette, then you would only need to change the spr_x and spr_y before calling OAM_Spr again.
Writing to the background
The print_score function always runs during v-blank. It has to, because it is writing to the VRAM. That is why we do it as soon as possible after the jsr Wait_NMI.
I’m using this Map_Offset function (in library.asm) to get the VRAM address of the numbers in at the top of the screen. It wants you to load X with the tile’s x position 0-31 and load Y with the tile’s y position 0-31. If you only have pixel X and Y, just shift right (lsr a) 3 times to get the 0-255 value to 0-31 (tile) for 8×8 tiles.
Map_Offset does some bit shifting to convert that to a VRAM address. It returns A16 = the offset. You add that to the base address (our BG3 map is at $7000).
jsr Map_Offset ; returns a16 = vram address offset
adc #$7000 ;layer 3 map
sta VMADDL ;$2116
and then copying 2 values per number on screen (by writing to $2118-$2119). We are writing with the VRAM increment set to +32. That means that the second write will go below the first one.
sta VMAIN ;$2115
Some of these values might be hard to understand, like, why are we adding $10 to the points_L? Our tiles for numbers begins at $10.
Try the demo. Press START to get it going.
Try to make this into a game by having controller 2 to move the right paddle.
The ball is a bit slow, though. Moving 2 pixels per frame might be too fast. It would be best to use “fixed point” math, that’s a 16-bit variable for ball speed and position, where the upper byte refers to a pixel position, and the lower byte is a sub-pixel position (and speed). Then we could have 1 1/2 pixel per frame movement.
I wish we had some sound effects too. Maybe a little later for that.