Jump to content

MRN

Member
  • Posts

    18
  • Joined

  • Last visited

  • Feedback

    0%

Gaming IDs

  • XBox Gamertag
    MariosNut

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

MRN's Achievements

Peasant

Peasant (1/20)

15

Reputation

  1. I'm looking to purchase "in bulk" to take chunks out of my lists. Please PM me with what you have, and hopefully we'll be able to work stuff out! I'm still missing quite a few of each library, so you'll probably have something I don't. Thanks for looking.
  2. Very nice. If you come up with a list of N64 and SNES stuff you're looking to unload, let me know. I am looking to cross a few off my list.
  3. It kind of depends on the person, I guess. If it's a game or something that I don't really care about, or something that would cost a bunch to upgrade, who cares if you have 1 or 2 or 3 that are crap. You don't accumulate 800 games without some baddies. I'd go crazy trying to find a perfect copy of everything. I keep track of my stuff in excel. I have it on Dropbox so I can look at it wherever I go and on my computer. Apps (and sites, for that matter) come and go, so you should probably try to keep your stuff in something that won't disappear at some point.... Just sayin'. But, at the end of the day, I liken it to 2:30 am, it's cold, no one is looking, and you don't want to go home alone. Walk of shame doesn't matter, had sex.
  4. Make sure to get MetalSlime's stuff as well!!
  5. Yes, sir. Didn't want them to be lost forever. Hopefully someone can post BunnyBoy's and MetalSlime's stuff as well
  6. INTRO I had a request for MMC1 mapper information, so I thought everyone might like to read what I have to say. This will help you figure out MMC1 memory mapping. It assumes that you have a working knowledge of NES programming and can understand my messed up little mind. Most of the information on this mapper can be found on: http://wiki.nesdev.com/w/index.php/MMC1 MMC1 will allow you to use up to 256 kB of PRG ROM and 128 kB of CHR ROM. I think that you can use SUROM to expand up to 4 MB of PRG ROM, but this is not covered here. First, you have to decide on what kind of configuration you want to use. MMC1 will support PRG bank switching in either 16kB mode or 32kB mode. And with the 16, you can choose whether you want the memory at $C000 or the memory at $8000 to be switched. Second, you have to decide on the CHR ROM switching. MMC1 supports switching of 4kB (background or sprite tiles separately) or 8kB (switching them together). After you decide this, you are ready to start. INES HEADER Notes: -You can only have even numbers of PRG banks. NESASM uses 8kB "banks", I'm talking about 16kB banks. i.e. 02 (32kB),04 (64kB),06 (96kB),08 (128kB),0A (160kB),0C (192kB),0E (224kB),10 (256kB) -CHR banks are in multiples of 8kB banks. (important if you are using 4kB swapping.) i.e. 01 (8kB),02 (16kB),03 (24kB),04 (32kB),05 (40kB), 06 (48kB), etc., 10 (128kB) -MMC1 mapper number is "1" ...creative! -Mirroring should match that used below in the initiation routine. In this exercise, we will use: .inesprg $10 ; 16x 16KB PRG code .ineschr $10 ; 16x 8KB CHR data .inesmap $01 ; mapper 1 = MMC1, 4KB CHR bank swapping .inesmir 0 ; background mirroring MAPPER CONTROL HEADER This is one bite that has all the information for the mapper. Observe: 76543210 Bits 7,6,5 - Not sure what these do. Bit 4 - CHR ROM bank mode - (0 means switch 8kB at a time, 1 means switch the two separate 4kB banks independently) Bit 3 - PRG ROM bank mode - (0 means switch all 32kB at once, ignores bit 2) (1 means switch ONLY the 16kB specified in bit 2) Bit 2 - PRG ROM location - (0 means switch 16kB at $C000, 1 means switch 16kB at $8000) Bits 1,0 - Mirroring - (0 means one screen, lower bank; 1 means one screen, upper bank 2 means vertical; 3 means horizontal) Here we will use LDX #%00011000 BITCH WORK! Look above and figure out what this means. INITIATE MAPPER Here we load the information required by the system to run the mapper as well as the initial banks. You have to do the 5 writes to make it work...for whatever reason. initMMC1Mapper: LDA #$80 ;this locks the PRG ROM at $C000-$FFFF to the last bank. STA $8000 TXA ;uses our header to initiate the mapper JSR setMMC1ControlMode LDA #$02 ;sets the CHR information for the sprites JSR setCHRPage0000 LDA #$01 ;sets the CHR information for the background JSR setCHRPage1000 LDA #$03 ;sets the PRG information JSR setPRGBank RTS setMMC1ControlMode: STA $8000 LSR A STA $8000 LSR A STA $8000 LSR A STA $8000 LSR A STA $8000 RTS setCHRPage0000: STA $A000 LSR A STA $A000 LSR A STA $A000 LSR A STA $A000 LSR A STA $A000 RTS setCHRPage1000: STA $C000 LSR A STA $C000 LSR A STA $C000 LSR A STA $C000 LSR A STA $C000 RTS setPRGBank: STA $E000 LSR A STA $E000 LSR A STA $E000 LSR A STA $E000 LSR A STA $E000 RTS Congrats....your program should work. USING THE MAPPER You can swap out banks whenever you want, even several times per NMI. Just load the bank number you want to use into A and call the appropriate subroutine. Just be sure that you don't switch away information that your program needs to run or it will die. Weird bank numbering notes: -CHR data is stored in 8kB for NESASM. If you want to call the first 4kB of data from the 6th 8kB chunk, you would use bank #$0C. Observe, call number for 4kB chunk vs. 8kB bank number: 00-0 01-0 02-1 03-1 04-2 05-2 06-3 07-3 08-4 09-4 0A-5 0B-5 0C-6 0D-6 0E-7 0F-7 10-8 11-8 12-9 13-9 14-10 15-10 16-11 17-11 18-12 19-12 1A-13 1B-13 1C-14 1D-14 1E-15 1F-15 Clear? -PRG info is stored in 8kB chunks in NESASM, but you call and switch 16kB banks. If you want to call bank 26, use call number #$0D. Observe, call number vs. bank number: 0-0,1 1-2,3 2-4,5 3-6,7 4-8,9 5-10,11 6-12,13 7-14,15 8-16,17 9-18,19 A-20,21 B-22,23 C-24,25 D-26,27 E-28,29 F-30,31 Clear? -At the end of each 16kB bank, you have to have vectors in place or it will die. .org $FFFA ;first of the three vectors starts here .dw NMI ;when an NMI happens (once per frame if enabled) the ;processor will jump to the label NMI: .dw RESET ;when the processor first turns on or is reset, it will jump ;to the label RESET: .dw 0 ;external interrupt IRQ is not used in this tutorial -Bank numbering is successive. i.e. if you have PRG banks numbered 0-23, you would start numbering your CHR banks at 24. -If you have, for example, a 16kB CHR file, you only have to include the starting place and NESASM will split the banks properly. i.e. in 4kB mode: .bank 32 .org $0000 .incbin "MMC1.chr" ;includes 16KB graphics file This will include 4 - 4kB (or 2-8kB) banks in the assembly process. Be sure to account for the 2 banks in your numbering. (see the attached ASM file.) PRACTICAL APPLICATION This is a little inefficient. To use all this nonsense in something real, an example would be: LoadBackground: ;find room info ;switch to room info table bank ;load bankground pointer ;switch to bank where background information is stored ;load background ;switch back to room info table bank ;load attribute and palette pointers ;switch to attribute/palette information bank ;load attributes/palettes ;switch back to room info table bank ;load collision detection pointer ;switch to collision detection bank RTS WORKING EXAMPLE Here we use the controller to switch banks for both CHR banks and the PRG bank. A and B - swap out CHR information for the sprites Select and Start - nothing Up and Down - load a background located in different banks (the flashing is cause you are holding the button down for more than one frame. Just tap it. I was too lazy to add stuff in to fix this.) Left and Right - swap out the CHR information for the backgrounds Download the attached file and assemble the program. Mess around with it and try to switch out the various buttons and banks. Fix the flashing. Add new backgrounds and characters...etc. The little DOS assembler file might not work, you may have to edit it to your drive. THE END! I'm sure I totally screwed this up, but don't worry! Someone will help me out if there are any mistakes. MMC1.zip
  7. ETA: Forgot to set the clear memory to load $FE to the $0700 page. Don't forget this.This tutorial is kind of a side note to the others. It just shows how to do sprite cycling and better attribute handling.This is fairly involved, so make sure you go slowly and think about stuff. If the position stuff doesn't make sense, it won't end well for you.First, when you have more than 8 sprites per scanline, some will dissapear. This is pretty simple to fix. You just flip-flop the order in which they are drawn every frame...keeping in mind that ones which are stored lower in RAM are drawn first (i.e. one at $0204 is drawn on top of $0208). So, by flipping the order they are stored in RAM, we can make them flicker so they won't dissapear completely.Start by reserving the set $0700 to be your working area for your sprites. Keep the set $0200 (or whatever) for your DMA memory. Then simply copy them "forward" one frame and then "backwards" the next...like so:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Sprite_Cycling:LDA Sprite_RotationBNE .Backwards.loop1LDA sprite_RAM,x ;copy forwardsSTA sprite_DMA,xINXBNE .loop1INC Sprite_RotationRTS.BackwardsLDY #$FC.loop2 ;copy backwardsLDA sprite_RAM,xSTA sprite_DMA,yINXINYLDA sprite_RAM,xSTA sprite_DMA,yINXINYLDA sprite_RAM,xSTA sprite_DMA,yINXINYLDA sprite_RAM,xSTA sprite_DMA,yTYASECSBC #$08TAYINYINXBNE .loop2LDA #$00STA Sprite_RotationRTS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Pretty simple. You can cycle them all (like here) or just some (if you have a sprite zero hit, you don't want that bitch moving around). Or you can make it so your PC is always on top.Next, we will look at how to better handle attributes, at least in my opinion. If you figure this out, you'll never have to fuck with them again! It will be automatic.So, first we need to modify our metatiles.MetaTileSeta00.db $44,$49,$5F,$7A,%00000000,%00001111The first 4 bytes are the tiles, like always.The sixth byte is the collision data, like always.The fifth byte is the attribute data for that particular metatile. If you want it to draw from pallete "01" you'd use "%01010101", pallete "11", "%11111111", and so on.Do this for all of them. Note that if you have two tiles that use the same bg tiles but different colors, you'll have to make a duplicate.It's probably better that you look through the routine as you read. It's pretty simple once you figure it out.Next, we need to look at our position in the background data table. If you think in hex, you always have $10 meta tiles across the screen. So, row 0 would be $00-$0F, row 1 is $10-$1F, 2 is $20-$2F.So, the attribute bytes are 2 metatiles across. This means that for each row, we just divide by 2 to get the position in the table...we hope that the NES has a convinent command for this. Just use LSR A!!Next, there is the same thing with the vertical. Row 0 and 1 use the first 8 attribute bytes, 2 and 3 use the second, and so on. So, to figure out a row, we AND the room table counter with %11100000 and LSR A twice...I'll leave it to you to work this out.The combo of these two steps will produce your position within the attribute table.Next, you simply use the table position to work out which quadrant you are within in the attribute byte, and Bob's your uncle.I kinda glazed over this finding the position thing. So if you can't figure it out, I can delve into it further. But it is important that you work it out and understand what it's doing. Take a pen and paper, draw out a screen, label each metatile (in hex), and manually decode the routine to see what it's doing. It will click and life will be titties and rainbows.I hope you enjoyed this. Post questions!! attributes.zip
  8. INTRODUCTION This time we are going to add in the (in my opinion) hardest part so far. This one takes some math skills, the ability to visualize exactly what the program is doing, and a complete understanding of how our pointers/data tables are set up. If you understand everything up to this point, you should be okay. If not, you're Fucked. But don't dispair, if you get stuck or don't understand, just ask questions. A couple things that I need to say before we get started: 1. The concept for this routine is 100% original, as is the actual code and the process. I had hints from people on NA and nesdev (mostly it was them telling me that I was attempting to do it wrong at the start), but this I came up with pretty much on my own. 2. There are a couple things that could be corrected in the program. Mainly that there are no walls in certain places and the routine doesn't exactly know what to do with the end of the screen, so it makes the NPCs jump to a random point when it hits the edge of the screen. Easily corrected though. 3. This program is easily modified for use in a platformer. If I get time in the near future, I'll go through that as well. 4. This program could also be modified to support scrolling. I might do this too if I ever get around to it. 5. Other than the collision routine its self, which isn't all that complex, all the PRG space that this takes up is what we need in the way of collision data: one (1) byte per meta tile. 6. This routine is based on the meta tiles and meta sprites that we've built up to this point. You could modify it to support different ways of doing things, but then you'd loose the compression effects. 7. Ever thus to deadbeats. CONCEPTS First we need to lay out what we're aiming for. We want some sort of routine that will detect when a sprite comes into contact with a piece of background that we define as "solid" and/or do something when it impacts a certain part of the screen. Then we need to tell the program what to do when this impact(s) occur(s). So, if we think about this, we need to somehow have the following steps. 1. Calculate the meta sprite's position on the screen. 2. Pull some sort of collision data based on this position and the current background. 3. Determine from this collision data if we need to move the character (or do something else). 4. Move the character. 5. Reset the graphics. 6. Repeat for each character/meta tile. 7. ??? 8. Profit. Note that we are not going to cover anything besides "simple collision" here. Once you have the program working that pulls the collision data, the various add ons (or op-codes) are fairly simple to input. COLLISION DATA I suppose that the best place to start would be for us to decide how we are going to define our collision data. Well, this is pretty simple. Remember our meta tiles?? Let's jump back to them. Remember this picture: Here, we are going to keep everything simple. "Solid" meta tile blocks and "hollow" meta tile blocks, however the process is exactly the same for something that only impacts in one direction. (Think the bricks in Mario Bros. that you can jump up through, but can't fall down through.) We are going to store all the data for each meta tile in one byte. Basically: %0,0,0,0,0,0,0,0 %Op-Code,Op-Code,Op-Code,Op-Code,Up,Down,Right,Left Where, for each bit: 0 = hollow -and- 1 = solid -or- 0 = do nothing -and- 1 = activate op-code (Note that this limits you to 4 op codes, you may add more pretty easily.) So, if we wanted something to only register an impact when we are: -going up, %00001000 -going down, %00000100 (heh, going down, get it?) -going right, %00000010 -going left, %00000001 -going up/down, %00001100 -goint right/left, %00000011 -etc. Nice and simple. And it keeps our data easy to read, which is always a plus. META TILE COLLISION DATA Again, for our purposes, we are going to keep it simple. While the program fully supports the various settings described in the previous section, we are going to stick with %00000000 and %00001111, hollow or solid. Once you complete this, you should go back and change some of these settings to see what it actually does. So, if we make the only "hollow" meta tile the "sky" tile and the rest of them "solid", our meta tile bank turns into this: ;------------------------------------------------------------------------------------ MetaTileSeta00 .db $44,$49,$5F,$7A,%00001111 MetaTileSeta01 .db $AB,$AD,$AC,$AE,%00001111 MetaTileSeta02 .db $53,$54,$55,$56,%00001111 MetaTileSeta03 .db $B0,$B2,$B1,$B3,%00001111 MetaTileSeta04 .db $A5,$A6,$A7,$A8,%00001111 MetaTileSeta05 .db $7B,$7C,$7D,$7E,%00001111 MetaTileSeta06 .db $24,$24,$24,$24,%00000000 MetaTileSeta07 .db $45,$45,$47,$47,%00001111 MetaTileSeta08 .db $47,$47,$47,$47,%00001111 MetaTileSeta09 .db $CA,$CB,$CC,$CD,%00001111 MetaTileSeta0A .db $C6,$C7,$C8,$C9,%00001111 MetaTileSeta0B .db $6B,$6C,$70,$71,%00001111 MetaTileSeta0C .db $6D,$6C,$72,$71,%00001111 MetaTileSeta0D .db $6D,$6F,$72,$74,%00001111 ;---------------------------------------------------------------------------------- See, this is all we have to specify our collision data. You could either have this monstorous set of "CMP"s for each direction (as in the sprite collisions), that takes on the order of 750 bytes per room, or one byte per meta tile. You decide. THE "collideptr" POINTER One thing that we need to add that is kind of random is a pointer that supports the collision routine. This needs to be set up in the "Room Loading" routine. Note that technically here you could use the "ptr1" that we are using in the decompression routine, but as your program gets more complex...well, it's just better to have something as important as collision detection use its own variables. So, in the "LoadbgBackground" routine: ;---------------------------------------------------------------------------------- ;;;;;;;;;;;;;insert;;;;;;;;;;;;; ASL A ;indexing to a table of words STA room_index LDX room_index ;using the room index, load the pointer to the correct background data LDA backgroundpointer,X STA ptr1 LDA backgroundpointer+1,X STA ptr1+1 ;;;;;;;;;;;;;insert;;;;;;;;;;;;; -becomes- ;;;;;;;;;;;;;insert;;;;;;;;;;;;; ASL A ;indexing to a table of words STA room_index LDX room_index ;using the room index, load the pointer to the correct background data LDA backgroundpointer,X STA ptr1 STA collideptr ;store the room data for collision detection LDA backgroundpointer+1,X STA ptr1+1 STA collideptr+1 ;;;;;;;;;;;;;insert;;;;;;;;;;;;; ;---------------------------------------------------------------------------------- Notice that our collideptr is pointing to the background data table for the room that we are loading! i.e. the table of meta tiles...see where we are going?? COLLISION POSITION REQUIREMENTS If we take a step back and think about this for a moment, we have a bit of a problem. Consider the following picture: Assume that the red square represents our metasprite and that the block/sky is the space that it is currently occupying over the background. You can see in each example picture, we are impacting the same 4 background meta tiles, but depending on EXACTLY where the 4 corners of our metasprite are, we would require completely different results from our collision routine. Take the upper left hand example in the picture. If we used the bottom of the meta sprite as the collision coordinates for moving to the right, we wouldn't register an impact, but the top would. Same with the upper, 2nd from the left. Or, if we were trying to move up, if we used the coordinates of the left side of the meta sprite, we wouldn't register a hit but the left side would. Work your way through each of the 8 pictures to be sure you understand this point. So what do we do about this little problem? Panic? Sure. Next take a breather and consider the following. We just need our program to compute the location of each of the 4 corners. Then if we were using the upper left picture and moving right, we would simply say: -Look at the bottom, right corner. No impact. -Look at the top, right corner. Oh shit! An impact!! Simple. Understand? Same with moving up: -Look at the top, left corner. No impact. -Look at the top, right corner. COLLISION! Slap ass crazy. So, now that we know that we have to compute the location of 4 different corners, we can define a few variables to help us along our way. I'm just going to put these out there and define them better as we go along. ;---------------------------------------------------------------------------------- collision_index .rs 4 ;indexes for the look up of meta tile info in the main collision routine collide_metatile .rs 4 ;place to store collision meta tile data collide_vertical .rs 4 ;store the index data for the sprite positions in the main collision routine collide_horizontal .rs 4 collide_data .rs 4 ;actual data from the meta tile streams collide_position .rs 4 ;the location to set him to if he hits something ;---------------------------------------------------------------------------------- DEFINING THE META SPRITE COLLISION BOUNDARIES First, we need to find the positions that we will be using to define our collision coordinates for these 4 corners. Remember how before we referenced everything to the "sprite_vertical" and "sprite_horizontal"? Well this is why. When we loop our different characters through this routine, we will be using all of our registers in another fashion. So, we need to simply transfer their coordinates to these variables before we call the collision routine, then, if there is a collision, modify these variables, then use these variables to transfer the data back into the RAM space. In this routine, we are going to need to use these two variables to specify the location of each of the 4 corners. Note that here we are assuming that all of our meta sprites are the same. If you use different sizes, you'll have to add something based on enemy type for this. Consider this picture and the variables that we introduced above: You can set your boundaries however you want, but if you don't set the bottom to the bottom of the feet, your character will "float" and if you don't leave a couple pixels at the top, your character will have trouble fitting between meta tiles. Sameish for the right/left. So, to load our values for our collide_horizontal and collide_vertical variables, we simply write: ;---------------------------------------------------------------------------------- .sprite_transferBGCD: LDA sprite_vertical ;transfer correct collision box coords CLC ADC #$02 ;to change this, must change DEC below to keep him from flashing (comes later) STA collide_vertical ;here we set the boundries of the collision detection for the metasprites STA collide_vertical+1 ;note that here we assume that all of the sprites are the same size LDA sprite_vertical ;if your sprites are different, you'll have to add a subroutine here CLC ADC #$10 STA collide_vertical+2 STA collide_vertical+3 LDA sprite_horizontal CLC ADC #$03 STA collide_horizontal STA collide_horizontal+2 LDA sprite_horizontal CLC ADC #$0C STA collide_horizontal+1 STA collide_horizontal+3 ;---------------------------------------------------------------------------------- Note that in the following, some of this is not necessary. However, I'm leaving it in here because if you expand the program, you will need all these variables later. FINDING META TILES Now that we have our dude's location on the background entered in temporary variables, we need to use them to calculate some sort of index to pull the meta tile information from our room data. And after we have the meta tile info, we use that to pull the collision data we specified eariler...but one thing at a time. Let's start with the top left corner of our meta sprite and a picture. We see that a room is composed of 16x16 (or $10x$10) pixel meta tiles. Consider this room and the data table that the room loading routine pulls data from. We see that each $10 pixels we go down, we are going another $10 entries into this table (or 1 row down, hence formatting the room data talbes this way)...or, for the left most meta tile: -row 0 (top row) = $00 -row 1 = $10 -row 2 = $20 -etc. So, to specify which meta tile the collision coordinates are occupying in the vertical direction, we simply take the vertical position: -compare it to $10 -if lower, we have found our meta tile -if higher -subtract $10 from the vertical position -add $10 to our collision_index variable then jump back to the start of the loop Now, we've specified which row our guy is in, so now we need to account for the horizontal portion of the position. This is pretty much the same as the vertical part, except that for every $10 you go to the right, you only add $01 entry to our collision_index variable. Make sense? Think about it. If we are in: -column 0 = $X0 -column 1 = $X1 -column 2 = $X2 -etc. So, to add in the horizontal part of our position, we simply take the horizontal position and: -compare it to $10 -if lower, we have found our meta tile -if higher -subtract $10 from the horizontal position -add $01 to our collision_index variable then jump back to the start of the loop So, now we've found the value for "collision_index". Does this make sense? This is basically the hardest part, so if you don't get it, stop and ask questions. All we're doing is using the location of our meta sprite and translating that to an index to pull data from the specific room we are in's background data table. Now, we have 4 corners that we need to consider when running collision detection. So, this implies that we will need to run this little loop 4 times, 1 time for each corner. However, this is resource intensive, it will basically take our program twice as long to run...so we are going to take a short cut. Remember how our collision box above always keeps the right side a fixed distance from the left side? Well, we can use that information to cut the 2nd and 4th loop out of the program. So, if we take the value for collision_index (upper left corner) and store it in collision_index+1, we can modify collision_index+1 to specify the upper right corner. Think back to the last time through the loop for the horizontal portion. We compare the value to $10 and if it is less than this, we just disregard this unused portion and jump down to the next step...however, if we do it correctly, the remainder should still be in the register, in this case "Y". So, if we take this remainder, add the fixed horizontal distance that we specified in our meta tile collision boundaries, we will come up with the horizontal location of our right corner!! So now, we simply take this value and: -compare it to $10 -if lower, collide_index = collide_index+1 (both sides of the meta sprite are on top of the same meta tile) -if higher -add $01 to our collision_index+1 variable -jump to the next part (we see that collision_index+1 will always equal collision_index or be 1 greater than) Now if we've done this correctly, we should have an accurate location of each of the top corners with respect to the background data. However, if we look back at our background data, we have one minor problem...the first entry in the table is always the meta tile bank number. So we need to INC collision_index and collision_index+1 to account for this fact. NOW we have the correct location of the data we are seeking. The next step is simple. Use these index numbers to pull the meta tiles from the room data. Then store these meta tiles into the variables we set up, collision_metatile and collision_metatile+1. Finally, we jump back to the start and repeat the process for the two bottom corners. Simply stated in code, we do the following: ;---------------------------------------------------------------------------------- LDX #$00 .find_collision_meta_tiles LDA collide_vertical,x ;find the index for the upper left corner of the sprite intersect ; SEC ; SBC #$40 ;take off the HUD if you are using one to save loop time. You'll have to change the vertical positions as well .loop CMP #$10 ;Here we use the sprite vertical/horizontal positions to find the look up index in the room data table BCC .next ;use the vertical position to find the first part SEC SBC #$10 ;take one row off of our vertical position TAY LDA collision_index,x CLC ADC #$10 ;add $10 to our index or skip a row in the data table STA collision_index,x TYA INC collide_position,x ;set up the value of the position marker (vertical_position) JMP .loop .next ;include the horizontal portion of our position LDA collide_horizontal,x .loop2 CMP #$10 ;for each meta tile we are from the left, incriment our index BCC .next1 SEC SBC #$10 INC collision_index,x INC collide_position+1,x ;set up the position marker JMP .loop2 .next1 TAY ;find the opposite corner, the right corner is always a FIXED distance from the left corner LDA collision_index,x STA collision_index+1,x TYA CLC ADC #$09 CMP #$10 BCC .next2 ;are the left and right corners in the same meta tile?? INC collision_index+1,x .next2 INC collision_index,x ;skip the meta tile bank entries in the table INC collision_index+1,x ;our index should be correct now! PowerYay!! .load_meta_tile_data LDY collision_index,x ;load the meta tile number from the room data block LDA [collideptr],y STA collide_metatile,x ;store the meta tile info LDY collision_index+1,x ;load the meta tile number from the room data block LDA [collideptr],y STA collide_metatile+1,x ;store the meta tile info INX ;first loop is for the top of the metasprite INX ;second is for the bottom of the metasprite CPX #$04 BNE .find_collision_meta_tiles ;---------------------------------------------------------------------------------- SPECIFYING POSITIONS TO RESET META SPRITES You'll notice the variable set "collide_position" in this routine. These are the variables that we use to specify where we want our meta sprite to be reset to given an impact in a certain direction. You'll see that the values of collide_position and collide_position+1 are modified each time we go through our loops when we try to use the vertical and horizontal parts of our position. So, we are counting meta tiles towards the bottom from the top of the screen and to the right of the left side of the screen. Basically, we are setting up a grid that our sprites will snap to if they hit something. Note that you only need the data acquired in the first time through the loop but you have to leave it in there cause we are using the "X" index. Now, after we've set up a variable to track the position like this, we need some sort of data set that translates this info into pixel coordinates. Like so: ;---------------------------------------------------------------------------------- vertical_positions: .db $00,$0F,$1F,$2F,$3F,$4F,$5F,$6F,$7F,$8F,$9F,$AF,$BF,$CF,$DF ;this is what we set the position data to when we hit something horizontal_positions: .db $00,$0D,$1D,$2D,$3D,$4D,$5D,$6D,$7D,$8D,$9D,$AD,$BD,$CD,$DD,$ED ;---------------------------------------------------------------------------------- This is a somewhat iterative process that I did by trial and error after freaking error. I'm sure if you tried hard enough, you could make this part cleaner, but at some point you just say "Fuck It" and move on. By using the same data sets as described above for, say, up and down collision, you run into a slight "flashing" problem. Basically, your dude runs through a wall before the program registers the impact, then the next frame the impact registers, then it moves the guy for that frame, then you move into the wall again, on and on, flashing one frame in each location, i.e.: So, we have to make some minor corrections to the vertical_positions and horizontal_positions data to make this flashing stop. In the end, to save variable space, we overwrite these position variables that we calculated above with the actual values that we will set our meta sprite positions to. This is a pretty simple part of the routine, suffice it to say that when it's done, you'll end up with this: collision_position = position to set the meta sprite to if moving DOWN collision_position+1 = position to set the meta sprite to if moving UP collision_position+2 = position to set the meta sprite to if moving RIGHT collision_position+3 = position to set the meta sprite to if moving LEFT -or, the actual code- ;---------------------------------------------------------------------------------- .position_finder LDX collide_position ;find the various stop places in the grid using our positions above LDA vertical_positions,x STA collide_position INX LDA vertical_positions,x LDX collide_position+1 STA collide_position+1 DEC collide_position+1 ;must change this if guy flashes LDA horizontal_positions,x CLC ADC #$06 ;must change this if guy flashes STA collide_position+2 INX LDA horizontal_positions,x STA collide_position+3 ;---------------------------------------------------------------------------------- PULLING COLLISION DATA FROM META TILE BANKS Now that we have everything pretty much set up, we need to do some simple looks ups to pull data from our meta tables. i.e. the 1 byte per meta tile that we specified in the first part of this write up. This part of the routine would run as follows: -load the meta tile bank number (first entry of the room data stream) and store the pointer info in our "meta_tile_sets_ptr" pointer -start a loop for X = 0, 1, 2, 3 -load the collide_metatile+X that we found above and pull the value from our room data table -using our meta_tile_sets_ptr and the collide_metatile values, load the pointer meta_tile_ptr with the address of the meta tile we are checking collision with -pull the 4th entry from the meta tile data stream and store it in the variable collide_data+X for later use -jump back to the start of the loop unless X = 3 -or- ;---------------------------------------------------------------------------------- LDY #$00 LDA [collideptr],y AND #%00111111 ;load the correct meta tile bank address ASL A TAX LDA meta_tile_sets,x ;load the meta tile set pointer STA meta_tile_sets_ptr LDA meta_tile_sets+1,x STA meta_tile_sets_ptr+1 LDX #$00 ;start out at X = zero .load_collision_data: LDA collide_metatile,x ;load the meta tile found earlier ASL A TAY LDA [meta_tile_sets_ptr],y ;load the pointer info for that metatile STA meta_tile_ptr INY LDA [meta_tile_sets_ptr],y STA meta_tile_ptr+1 LDY #$04 ;load the collision data from the meta tile string LDA [meta_tile_ptr],y STA collide_data,x ;store it for later use INX CPX #$04 ;repeat for each corner of the meta sprite BNE .load_collision_data ;---------------------------------------------------------------------------------- Simply put, this uses the data that we've compiled throughout this exercise and finally pulls the collision data from our game's memory. If we've made it this far, we are just about done! Just need to tie up some loose ends and viola! Success. COLLISION DETECTION Now that we've got this, it is a simple matter of checking a couple positions and determining if we need to move our meta sprite or not. Each time we run our Collision_Detection subroutine, the first thing we need to do is clear out some variables: the collision_index variable set and the collide_position set. You'll notice on these variables we never actually store a value to them. We either INC them or ADC to them. So, if we don't reset them every time, we'd have some residual data that would fuck up our routine. ;---------------------------------------------------------------------------------- Collision_Detection: LDA #$00 ;clear out the indexes STA collision_index STA collision_index+1 STA collision_index+2 STA collision_index+3 STA collide_position STA collide_position+1 STA collide_position+2 STA collide_position+3 ;---------------------------------------------------------------------------------- Next, we JSR to the "Find_Collision_Meta_Tiles" subroutine (i.e. find the collision data) that we just constructed above. Note that you don't need to JSR here, but if you modify this for platformer use, you'll have to. Then, if you think about it, in this "top-down" type game, you only run in one direction at any given time...so you only need to check collision in the direction you're moving. This saves us some effort. (Again, with a platformer, you are moving up/down and right/left at the SAME time...keep that in mind.) So, we need ot construct a simple routine that tells the program which direction we want to check: ;---------------------------------------------------------------------------------- Collision_Detection: LDA #$00 ;clear out the indexes STA collision_index STA collision_index+1 STA collision_index+2 STA collision_index+3 STA collide_position STA collide_position+1 STA collide_position+2 STA collide_position+3 JSR Find_Collision_Meta_Tiles ;find the collision data from the meta tiles LDX enemy_number ;we only need to check the direction we are currently traveling LDA enemy_direction,x BEQ .up CMP #$01 BEQ .down CMP #$02 BEQ .right JMP .left ;---------------------------------------------------------------------------------- Then we will jump to the various directions. Note that these directions are all pretty much the same, so we'll just go over one of them. But first, remember the AND function that we learned about from MetalSlime? We use it here. You'll remember that we specified the solid/hollow collision data with one bit for each direction in a single byte to save space. So, we'll have to use the AND command to pull the data for a single direction from this byte. So, to check collision if we are moving UP: -load the variable "collide_data" into A (note that this is the upper LEFT corner) -AND #%00001000 (the UP bit) -if A=0, there is no collision, jump to "up1" -if A is not = 0, we have a collision!!! -load the correct "collide_position" value that we calculated into A (+1 in this case) -store A in sprite_vertical (move the meta sprite to the collision boundary) -jump to .continue_on .up1 -load the variable "collide_data+1" into A (note that this is the upper RIGHT corner) -AND #%00001000 (the UP bit) -if A=0, there is no collision, jump to "up2" (a RTS command) -if A is not = 0, we have a collision!!! -load the correct "collide_position" value that we calculated into A (+1 in this case) -store A in sprite_vertical (move the meta sprite to the collision boundary) -jump to .continue_on .continue_on (this is just to make the meta sprites do something when they hit a wall, otherwise, they'd just sit there and keep running at the wall) -check to see if it is the PC (if it is, we don't want to mess with it because we control it) -yes, RTS -no, continue -JSR to our direction_change subroutine so that the meta sprite "bounces" off the wall. However, you can put whatever you want here. -RTS We do this for all four directions and we're done!! ;---------------------------------------------------------------------------------- Collision_Detection: LDA #$00 ;clear out the indexes STA collision_index STA collision_index+1 STA collision_index+2 STA collision_index+3 STA collide_position STA collide_position+1 STA collide_position+2 STA collide_position+3 JSR Find_Collision_Meta_Tiles ;find the collision data from the meta tiles LDX enemy_number ;we only need to check the direction we are currently traveling LDA enemy_direction,x BEQ .up CMP #$01 BEQ .down CMP #$02 BEQ .right JMP .left .right LDA collide_data+1 ;load the data we found AND #%00000010 ;check only the right direction data BEQ .right1 ;no impact, check the bottom of the sprite LDA collide_position+2 ;load the position setting we found STA sprite_horizontal ;store it in the horizontal position of the sprite JMP .continue_on ;jump to the next step .right1 LDA collide_data+3 ;same thing on the bottom AND #%00000010 BEQ .right2 LDA collide_position+2 STA sprite_horizontal JMP .continue_on .right2 ;no impact? Don't do shit!! RTS .left LDA collide_data ;same as above. AND #%00000001 BEQ .left1 LDA collide_position+3 STA sprite_horizontal JMP .continue_on .left1 LDA collide_data+2 AND #%00000001 BEQ .left2 LDA collide_position+3 STA sprite_horizontal JMP .continue_on .left2 RTS .up LDA collide_data AND #%00001000 BEQ .up1 LDA collide_position+1 STA sprite_vertical JMP .continue_on .up1 LDA collide_data+1 AND #%00001000 BEQ .up2 LDA collide_position+1 STA sprite_vertical JMP .continue_on .up2 RTS .down LDA collide_data+2 AND #%00000100 BEQ .down1 LDA collide_position STA sprite_vertical JMP .continue_on .down1 LDA collide_data+3 AND #%00000100 BEQ .down2 LDA collide_position STA sprite_vertical JMP .continue_on .down2 RTS .continue_on LDA enemy_number BEQ .done JSR direction_change ;need to add this in here so that the enemies don't just sit there and run at the walls .done ;errrr....helments!! RTS ;---------------------------------------------------------------------------------- CALLING THE COLLISION DETECTION ROUTINE Now that we've written the routine, we'll need to call it either in NMI or our Main Program. Putting lenghtly stuff in NMI is bad, so we'll put it in the main program. Think about where we should put it. We'll have one place for our PC JSR group and on place in our NPC loop. For the PC, if you think about it, the place that makes sense is right after we "handle_input". That way we move our dude based on the controller input, check to see if he hits a wall, then run the graphics updates. If you put it after the graphics updates, you'd have to run them again if you moved the meta sprite during collision which is a waste of time. Note that these "transfer_location" routines are necessary to run the Collision_Detection routine we've set up, but you don't have to make them their own routine. I do it this way because when I make my own programs, I reference EVERYTHING to the sprite_vertical and sprite_horizontal variables (rather than the Sprite_RAM+X stuff we use here). I think that the Sprite_RAM stuff is easier to grasp and visualize when you are first starting. It is a little more difficult to grasp when you use the same variables for everything. But referencing everything to these two variables makes your routines faster and easier to loop through, IMHO. But to each his own. Back on topic, we update our PC JSR group as follows: ;---------------------------------------------------------------------------------- ;;;;;;;;;;;;;insert;;;;;;;;;;;;; LDA #$00 STA enemy_number STA enemy_ptrnumber JSR handle_input JSR transfer_location_info JSR Collision_Detection JSR restore_location_info JSR Enemys_Animation JSR Enemys_Sprite_Loading JSR update_enemy_sprites JSR transfer_location_info JSR arrow ;;;;;;;;;;;;;insert;;;;;;;;;;;;; ;---------------------------------------------------------------------------------- Then it's basically the same thing in the enemy update routine: ;---------------------------------------------------------------------------------- ;;;;;;;;;;;;;insert;;;;;;;;;;;;; UpdateENEMIES: LDA #$01 ;this loop updates the enemies one at a time in a loop STA enemy_number ;start with enemy one LDA #$02 STA enemy_ptrnumber .loop JSR enemy_update ;move the sprites based on which direction they are headed JSR transfer_location_info JSR Collision_Detection JSR restore_location_info JSR Enemys_Animation ;find out which frame the enemy animation is on JSR Enemys_Sprite_Loading ;update the enemy meta tile graphics JSR update_enemy_sprites ;update position LDY #$00 ;arrow ;sprite collision detection routines JSR enemy_collision ;note that 00, 02, and 04 are to specify the variables of each weapon LDY #$02 ;mace JSR enemy_collision LDY #$04 ;Playable Character JSR enemy_collision INC enemy_number ;incriment the enemy number for the next trip through the loop INC enemy_ptrnumber ;these are addresses for the graphics data, so we need to keep it at 2x the enemy number INC enemy_ptrnumber LDA enemy_number CMP #$04 ;if it is 4, we have updated enemies 0,1,2,3 so we are done BNE .loop UpdateENEMIESdone: ;;;;;;;;;;;;;insert;;;;;;;;;;;;; ;---------------------------------------------------------------------------------- END GAME I think that's about it. You should have a pretty good start to a background collision engine. I would suggest that you take and modify the rooms to see that it infact updates the collision detection automatically. Then change some of the collision properties of the various meta tiles. Then add in some simple op-codes like infront/behind some hollow background stuff or something. Play around and see what you can come up with. Maybe expand the map to be sure you understand the room loading/switching routines. Modifying this for use in a platformer is pretty simple. I'll try to sit down and write something up for that when I get time. Scrolling is somewhat more complex. Basically you'd just have to use your scroll values to modify your index that pulls data from the background tables and the collide_position variables will need changing. If I do this one, it will be a while as scrolling and I don't really see eye to eye...YET! Please enjoy and let me know if you have any questions. Until next time !! backgroundcollision.zip
  9. INTRODUCTION This write up is pretty straight forward. It basically merges our Sprite Collision program with our Background Compression program. There are some changes here and there, but that's basically it. I've added some extra stuff to give this part some content though. It should be helpful to you. MERGING THE PROGRAMS Well, the first thing that we need to do is merge the programs. This is pretty much a cut/paste job that you can walk yourself through. Just start at the top and work your way down. I'll leave it to the user to figure this out as it really is simple. CHR_RAM PART 1 First, you'll notice that the Sprite Collisions uses CHR_RAM and the Background Compression uses the standard CHR banks. We'll we are going to use the CHR_RAM technique because it is much more interesting in my opinion. First thing that we need to do is make sure that the .ineschr value is "$00" because we don't have any CHR banks. All of our CHR data is stored in the program, not in seperate CHR banks. Next, we need to add this to our RESET routine: ;------------------------------------------------ LDX #$00 JSR LoadCompleteBank ;load the sprite data LDX #$02 JSR LoadCompleteBank ;load the background data ;------------------------------------------------ In the Sprite Collisions we were only using the sprite table, so we hardcoded the addresses. Here we have 2 banks to mess with so we'll need some sort of pointer set up to reference the graphics we want to load. So, we set up a address table as follows: ;------------------------------------------------ graphicspointers: ;addresses of the CHR Data .word Sprite_Data,CHR_Data ;------------------------------------------------ What does this reference? Well, now we get into how to include the graphics files into your program without actually having to enter all the numbers in. Thankfully, we can use the tools and CHR files that we've already made through our friend Tile Molestor. Now, you don't have to include entire banks, you can do partial banks, or even one tile using this method, but we'll get into that later. Here, because we're lazy, we are just going to keep including the entire CHR data banks. So, we need to do as follows in the various banks of our program data: ;------------------------------------------------ ;;;;;;;;;;;; ;;;;;;;;;;;; .bank 2 .org $C000 ;;;;;;;;;;;; ;;;;;;;;;;;; CHR_Data: .db $10,$00 ;background address in the PPU .incbin "CHR_Files/mario1.chr" ;;;;;;;;;;;; ;;;;;;;;;;;; ;;;;;;;;;;;; .bank 3 .org $E000 ;;;;;;;;;;;; ;;;;;;;;;;;; Sprite_Data: .db $00,$00 ;sprite address in the PPU .incbin "CHR_Files/SpriteMovement.chr" ;include the sprite graphics data ;;;;;;;;;;;; .org $FFFA ;first of the three vectors starts here .dw NMI ;when an NMI happens (once per frame if enabled) the ;processor will jump to the label NMI: .dw RESET ;when the processor first turns on or is reset, it will jump ;to the label RESET: .dw 0 ;external interrupt IRQ is not used in this tutorial ;;;;;;;;;;;; ;;;;;;;;;;;; ;------------------------------------------------ Before we used the .include command. .include is for additional code that we want the assembler to incorperate into our program. .incbin is "include binary file". Basically, just include data. You'll also note that there are 2 numbers before we say .incbin but after the address labels we used in our table above. This is the address that we want the CHR data loader to start writing data to. We have to tell it where to put the data or it will just put it wherever the hell it wants to. You can use whatever you want, but here we are putting our sprite data at PPU address #$0000 and background data at #$1000. Hence the labels. Well, what do we do with it? This is pretty much exactly the same as writing tiles to get background as you would do when loading a room. Just remember, you don't have to turn off NMI to write to the CHR space, but you MUST either do it during NMI or with the background off. Otherwise your program will frac out. Next, we need to introduce a new pointer. This is the pointer to the current tile data to be loaded. We will just use: ;------------------------------------------------ tile_loader_ptr .rs 2 ;pointer to the tile data for our CHR_RAM loading routine ;------------------------------------------------ Next, we need to write a simple program to simply load the data to $2007. Note that I haven't tried it, but in this form, it looks like without simple modification, this technique will only work if you start your CHR data at the beginning of a .bank XXXX. ;------------------------------------------------ LoadCompleteBank: ;load in the graphics LDA graphicspointers,x ;specify the address using the first two entries before the .incbin commands STA tile_loader_ptr ;and the value we load into "X" before calling the LoadCompleteBank routine. LDA graphicspointers+1,x STA tile_loader_ptr+1 LDY #$00 ;start with zero LDA $2002 ;read PPU status to reset the high/low latch LDA [tile_loader_ptr],y STA $2006 ;write the high byte INC tile_loader_ptr ;go to the next entry LDA [tile_loader_ptr],y STA $2006 ;write the low byte INC tile_loader_ptr ;next table entry LDX #$00 LDY #$00 .LoadBank: LDA [tile_loader_ptr],y ;load the value STA $2007 ;store it to the PPU at the address we previously specified INY ;next Y value CPY #$00 BNE .LoadBank ;if Y flips over back to zero, we need to change our pointer values INC tile_loader_ptr+1 ;next chunk of ROM loaded INX ;incriment our outer loop counter CPX #$10 ;if X=$10, we are done, if not, jump back into the loop BNE .LoadBank RTS ;------------------------------------------------ Simple. Note again that this is for an entire bank of CHR information. We'll see later how to change this for smaller data sets. ROOM SWITCHING Now that we've got all our program ready for input, we need to work out a way of switching rooms. Before we were using the D-PAD to switch rooms. Now the D-PAD controls character movement and the rest of the buttons are pretty much spoken for. So....?? Well, how about making the PC sprite switch rooms when you hit the edge of a screen? Let's do it. First, we need to set some arbatrary constants that will trigger our room switching. These can be whatever you want. Here, we are going to use: ;------------------------------------------------ top_exit = $0F ;room switching locations bottom_exit = $CF left_exit = $0F right_exit = $E1 ;------------------------------------------------ Next, we need to define some sort of map for our rooms. We only have Four Rooms like Tim Roth, so our map is pretty simple. This can be expanded to up to 256 rooms without too much trouble. Here is our map layout: X---0-----1----> Y 0 Room0 Room2 | 1 Room1 Room3 | Note that Y values increase as you go down and X values increase as you go to the right. Now that we have this idea in our heads, we need to define variables to track our current location in the grid. ;------------------------------------------------ Y_coord .rs 1 ;Y map coordinate X_coord .rs 1 ;X map coordinate ;------------------------------------------------ Again, Y is your vertical location in the map and X is your horizontal location in the map. Make sure you understand this or you'll screw it up and get lost. I speak from experience. Now, we pull out our fancy pointer table that specifies the location of the room data that we had before. ;------------------------------------------------ backgroundpointer: ;room data for our room_index to reference to .word Room1,Room2,Room3,Room4 ;------------------------------------------------ Now, remember the value "room_index" that we used in the background switching routine? Well, not it's not set in our switching commands, so we need to find it using our Y_coord and X_coord values. Let's write the following: ;------------------------------------------------ .Find_Room_Index: LDA Y_coord ;load the coordinates we set in our movement routines LDX X_coord CPX #$00 ;if we are in the first column, the Y coord is the room index BEQ .done .loop: ;for each X, add 2 to the index ;note that this would change depending on your map configuration CLC ;there are only 2 values that Y can take on, so we use 2. ADC #$02 ;if there were values 0-5 that Y could be, we'd use 6, etc. DEX CPX #$00 BNE .loop .done ASL A ;indexing to a table of words...careful here!!! See **. STA room_index ;store it in the variable and we're done **If you have more than 128 rooms and you double it, you'll over run and flip back past zero. It is a simple problem to get around, just keep it in mind. ;------------------------------------------------ This is a very simple routine, but it took me a while (and a lot of help from metalslime) to get it working properly. Study it and make sure you understand what we're doing. Add more rooms and modify it to fit. It's powerful, but can be a tricky bastard. The rest of the background loading is exactly the same as the Compression write up. Now we need to set up our program to call the routine when we want to switch rooms. Again, nothing too complex. First, note that all four directions are pretty much the same, so we'll just go over one to save time. Modifying our "UpMovement" routine, we write: ;------------------------------------------------ UpMovement: LDA #$00 ;load the appropriate direction into the direction flag STA enemy_direction LDA sprite_RAM ;move the character SEC SBC #enemy_speed STA sprite_RAM INC Enemy_Animation ;incriment the animation counter ;;;;;;;;;insert;;;;;;;;;;;;; LDA sprite_RAM ;check to see if we are at the top of the screen CMP #top_exit BCS .done ;if not, we are done. If so, we need to switch rooms DEC Y_coord ;DEC the Y coordinate of the world map LDA #$BF ;move our character so that it looks like he is walking across the screen STA sprite_RAM ;this is another random value, set whatever you like JSR update_enemy_sprites ;update the sprite meta tile JSR LoadbgBackground ;using the new Y_coord, update the background to the new room .done ;;;;;;;;;insert;;;;;;;;;;;;; RTS ;------------------------------------------------ Simple like NGD. Hmmm...what else?? Oh, yes. Remember that this routine is set up to switch rooms any time you impact an edge of the screen. There is currently nothing stoping you from entering a room that doesn't exist. Until we fix this little issue, just notice where the background is set up to have solid walls...and try not to walk through them. CPU USAGE BAR REVISITED Well, given my status as a noob, I screwed up the placement of the call for the showCPUUsageBar command. We need to put it at the end of our Forever loop to show the end of the program. You can put in as many bars as you like, but I'm mostly interested in how long the total thing takes to run. You'll notice that it starts to show up here. Our program is getting some to it. CHR_RAM PART 2 - BACKGROUND ANIMATION Now that we have the tools in place to mess with CHR_RAM, we can add some interesting effects. Most of the CHR_RAM effects can be done using MMC3 CHR bank switching faster and easier than this, but I'm too lazy to figure out MMC3 and it won't work on the powerpak anyway. The quickest example that comes to mind are the moving ? marks in Mario 3. Open the ROM and look at the PPU viewer. You'll see the bottom part of the background data change instantly every frame. This is the bank switching that I'm talking about. The technique that we are using doesn't happen instantly like bank switching does, so we have to be careful not to run out of NMI. You can load something like 8-9 tiles during each NMI if you code it efficiently enough. I've only been able to get 6 to work (on NTSC, PAL works a lot better for this stuff) and I need time for other background updates, so I stick to 3 or 4 tiles per frame. In this example, we are going to use 4 tiles/frame and attempt to duplicate the scrolling ? marks of Mario 3 with Mario 1 graphics. Awesomeness. The first step is simple. We need to create CHR data for each frame of the animation that we want to use. Tile Molestor to the rescue!! Simply create a new file using the methods discussed in a previous write up and you'll be on your way. Simply cut and paste the 4 tiles that make up the ? mark block out of our existing CHR file and paste it into your new one. Save it. Then use the handy button that Tile Molestor that shifts all the pixels in the area that you highlight one pixel right/left/up/down. This is a handy button set, up to this write up, I've been entering every frame manually. Then save the next frame as a new file, and so on till you get every frame saved in its own file. I guess that I should point out that you could technically store all of this in one file and just specify the appropriate starting address, but I like it this way so blah. After we've created all of our files for each frame, we need to include them in our program with some sort of word table to pull addresses from. Simply: ;------------------------------------------------ TileUpdates: .word Question1,Question2,Question3,Question4,Question5,Question6,Question7,Question8 .word Question9,Question10,Question11,Question12,Question13,Question14,Question15,Question16 Question1: ;this is all the data for the various ? block frames .incbin "CHR_Files/Question1.chr" Question2: .incbin "CHR_Files/Question2.chr" Question3: .incbin "CHR_Files/Question3.chr" Question4: .incbin "CHR_Files/Question4.chr" Question5: .incbin "CHR_Files/Question5.chr" Question6: .incbin "CHR_Files/Question6.chr" Question7: .incbin "CHR_Files/Question7.chr" Question8: .incbin "CHR_Files/Question8.chr" Question9: .incbin "CHR_Files/Question9.chr" Question10: .incbin "CHR_Files/Question10.chr" Question11: .incbin "CHR_Files/Question11.chr" Question12: .incbin "CHR_Files/Question12.chr" Question13: .incbin "CHR_Files/Question13.chr" Question14: .incbin "CHR_Files/Question14.chr" Question15: .incbin "CHR_Files/Question15.chr" Question16: .incbin "CHR_Files/Question16.chr" ;------------------------------------------------ Note here that if you have more than one place to write the various tiles to, you'll need to include some sort of "write to" address table. We're not going to cover this here as it's fairly easy to modify. Now that we have all of our files lined up like so, we need some sort of thingie to input them to the background. Now we need to come up with the starting PPU address to write the tiles to. Hmmm? Break out the pencil and paper and do some math? Nope. Lazy people unite!! If you open your ROM and open the PPU, there is a simple trick to finding PPU addresses in CHR space. Remember that we are using $0000 as sprites and $1000 as background. If you put your mouse over the tile where you want to write to, it will display the tile number at the bottom. This is all you need: -Writing to tile $56 in the sprite space? The starting address is $0560. -Writing to tile $F4 in the background space? The starting address is $1F40. -Writing to tile $56 in the background space? The starting address is $1560. See the pattern?? Turns out that we want to start writing our ? mark data to tile $53, so we start with PPU address $1530. Now we have everything set up and all we need to do is write a quick code snippet and we're done. Technically you could use the same code for the complete bank here, but we are going to do this because I like it. Remember: NMI time is special. There's not much of it and more than likely, this CHR_RAM stuff is not the only thing happening during that time. So, we need to get everything set and ready to go so all NMI has to do is send data to the PPU. MetalSlime does this in his tutorials by buffering writes. I'm going to do this in a little different way, but it's basically the same principal. At the end of the main program, we will set up all the pointers and everything for out next frame of animation. Then when NMI hits, we simply write the address to $2006 and dump the data into $2007. Let's set up our pointers. To do that we use the following routine: ;------------------------------------------------ PartialBankSetUp: ;load the address that we want to write to. See the tip in the write up. LDA #$15 STA tile_loader_addy LDA #$30 STA tile_loader_addy+1 LDA #$40 ;load the number of entries. #$10 for each tile to load. STA tile_loader_stop LDA tile_loader_counter ;load the pointer to the information ASL A TAX LDA TileUpdates,X STA tile_loader_ptr LDA TileUpdates+1,X STA tile_loader_ptr+1 INC tile_loader_counter ;handle the counters, so that the next frame loads next time INC tile_loader_counter+1 ;This variable is used when you are loading a different number of tiles INC tile_loader_counter+1 ;to different addresses. Not really needed here. INC tile_loader_counter+1 LDA tile_loader_counter ;reset if we get to the last frame. CMP #$10 BNE .done LDA #$00 STA tile_loader_counter STA tile_loader_counter+1 .done RTS ;------------------------------------------------ Step by step, here's what we are doing here: 1. We need to specify the address that we want to send the CHR data to. This is hard coded here because we are ONLY WRITING TO ONE PLACE!! Normally, you'd set up a table of addresses for the program to write to. This is the function of the currently useless "tile_loader_counter+1" variable, to pull data from this table. 2. Specify the number of tiles that we want to send to the PPU. Remember from previous lessons that writing one tile is $10, 2 is $20, etc. Again, you'd have a table with the quantities of tiles to be loaded each frame. Another function of the "tile_loader_counter+1" variable. Note: You can see that tile_loader_counter+1 is INC 3 times per frame. If you wanted to set this up a little more complicated using this other variable, you'd do this: ;------------------------------------------------ LDX tile_loader_counter+1 ;load the addresses LDA LoaderSpecs,X STA tile_loader_addy LDA LoaderSpecs+1,X STA tile_loader_addy+1 LDA Specs+2,X ;load the number of entries STA tile_loader_stop -with- LoaderSpecs: .db $15,$30,$40,etc for each frame ;------------------------------------------------ 3. Using the frame counter, tile_loader_counter, you load the starting address for the next CHR data block into the data pointer using the "TileUpdates" table that we laid out before. 4. Set up the frame counter and address/quantity data counter for the next animation frame. 5. Reset the counters if you are on the last frame of animation. We should now be set up to load our data into the PPU during the next NMI. So, we write the shortest, most efficient sub-routine we can make and run it during NMI. Simply: ;------------------------------------------------ LoadPartialBank: LDA $2002 LDA tile_loader_addy ;input the address STA $2006 LDA tile_loader_addy+1 STA $2006 LDY #$00 .LoadBank: LDA [tile_loader_ptr],y ;load the data to the CHR space DURING NMI STA $2007 INY CPY tile_loader_stop BNE .LoadBank RTS ;------------------------------------------------ That's it. Now we should have a scrolling ? mark block everywhere we specify it in our background. So, now all our room data should look the same. A meta tile bank specification, a nice rectangle of meta tiles, then a $FF signifying the end of the data string. Basically: ;------------------------------------------------ Room1: .db %11000000 .db $08,$03,$03,$04,$04,$04,$03,$03,$03,$03,$04,$04,$04,$03,$03,$03 .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06 .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06 .db $08,$04,$04,$04,$04,$04,$04,$06,$06,$04,$04,$04,$04,$04,$04,$06 .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06 .db $08,$00,$06,$00,$06,$00,$06,$00,$00,$06,$00,$06,$00,$06,$00,$06 .db $08,$02,$02,$02,$02,$02,$06,$02,$02,$06,$02,$02,$02,$02,$02,$06 .db $08,$03,$03,$04,$04,$04,$06,$03,$03,$06,$04,$04,$04,$03,$03,$06 .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06 .db $08,$04,$04,$06,$04,$04,$04,$04,$04,$04,$04,$04,$06,$04,$04,$06 .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06 .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06 .db $08,$05,$06,$05,$06,$06,$05,$06,$06,$05,$06,$06,$05,$06,$05,$06 .db $08,$07,$07,$07,$07,$07,$07,$06,$06,$07,$07,$07,$07,$07,$07,$07 .db $08,$08,$08,$08,$08,$08,$08,$06,$06,$08,$08,$08,$08,$08,$08,$08 .db $FF ;------------------------------------------------ See how it is nice and even and perfect for looking up data? Sweet. And it still takes us from 960 bytes per room to 240, a size reduction of 75% (not counting meta tiles). Then we go back and make our room data look like this, then we're set for our next challenge. CONCLUSION Next time we will tackle simple collision detection with the background. This will basically be the money write up, the place where most people get stuck. Stay tuned and post comments/questions if you have them. If you don't understand everything to date, the next stuff will be trouble for you. Until next time.... Thanks for reading. background_set_up.zip
  10. BACKGROUND First off let me say that I am a noob programmer and I don't know what I'm doing. However, this tutorial should give you a good start towards simple background compression. Also, there is a minor bug in the final program. I'm tired of messing with it, but if someone finds it, I'd love to fix it. This tutorial also assumes a working knowledge of NES programming. The purpose of background compression is just that: to make your program smaller. It works best with a large number of rooms, but can be beneficial to most anything. For example, in my personal program, I did not design it to support this routine, and as a result, the compression took up a shit ton more space than it would have knowing what I know now, but it still shrank the stuff down to 25% of its original size. In this example, we will make 4 rooms. Uncompressed, this would run you 960 bytes per room (or 3840 bytes total). Compressed, the 4 rooms take up about 663 bytes, or 17.3% of their original size. The point is obvious. Compressing stuff is not very complicated, there are just a bunch of little steps. In this example, we are going to steal and build on BunnyBoy's "background" program and "mario" CHR file. Assuming that you are making your own game, you would want to make all of your tiles and put them into a CHR file like the mario one that we are using. You want to be able to view them in a PPU viewer for use in construction of meta tiles that will come later. We see that most of the 8*8 tiles in this tile set are used to construct 16*16 blocks. We just take this natural tendency and use it to make stuff smaller. Once you have your nice tile set, we need to make what are called "Meta Tiles". Or, make rooms. Note that you can have up to 128 meta tiles in each meta tile "bank". But what if you needed more than 128, like 1000??? You will need multiple meta tile banks, this will come later. I just split them up this way to demonstrate the multiple bank function. I'm not sure how to explain this better than the picture. You can see how neatly the tiles fit into meta tiles and Walla! Now you only have to specify one byte for every 4 bytes written to the PPU! Savvy? After you have all of your meta tiles defined, start building rooms. You can build as many rooms as you have the patience to build. We will look at this example: You can see that you have this giant ass room made up of 8 meta tiles. Easy, no? META TILE BANK SWITCHING First, let me explain the meta tile bank switching flag. For this, we will use binary (only to set it apart and make it easy to see what we are doing). For each meta tile bank, we need to assign a number. We are going to call the top one, creatively enough, "MetaTileSeta" and the second "MetaTileSetb". This will come later, but we need to use the following for these meta tiles sets: MetaTileSeta = %11000000 MetaTileSetb = %11000001 The first two bytes specify stuff that will come later and the bottom ones specify the meta tile bank number, see that a is bank 0 and b is bank 1. REPEATING META TILES Okay, next we need to specify repeats of meta tiles. If you look at the picture, there are a bunch of places where meta tiles are repeated. So, for the top row, wouldn't it be easier to have only a repeat number and the tile number rather than having to repeat the tile number 16 times?? The thing to note is that the way I have my program set up, you need to specify the number of repeats - 1. (i.e. if you want to repeat it 16 times, you have to use 15, 3 times, use 2, etc.) Here we are going to use "binary" as well, but with this one, we only need to have bit 7 = 1, bit 6 = 0, and the other 6 can be used to specify the repeat numbers. So, this makes it easier to use $8(repeat number). Or: Repeat 6 times, $85 Repeat 16 times, $8F When specifying meta tiles, you will only go up to $7F in any given bank, so the 8 stands out just like the binary. SPECIFYING META TILES Okay, now we know how to specify repeats and tile set switching sets, what about meta tiles? Well, this is the easy part. See the fancy picture? The numbers down the side are the first digit in hex and the numbers across the top are the second. EX: Tile in row 5 and column 9 would be $59 Tile in row 0 and column 0 would be $00 and so on. COMPRESSING A ROOM We'll learn how to read this data later, but for now let's make a room and we'll hopefully understand it a little better. Looking back at that picture, we see that there are rows of tiles. I have found that it makes sense to me to leave the data string in these rows for later debugging and/or copying parts of rooms into other rooms. So, first we need to specify the meta tile bank for the program to pull meta tiles from. So, here we see that the first tile is $06 of the first bank and it is repeated 16 times. So, the first row would be: .db %11000000(bank 0),$8F(repeat 16 times),$06(the tile in row 0, column 6) Pretty simple, right? Second row: .db $06,$03,$03,$82,$04,$83,$03,$82,$04,$03,$03,$06 See if you can decode what the data string is saying. If you can't, the rest of this won't make sense. Please re-read up to this point until you get it, or ask me a question. Note that you only need to specify meta tile bank switches when you switch banks, not every row. So, the completed room data string would look like this: .db %11000000,$8F,$06 .db $06,$03,$03,$82,$04,$83,$03,$82,$04,$03,$03,$06 .db $8F,$06 .db $06,$8D,$04,$06 .db $8F,$06 .db $06,$00,$06,$00,$06,$00,$06,$00,$06,$00,$06,$00,$06,$00,$06,$06 .db $06,$8D,$02,$06 .db $06,$03,$03,$82,$04,$83,$03,$82,$04,$03,$03,$06 .db $8F,$06 .db $06,$8D,$04,$06 .db $8F,$06 .db $8F,$06 .db $06,$05,$06,$05,$06,$06,$05,$06,$06,$05,$06,$06,$05,$06,$05,$06 .db %11000001,$8F,$00 .db $8F,$01,$FF Again, if you can't decode what this means, you should go back and study a little more, cause the rest of this won't make sense to you. THE "$FF" FLAG Also, we see the first appearance of $FF. This simply signifies the end of a data string and tells the subroutine that it is done. Poweryay!!! Our first room! CODING IN THE META TILES Okay, now we have this happy little string, what do we do with it? Well, here we will learn how to tell the processor to decompress the stream and write to the PPU. First, we will cover basic decompression. If you didn't use any bank switches, or repeats, your program would look pretty simple. Here, you need to code in your meta tiles. This is the part where having your plain tiles in the PPU will be most helpful (especially if you have the afore mentioned 1000.) Pretty simple, you simply take the top row then the bottom row in a series of bytes using the normal tile numbers from your original tile set (i.e. what you see in the PPU viewer). For example, meta tile bank a: MetaTileSeta00: .db $44,$49,$5F,$7A MetaTileSeta01 .db $AB,$AD,$AC,$AE MetaTileSeta02 .db $53,$54,$55,$56 ...and so on till you get all of them done. I usually keep them at the top of my meta tiles in ROM space for easy access, but you can put the following wherever. You can see that you will have to use some sort of look up table to find the data for each meta tile, so we may as well do this now. ;------------- MetaTileSeta: .word MetaTileSeta00,MetaTileSeta01,MetaTileSeta02,MetaTileSeta03,MetaTileSeta04,MetaTileSeta05,MetaTileSeta06 ;------------- MetaTileSetb: .word MetaTileSetb00,MetaTileSetb01,MetaTileSetb02,MetaTileSetb03,MetaTileSetb04,MetaTileSetb05,MetaTileSetb06 ;------------- Note that this is a table of addresses! This is why you can only put 128 meta tiles in each bank (even if we are using Deniz sized tile sets here, the full sets would be bigger....) And eventually, you will need a look up table for the meta tile bank you want the program to read from, so we may as well put that here as well: ;------------- meta_tile_sets: .word MetaTileSeta,MetaTileSetb ;------------- Make sense so far? At this point, we have all the data we need to decode all the data streams. Now, onto actual decoding. SIMPLE DECOMPRESSION For reasons of my sanity, I will skip the stuff that I think is pretty basic...like disabling NMI while writing to the background, turning off the PPU, specifying which room to load, and how to set up the PPU to receive a new room. This will focus on the important decompression stuff. If we didn't have any repeats, or bank switches, we would be able to hard code the pointers and that would be that, let's assume for a moment that is the case. When writing our meta tiles to the background, we run into the problem of trying to write two rows at one time, when the PPU accepts data only one row at a time.... What to do? Here we should buffer our writes. So, we need to reserve 32 variables in zerospace, one for each tile what we are writing to the background. background_row .rs $20 ;the second row that each meta tile contains So, now we take our meta tile and write the first two entries to the PPU, then the second two entries to our buffer. Now, to keep our positions in the meta tile and in the buffer straight, we need to use the registers X and Y. We need to reserve Y for pointer use, so X will be used for the buffer. To keep them straight, we will use simply: data_y .rs 1 ;y counter data_x .rs 1 ;x counter So, after our buffer fills up, we need to write the second row to the PPU, then jump to the next row of meta tiles. This is pretty easy, so I won't go into it. (It should be noted here that the Y register is used for multiple things. It uses the data_y for reading the room data stream, and here we see that it uses the register y to decode the meta tiles.) Here is our code up to now: ;------------- decompress: LDY #$00 LDX data_x LDA [meta_tile_ptr],y ;write the first tile of the meta tile to 2007 STA $2007 INY LDA [meta_tile_ptr],y ;write the second tile of the meta tile to 2007 STA $2007 INY LDA [meta_tile_ptr],y ;write the third tile of the meta tile to memory for later use STA background_row,x INY INX LDA [meta_tile_ptr],y ;write the forth tile of the meta tile to memory for later use STA background_row,x INX STX data_x CPX #$20 ;see if we are at the end of a row. If so, we need to use the tiles stored in memory, if not, jump to the top. BEQ .next JMP background_start .next LDY #$00 row_start: ;write the tiles stored in memory to the background LDA background_row,y STA $2007 INY CPY #$20 BNE row_start LDX #$00 STX data_x JMP background_start ;jump back to the start of the routine and continue on ;------------- Not all that difficult, got it? Okay, now onto bit testing. Back when we specified our repeats, end of data string, and bank switches, we set up the data string to be bit tested. First, the end of the string. Simply tell the routine that if it encounters: %11111111 it is done and that it should "RTS". Simple enough. Next, we tell the program that if it encounters: %1xxxxxxx it needs to do something special. %10xxxxxx means that it needs to repeat the meta tile %11xxxxxx means that it needs to switch meta tile banks I guess rather than my beating it to death, it would be easier for you to spend a couple minutes with this part of the program and either make a flow chart or just convince yourself that it is working. It should be noted that there are 3 pointers at work here: ptr1 .rs 2 ;pointer to the background information. This is loaded in the part we skipped ;about which room you want to load at a given time meta_tile_sets_ptr .rs 2 ;pointer to which meta tile set you want to read from at any given time meta_tile_ptr .rs 2 ;pointer to the meta tile/repeat number/bank number in the data stream Take a look through this bit of code and see if you understand what it is doing. If not, please ask questions. Here we need to define a variable for the number of meta tile repeats. repeat_meta_tile .rs 1 ;times to repeat the current meta tile All this section of code does is: -test if you are in the middle of repeating meta tiles, if so repeat till it is done -test if you are at the end of a data stream, if so, skip to the end -test if it is a repeated meta tile or a bank switch -test if it is a switch, if so switch meta tile banks and jump back to the start and load the next entry in the stream -if it is not a switch, it is a repeat, load the number of repeats and load the next meta tile in the stream -then decompress the data as shown above ;------------- background_start: LDA repeat_meta_tile ;see if you are repeating a tile BEQ .jump DEC repeat_meta_tile ;decrement the repeated tile and jump back to the decompression routine JMP decompress .jump LDY data_y LDA [ptr1],y CMP #$FF ;see if it is at the end of the data string BNE .next JMP background_complete .next LDA [ptr1],y AND #%10000000 ;see if this is a repeated tile or a switch of meta tile banks BNE .next1 JMP .loadtile .next1 LDA [ptr1],y AND #%01000000 ;see if this is a switch of the meta tile banks BNE .next2 JMP .repeat .next2 LDA [ptr1],y AND #%00111111 ;switch meta tile banks ASL A TAX LDA meta_tile_sets,x ;load the new meta tile bank STA meta_tile_sets_ptr LDA meta_tile_sets+1,x STA meta_tile_sets_ptr+1 INY STY data_y JMP background_start ;jump to the next entry in the data table and return to the start of the routine .repeat LDA [ptr1],y AND #%00111111 ;load number of repeats for this meta tile STA repeat_meta_tile ;note that this is set up so that you need to have the number of repeats - 1 INY STY data_y .loadtile LDA [ptr1],y ;load the meta tile number INY STY data_y ASL A ;this is a table of addresses, so multiply by 2 TAY LDA [meta_tile_sets_ptr],y ;load the address of the meta tile in question STA meta_tile_ptr INY LDA [meta_tile_sets_ptr],y STA meta_tile_ptr+1 decompress: ;------------- That's it. There's not much to it. For your convenience, you will find an attachment entitled "blankmetatiles". I made this when I was making my last backgrounds because I kept doing it over and over. If you label your meta tiles as we did here, all you have to do is paste "MetaTileSetX" in front of all the freaking letters and you're done. For practice, the attached picture "Graphics" has the other three rooms that are compressed in the attached program. I would suggest that you practice with these and then make your own. Using the D-PAD in the program will load each of the Four Rooms. Like I said at the top, there is a weird bug, but I'm too lazy to look for it. I guess that's it. Post here or PM me if you have questions. On a side note, I haven't got into scrolling too much, but a working example of compression and scrolling can be found: http://nintendoage.com/forum/messageview.cfm?catid=22&th... Thanks again! Until next time... Background Compression.zip
  11. INTRODUCTION Last time we added weapons to our playable character, but they didn't do anything. This time we are going to add some depth to our game by adding impacts between: 1. NPCs and PCs 2. Our projectiles and NPCs 3. Our whoop-ass stick and NPCs. RESULT OF A HIT First, we need to define what we do when we register a hit. In practice, this would be a fairly complicated routine involving death checks, hit points, moving, etc., but here we are going to keep it simple and you can add stuff as you see fit. This is the subroutine that we are going to jump to when we register a hit: ;----------------------------------------------------------------------------------------------------- direction_change: ;this is just a random routine to make something happen when a collision occurs LDX enemy_number LDA random_direction STA enemy_direction,x RTS ;----------------------------------------------------------------------------------------------------- All we do here is if there is a hit, make the enemy change direction. That's it. Just take the random direction we have stored in our random direction counter and store it in the active enemy direction. It should be noted that in some cases the random direction you input into your character's direction could very well be the direction he has been traveling. You could add code to fix this if you like. COLLISION AREAS Next, we need to define the area that we will use when we are looking for hits (or collisions). Consider the picture below: I highly recommend making a picture like this anytime you define collision coordinates. It makes it easy to see and keeps you "organized". First, draw out your playable character in all of its shooting poses. It is pretty obvious here, but as your characters get bigger and more complex, this will be harder to visualize. After we draw our pictures, we can see that there are 4 views for each weapon we need to consider. In this case, the 4 directions of the D-PAD. Then, draw an 8x8 pixel grid (or however big your weapon sprite is) as shown at the bottom of the picture. Then draw the various views of the weapon sprite area THAT YOU WANT TO REGISTER A HIT on top of this grid. Then draw a box around the sprites in the empty space of the grid as shown in this picture. This is your "HIT" area. Remember that the sprites on the NES are referenced from the upper left hand corner. So, we need to figure out what the offsets are in all four directions for each view. Sounds complicated, but it's really pretty simple. We'll start with the arrow facing up. Here, we see that our blue box extends all the way over the top of the last row of pixels. So, since we are referencing our collision coordinates to the top of the sprite, we would basically have the vertical arrow sprite location + 0 = top collision coordinate of the arrow. Then, we see that the box extends all the way past the bottom row of pixels, so we have vertical arrow sprite location + 8 = bottom collision coordinate of the arrow. Then, we see that we skip one pixel from the left side of the arrow sprite, so we would have the horizontal arrow sprite location + 1 = left collision coordinate of the arrow. The we skip 6 pixels before the right side of the box, or horizontal arrow sprite location + 6 = right collision coordinate of the arrow. That really makes it sound more difficult than it is. Basically we just count the pixels above and to the left of the blue lines. So, for each direction we would have: TOP,BOTTOM,LEFT,RIGHT Up View: 0,8,1,6 Down View: 0,8,1,6 Right View: 1,6,0,8 Left View: 1,6,0,8 STORING COLLISION INFO IN RAM Now, we've defined the offsets required for each direction that our little homie can shoot. Now...what the hell do we do with it?? Well, if we think back, we remember that we have a loop running for each of our enemies. We can either re-compute these coordinates for each enemy (each loop), or we can compute it once and store it somewhere in RAM. It's really up to you, but here we are going to store it in RAM. To do this, we will need to define some new variables: ;----------------------------------------------------------------------------------------------------- arrow_vertical .rs 2 ;the vertical collision coordinates we define for our arrow arrow_horizontal .rs 2 ;horizontal coordinates ;----------------------------------------------------------------------------------------------------- Next, we basically just say that for each one of these new variables, we take the sprite location, add the appropriate offset we defined above, and store it in one of these variables. Simple, no? Well, I'm having a hard time putting it into words, so let's look at the subroutine: ;----------------------------------------------------------------------------------------------------- arrow_transfer: LDY arrow_direction LDA Weapons_RAM+4 ;transfer arrow collision coordinates CLC ADC arrow_a,y STA arrow_vertical LDA Weapons_RAM+4 CLC ADC arrow_b,y STA arrow_vertical+1 LDA Weapons_RAM+7 CLC ADC arrow_c,y STA arrow_horizontal LDA Weapons_RAM+7 CLC ADC arrow_d,y STA arrow_horizontal+1 RTS ;----------------------------------------------------------------------------------------------------- All it is is load the sprite location, add a number, and dump it in the variables. However, since we have 4 different views to work with, we can't just add a constant and make it work right. We need to add some sort of look up table that stores the offset data that the program can access. Here, arrow_a is the offset for the top of the blue box in the picture for the up, down, right, left directions, respectively. arrow_b is the bottom, arrow_c is the left, and arrow_d is the right. If you study the offsets we came up with and the following table, you'll see the relationship between the two. Basically the transposition. If we put this into a neat table, we get: ;----------------------------------------------------------------------------------------------------- arrow_a: ;outlines of the arrowhead .db $00,$00,$01,$01 arrow_b: .db $08,$08,$06,$06 arrow_c: .db $01,$01,$00,$00 arrow_d: .db $06,$06,$08,$08 ;----------------------------------------------------------------------------------------------------- Now, we've got a routine that we can put somewhere that will define the position of the arrow on the screen and give us the collision boundries to check against the bad guy locations. The exercise is exactly the same for the hammer/mace. I'll leave that to you to figure out. It is good practice. The resulting table is in the program. The exercise for the Playable Character sprites is exactly the same with one exception. The playable character has the same "blue box" in every direction. This saves us the trouble of messing with the look up table and allows us to hard code the location info. However, if your character looks different in each direction, you may want to consider making a table for it as well. CALLING THE TRANSFER ROUTINES Now that we've defined how we are going to transfer the sprite collision data into RAM, we need to call it at the appropriate time in the main program. It's really up to you when you want to call it as it doesn't matter too much, but here we are going to call them after we finish handling the weapons routines, but right before we run the UpdateENEMIES loop. This sets us up nicely for what comes next. CALLING THE SPRITE COLLISION DETECTION ROUTINES I guess I should explain this here to avoid confusion. We need to put our collision data variables in a certian order in RAM to make the following collsion routine work correctly. Observe: ;----------------------------------------------------------------------------------------------------- arrow_vertical .rs 2 ;vertical position of the arrow for collision hammer_vertical .rs 2 ;hammer PC_vertical .rs 2 ;PC arrow_horizontal .rs 2 ;horizontal position of the arrow for collision hammer_horizontal .rs 2 ;hammer PC_horizontal .rs 2 ;PC ;----------------------------------------------------------------------------------------------------- Messing up the order of these will cause you to waste time debugging. You'll see why this is necessary in a second here. Next we need to update our "UpdateENEMIES" loop to call for sprite collision detection. It will be simplest to just show you: ;----------------------------------------------------------------------------------------------------- UpdateENEMIES: LDA #$01 ;this loop updates the enemies one at a time in a loop STA enemy_number ;start with enemy one LDA #$02 STA enemy_ptrnumber .loop JSR enemy_update ;move the sprites based on which direction they are headed JSR Enemys_Animation ;find out which frame the enemy animation is on JSR Enemys_Sprite_Loading ;update the enemy meta tile graphics JSR update_enemy_sprites ;update position ;;;;;;;insert;;;;;;;;; LDY #$00 ;arrow ;sprite collision detection routines JSR enemy_collision ;note that 00, 02, and 04 are to specify the variables of each weapon LDY #$02 ;mace JSR enemy_collision LDY #$04 ;Playable Character JSR enemy_collision ;;;;;;;insert;;;;;;;;; INC enemy_number ;incriment the enemy number for the next trip through the loop INC enemy_ptrnumber ;these are addresses for the graphics data, so we need to keep it at 2x the enemy number INC enemy_ptrnumber LDA enemy_number CMP #$04 ;if it is 4, we have updated enemies 0,1,2,3 so we are done BNE .loop UpdateENEMIESdone: ;----------------------------------------------------------------------------------------------------- Here we see why we need to put our variables in the correct order. We run the enemy_collision routine 3 times, one for each weapon type. First, we use index $00 for the arrow, index $02 for the mace, and index $04 for the playable character collision detection. Now, we're all set up to write some sort of routine to check for collisions. SPRITE COLLISION DETECTION After we've defined all the stuff above, the actual detection routine, the "man meat" if you will, is pretty straightforward. Remember the BCC and BCS commands we learned last time?? We need them here. If you don't get it, go back and review or ask a search engine or something. These are important. Again, it's probably easiest if I just lay out the sub-routine and comment the hell out of it. But, basically, all we are doing here is seeing if the "blue boxes" we defined above are intersecting with the "blue boxes" defined by the NPCs. Note that this routine assumes that the NPCs are basically the same as the PC. If you're enemies are of different styles, or have different looks depending on the direction they are traveling, it's not really any big deal, you just have to set up tables like we did before. Just base them on the enemy_direction and/or define an "enemy_type" variable to guide your program to the correct "blue box" data. Here, we are going to keep it simple. Oh, and I lied above. When we register a hit, we also have to tell the mace/arrow routines to dissapear the weapon sprites. I think that the following picture will help illustrate my little routine here. "Steps" refer to the comparisions in the following routine. You can see examples of where you would branch and where you would RTS (green boxes mean branch, red mean RTS). I think that this figure will make it a little clearer. See the following: ;----------------------------------------------------------------------------------------------------- enemy_collision: ;check collision with the arrows, mace, PC LDX enemy_number ;load the enemy number for this time through the UpdateENEMIES loop LDA updateconstants,X ;check the outline of the enemy vs. the weapon TAX ;change the constant to the X variable LDA sprite_RAM,X ;load the vertical position of the dominate enemy sprite (refer to last week for "dominate") CLC ;make sure that the carry flag is clear ADC #$01 ;add on the offset for the up direction for the NPC CMP arrow_vertical+1,y ;compare the top of the NPC with the bottom of the weapon in question BCC .next ;if the top of the NPC box is above the bottom of the weapon box, branch to .next RTS ;otherwise, there is no collision, so RTS .next LDA sprite_RAM,X ;load the vertical sprite position CLC ADC #$0E ;add the offset for the bottom of the metasprite CMP arrow_vertical,y ;compare with the top of the weapon blue box BCS .next1 ;if the bottom of the NPC box is below the top of the weapons box, branch to .next1 RTS ;otherwise, there is no collision, RTS ;inserting another comment here, so far, we have determined that at least 1 pixel of the defined collision boxes share the same vertical space on the screen. Now, we need to check and see if they share the same horizontal space. .next1 LDA sprite_RAM+3,X ;load the sprite horizontal position CLC ADC #$03 ;add the offset defined in our pictures with the blue box to get the left side of the metasprite CMP arrow_horizontal+1,y ;compare this the the right side of the weapon sprite BCC .next2 ;if the left side of the NPC box is to the left of the right side of the weapons box, branch to .next2 RTS ;otherwise, there is no collision, RTS .next2 LDA sprite_RAM+3,X ;load the enemy sprite horizontal position CLC ADC #$0C ;add the defined offset to get the right side of the enemy metasprite CMP arrow_horizontal,y ;compare to the left side of the weapons sprite BCS .next3 ;if the right side of the NPC is to the right of the left side of the weapons box, branch to .next3 RTS ;otherwise there is no hit, RTS ;inserting another comment here, in this previous snippet we determined that the weapons sprite and the enemy meta sprite share the same horizontal space on the screen. ;However, the top part of this subroutine showed us that we were also occuping the same vertical space. So, we know that if we are branching to .next3, the weapon sprite and the meta sprite are, in fact, occuping the same space on the screen!! .next3 ;now that we've confirmed a hit, what do we do? CPY #$00 ;we need to decide what weapon hit the duder, so remember $00 is for arrows BNE .nexts ;did we go over this? Branch Not Equal (i.e. not an arrow) to .nexts JMP arrow_hit ;do the arrow collision stuff .nexts CPY #$02 ;is it the mace? BNE .nexts1 ;again, Branch Not Equal to .nexts1 JMP mace_hit .nexts1 JMP PC_hit ;add more here if there are more weapons, or make a look up table, but all that's left is the PC impact, so jump to it! ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; mace_hit: JSR direction_change LDA #$01 ;set the mace timer to $01 to reset the mace graphics. STA B_timer RTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; arrow_hit: LDA #$FE ;here we are just making the arrow inactive. STA Weapons_RAM+4 ;see the arrow routine to see how this works. STA Weapons_RAM+7 JSR direction_change RTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; PC_hit: JSR direction_change RTS ;----------------------------------------------------------------------------------------------------- So: 1. Determine if the sprites are overlapping in the vertical direction. a. If not, RTS. b. If so, go on to #2. 2. Determine if the sprites are overlapping in the horizontal direction. a. If not, RTS. b. If so, go on to #3. 3. Determine which weapon we are attacking with. 4. Do the collision shit for the various weapons. Easy as cake. If you have any questions, feel free to PM me or post. If you don't understand what we did here, we need to figure it out. This is important for stuff to come later. DOWNLOAD THE PROGRAM Now we should have all the tools we need to make our program recognize sprite collisions. Download the zip file and mess around with the ROM. Change the offsets in the tables and see how it changes the collision areas. Maybe even add some other thing for the program to do with each weapon hit, or add health, or add in your own weapons. Whatever tickles your . Next we'll start working with backgrounds. Please let me know if you have any questions. Good luck. Sprite Collisions.zip
  12. This expands on #2 where we added a playable character to our game and some random content. INTRODUCTION This time we will talk about adding two kinds of weapons to our playable character's um...playability. You'll have to excuse the crappy graphics. For me, building successful sprites takes the most time and effort out of all the skills required for programming. As a result, I just halfassed these to get my point across. If you want to, you can alter the program to include your own sprites. In #1 we talked about building sprites and tiles. However, it didn't even cross my mind that using Tile Molestor would not be something that most people would know how to do. We'll correct that oversight and cover it briefly here. But at the end of this write up we will have added a projectile type weapon and a sword type weapon to our character that will be activated using the A and B buttons. TILE MOLESTOR I do most of my actual graphics design in MS Paint. It is familar and easy to use. After I get everything the way I want it and split it into tiles, only then do I input everything into Tile Molestor. So, let's think about what we'll need as far as sprites for our character. 1. We need a shooting pose for 4 directions. (We could technically make a different pose for projectile and sword, but the process is exactly the same and sloth wins out, so we are going to use the same sprites for both weapons. 2. We need a sword design. Keep in mind that the weapons design HAS TO MATE UP with the shooting pose from point 1. Also, with 4 directions, we'll need two sprites. An up/down mirrored sprite as well as a left/right mirrored sprite. 3. We need a projectile design. See notes in point 2. Following the procedeure laid out in write up 1, we construct the following: Armed with our sprites, we head over to Tile Molestor. Heh, molestor. You don't need all this info for this write up, but we'll need it later, so shut up and listen. An NES pallette has 4 colors that can be applied to each pixel of each tile. This jives with the attributes that we input for tiles in that we get two bits with which to specify info. However, what you are specifying in the attributes is the sprite pallette, not the pixel colors. To get into pixel colors, we have to go deeper. To specify the colors for each pixel, we need two bits per pixel (i.e. color 1,2,3,4 of our pallette, or 00,01,10,11). Make sense so far? Now, each tile is 8x8 pixels, or 64 total pixels. If we multiply this out, 2 bits per pixel X 64 total pixels = 128 bits or 16 Bytes per tile. When you click "NEW File" in Tile Molestor, this is the size it prompts you for. So, if you want a file with 6 tiles, you would put in 6 tiles X 16 Bytes/tile = 96; or 28 tiles X 16 Bytes/tile = 448. Easy as American Pie. The defaults in TM are not set up for the NES. You need to change the Codec to the code that the NES needs. Go to View>Codec>2bpp planar, composite. This is demonstrated in the following pic: Now we should be set up to input NES graphics. However, it just gives you a blank, black block where you input your tiles. I like to select the "View Tile Grid" as shown in the above pic. This keeps you from offsetting your graphics by 1 pixel or something retarted. Next, you can use the default pallette shown at the bottom, or if you like, to make it easier, you can specify your own. You can do one of two things. Either click on the arrow buttons on the sides of the pallettes to use a standard one or double click on each color to specify your own pallette as shown here: Now, if someone would add a cut from paint paste into Tile Molestor, I would gladly impregnate them. But so far, I haven't figured that one out. So, just go through pixel by pixel and transfer your stuff into the file. I like to do the outlines and then fill in the solid colors. Just try to keep your tiles easy to find and input into our sprite tables. The rest of the controls work more or less like paint, so just let me know if you have any other questions. HANDLING INPUT First thing that we need to do is handle the input from the A and B buttons. To do this, we are going to try to keep it as simple as possible. All we are going to have the buttons do is set timers. That's it. We want our routines to be seperate entities so that we can reuse them without having to dig around for code. So, we do this: ReadA: LDA joypad1_pressed ; player 1 - A AND #%10000000 ; only look at bit 8 BEQ ReadADone LDA Weapons_RAM+4 ;see if the arrow is ready to fire again CMP #$FE BNE ReadADone LDA #Atimer ;reset the timer so that the arrow is fired next frame STA A_timer ReadADone: ; handling this button is done ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ReadB: LDA joypad1_pressed ; player 1 - B AND #%01000000 ; only look at bit 7 BEQ ReadBDone LDA B_timer ;check and see if the mace is still out BNE ReadBDone LDA #Btimer ;reset the timer if it was not pressed last time, but is this time STA B_timer ReadBDone: ;handling this button is done All these routines do is input a number (in this case a constant declared in our constants file) into the timer. A checks to see if the arrow has hit something and has been reset before firing again and B checks to make sure that we don't try to hammer something again before the previous ass kicking has finished. Note that from here on out I will assume that you know what I'm talking about with all the new variables and/or constants. It feels redundant restating them all. When we input these constants into the timer, we need an independant way of counting down the timers. So, we put a simple routine in our GameState0 routine AFTER the PC updates but BEFORE the NPC updates. The order of operations is important here because if you put the weapons updates after the enemy updates, you'll have a delay before they kick in and it could mean the difference between life and death. This way everything activates at the same time and your timing is right on. So, we add the following: JSR update_enemy_sprites ~~~~~~~~~~~~~~~~~~~~~ JSR transfer_location_info JSR arrow arrow_dec: LDA A_timer BEQ .done DEC A_timer .done JSR mace mace_dec: LDA B_timer BEQ .done DEC B_timer .done ~~~~~~~~~~~~~~~~~~~~~ UpdateENEMIES: Pretty simple right? TRANSFER LOCATION INFO Technically we don't need this yet, but we are going to put it in here to save us from having to hunt down and change a bunch of variables later. You could use the ",x" indexing method for everything that this does, but with everything that we are going to use it for, it will save processor time and ROM space to use this quick transfer rouitine. All it does is loads the position info for the "dominate" sprite into new variables called "sprite_vertical" and "sprite_horizontal". Now that we have these we can write ALL of the routines that we would normally loop all of our enemies/PC/etc. through relative to these variables. Just keeps it less confusing. (Note that I refer to "dominate sprite". I keep everything the same, meaning that all meta sprites are referenced to the upper left hand corner of the top left sprite tile.) transfer_location_info: LDX enemy_number LDA updateconstants,x TAX LDA sprite_RAM,x STA sprite_vertical LDA sprite_RAM+3,x STA sprite_horizontal RTS Later we'll add what amounts to the opposite of this routine. Collision detection will operate on the variables then at the end we will transfer the variables back to Sprite_RAM. In this write up, however, weapon positions are measured relative to "sprite_vertical" and "sprite_horizontal". BRANCH CARRY SET and BRANCH CARRY CLEAR COMMANDS I don't know if these are covered elsewhere, but we'll need them A LOT here, so be sure you understand these. Branch Carry Set or BCS: For this instruction, you would use something like this: LDA variable CMP #$08 BCS .next On the CMP function (or CPX or CPY), the carry flag is set when A is greater than or equal to the value placed in the CMP line, in this case, #$08. Or, if A is less than #$08, the flag will be cleared. So, BCS is telling the thing to: -if variable is less than #$08, Carry is CLEAR, so do nothing -if variable is equal to #$08, Carry is SET, so jump to .next -if variable is greater than #$08, Carry is SET, so jump to .next Branch Carry Clear or BCC: This is basically the same as BCS, but branch when clear rather than set. So, we'd write: LDA variable CMP #$08 BCC .next On the CMP function (or CPX or CPY), the carry flag is set when A is greater than or equal to the value placed in the CMP line, in this case, #$08. Or, if A is less than #$08, the flag will be cleared. So, BCC is telling the thing to: -if variable is less than #$08, Carry is CLEAR, so jump to .next -if variable is equal to #$08, Carry is SET, so do nothing -if variable is greater than #$08, Carry is SET, so do nothing Simple, easy, and powerful. Defenatly something you'll want to add to your toolbox. There is also a BMI (Branch Minus) and BPL (Branch Plus), but I don't use them that much, if ever, so I'll leave that up to you to figure out. ARROW ROUTINE Now that we have everything set up, we can start working on our arrow routine. Strive on. Remember: control over the NES is won, not given. Basically we need to impliment the following steps in our program: 1. Check and see if the arrow is inactive (i.e. sitting with #$FE for its vertical and horizontal position) A. If it's inactive a. See if our A timer is equal to the value input by pressing A. Yes=load the starting position and graphics. No=RTS b. If it is NOT equal the the #Atimer and NOT active, the arrow is not being used. Do nothing. B. If it's active. a. Check which direction the arrow is moving and move it in that direction the value that we have specified for our #arrow_speed. 2. Check the arrow collision with the walls. (Just pick an arbatray position for the walls or our arrow will continue to loop around endlessly) A. Reset if it hits a wall. B. Do nothing. (We'll add sprite collision here-ish later) 3. Is the A_timer is greater than or equal to zero? A. We need to impliment our shiny new graphics that we just made for shooting poses. B. The arrow is already away and we don't need to do anything else. Note: We will use a constant similar to "Sprite_RAM" for our weapons sprite. In this case, "Weapons_RAM". Here, we use the sword weapon as Weapons_RAM+0,+1,+2,+3 and arrow as Weapons_RAM+4,+5,+6,+7. Let's assume that the arrow is inactive first. We push the A button resetting our A_timer. Our routine jumps to the loading of the arrow's initial position. Note that there will be a similar routine for each of the 4 directions, but they are pretty much the same, so we'll just go through the one here: LDA enemy_direction ;load the direction that our character is facing/traveling STA arrow_direction ;store this direction in the variable for use in when the arrow is traveling CMP #$00 ;our value for UP BNE .nexts ;if not facing UP, branch to the next direction LDA #$1E ;load the tile for the up/down arrow STA Weapons_RAM+5 ;store the tile in the appropriate place in RAM LDA #%00000010 ;load the attributes for the arrow STA Weapons_RAM+6 ;store in the appropriate place in RAM LDA sprite_vertical ;load our dominate sprite's vertical position SEC SBC #$08 ;here we are just making the arrow line up with our character STA Weapons_RAM+4 ;store the apprpriate vertical position in RAM LDA sprite_horizontal ;load the dominate sprite's horizontal position SEC SBC #$01 ;here we are just making the arrow line up with our character STA Weapons_RAM+7 ;store the apprpriate vertical position in RAM JMP .graphics ;jump down to the graphics updates for our PC meta tile This is a pretty simple routine. The only thing a little challenging is the position changes when specifying the initial arrow location. You could technically work this out on paper, but I'm lazy and like to just adjust it until it lines up. You know, hump it into submission and show it who's the bitch. Now that we have our loaded and fired, on the next frame it will need to travel. How do we do this, you ask, the same as we do our other automatic movement. Again, for up only, simply: LDA arrow_direction ;load the arrow direction stored in the above routine BNE .down ;moving in the up direction (note that up is direction #$00) LDA Weapons_RAM+4 ;load the starting position SEC SBC #arrow_speed ;move up whatever constant we input for #arrow_speed STA Weapons_RAM+4 ;store the ending position JSR arrow_collision ;check collsision with the walls JMP .loadarrow ;jump to the next point in the routine Next, I guess we'll go over the arrow_collision routine. All this says is that if the arrow hits a wall, reset it. arrow_collision: ;arrow collision detection with the walls LDA Weapons_RAM+4 ;load the arrow's vertical position, note: the updated position after we've moved it CMP #$08 ;random value for the top wall BCC .next ;hey, look, our new command CMP #$E8 ;random value for the bottom wall BCS .next ;holy shit, there's the other one LDA Weapons_RAM+7 ;load the arrow's horizontal position CMP #$02 ;random value for the left wall BCC .next ;again! CMP #$FE ;random value for the right wall BCS .next ;wow JMP .end ;Well, we could change the line above to BCC, but I must have done this for some reason, so we'll leave it here. .next LDA #$FE ;reset the arrows by loading the idle position into the vertical and horizontal positions STA Weapons_RAM+4 STA Weapons_RAM+7 .end RTS If we really didn't care, we could stop right there, but we want our game to not suck like NGD, so we will make our character pose when he is firing. So, if we input the appropriate information into our subfile for "spritegraphics", we will be almost set up. So, if the A_timer is not equal to zero, we jump to this routine. Like so: shooting_graphics: LDA enemy_direction CMP #$00 BNE .next LDY #$60 ;up JSR enemyspritesupdate JSR update_enemy_sprites RTS .next CMP #$01 BNE .next1 LDY #$78 ;down JSR enemyspritesupdate JSR update_enemy_sprites RTS .next1 CMP #$02 BNE .next2 LDY #$68 ;right JSR enemyspritesupdate JSR update_enemy_sprites RTS .next2 LDY #$70 ;left JSR enemyspritesupdate JSR update_enemy_sprites RTS You'll notice that this is pretty much EXACTLY the same as the updates that take place when we move our guy during "handle_input". Granted, it is a little redundant, but this keeps our routines independent and seperate. Meaning that we can call them in any order, any time. All we are doing is over riding the values set previously. That's it! Now our guy will shoot arrows and look like he's having fun doing it!! SWORD TYPE WEAPONS Now that we have the PC sprite graphics set up, Swords are easy. They don't move independent of the main character, so there's no moving routine to worry about. This works exactly like the arrow loading routine. First we need to just add a quick "skip the routine" if the B_timer is zero and, since the sword doesn't move, we need to add some sort of routine to move the sprite off screen as the B_timer approches zero. Or: mace: ;graphics updates for the mace LDA B_timer BNE .down RTS .down JSR shooting_graphics ;note that this JSR is very important LDA B_timer CMP #$01 BNE .next3 LDA #$FE STA Weapons_RAM STA Weapons_RAM+3 RTS .next3 Simple so far. All we need to do now is load the position and orentation of the sword sprite and we're done!! Like so: .next3 LDA enemy_direction ;check the UP direction CMP #$00 BNE .next LDA #%00000010 ;update the UP graphics STA Weapons_RAM+2 LDA #$1C STA Weapons_RAM+1 LDA sprite_vertical ;save sprite Y position SEC SBC #$07 STA Weapons_RAM LDA sprite_horizontal ;load sprite X position SEC SBC #$00 STA Weapons_RAM+3 RTS .next Viola!! That's how it's done. I believe that is everything in this everything that we've added this time. PM or post if you have any questions. Enjoy and we'll see you next time! Weapons Write Up.zip
  13. Here we will continue where we left off with #1: Sprite Animations. BETTER VARIABLE ORGANIZATION First, we need to reorganize our variables. It's not really necessary here, but as our program gets bigger and stronger, we will run out of variables in zero space...so, this is how we will change it. .rsset $0000 ;only pointers here .rsset $0100 ;stack .rsset $0200 ;sprites .rsset $0300 ;sound .rsset $0400+ ;other variables This will keep it clean and we will avoid having to change stuff around later. NPC VARIABLE SET UP You'll remember from the last write up that we used strings of variables like: Enemy_Animation .rs 4 ;Animation Counters Now, we get creative. Currently all the graphics update routines for the enemies and our playable character are the same (as you'll see below). So, what we're going to do is steal the variables for enemy 0 and use them for our playable character. So, now we'll have our Playable Character as "Enemy 0" and our enemies as "Enemy 1,2,3". Make sense? This way we can reuse all of our code and not have to make seperate routines that do the same thing for our playable character. Now we will modify our random direction routine. This will come in handy later when we have sprite/sprite and sprite/background collisions. Basically, every frame we are putting a random number into the "random direction" that the sprite will take on when something causes it to change direction. Not only that, but when the counter resets back to zero, we query our playable character's direction to further jumble up the direction routine. Observe: random_table: ;values entered at random for direction changes .db $01,$00,$03,$02,$02,$01,$00,$00,$01,$03 random: INC random_direction1 ;random direction counter LDA random_direction1 CMP #$0A BNE .next LDA enemy_direction ;playable character's direction STA random_direction1 .next LDX random_direction1 LDA random_table,X STA random_direction ;actual direction in the movement routine RTS PLAYABLE CHARACTER SET UP AND BIT TESTING This info is pretty basic, so we'll breeze through it. If we look at the main program, we can see that the playable character routine looks like this: LDA #$00 STA enemy_number STA enemy_ptrnumber JSR strobe_controllers JSR handle_input JSR Enemys_Animation JSR Enemys_Sprite_Loading JSR update_enemy_sprites You see that this is almost exactly the same as the enemy update routine that we used last time. The only changes are "strobe_controllers" and "handle_input". First, "strobe_controllers". I stole this routine from MetalSlime (I think) and he does an excellent job of going through it in his tutorials, so we'll skip it here and assume that you know what it is doing. Next, "handle_input". This is where the PC differs from the NPC. All it does is tells the program where to move the sprites (rather than having the automatic random movement generator do it). However, here we use a little "Bit Testing". I can't remember if we covered this and I'm too lazy to look, so we'll go through it here. There are three types of bit testing, AND, ORA, and EOR. Again, I'll credit MetalSlime for explaining it to me a while ago, and I still haven't found a better explination of it, so we'll use it here. (Let me know if you don't want me to use it and I'll remove it.) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; AND When you AND two values together, it compares them bit by bit. If the two bits are both 1, the result will be one. If either of the bits are 0, the result will be zero: 0 AND 0 = 0 0 AND 1 = 0 1 AND 0 = 0 1 AND 1 = 1 Example: 00111101 AND 11100110 -------- 00100100 Notice that the 1's in the result are the bit positions where the two original values BOTH have a 1. If either has a 0 bit, the result will have a zero bit. AND is often used to test individual bits in a bitflag. You use 0's to clear all bits except the one you want to check. If the result is 0, the bit you were checking was 0. If the result is non-zero, the bit you were checking was 1. For example, say we have a bitflag variable and we want to check bit 2. lda bitflag and #%00000100 ;clear all bits except bit 2. If the result is 0, bit 2 was clear. beq .end ;if non-zero, the bit was set. ; the only way this could be non-zero is if bit 2 was 1. Because the only way ; to get a 1 in the result is if you have 1 AND 1. Since we only put a 1 in one ; position in our AND instruction, a non-zero result must mean that bitflag also has ; a 1in that position. Make sense? ; ; ... bit set here, so do something ... .end: rts AND is also used to clear bits. Most often used to turn off bits in a bitflag: lda bitflag and #%11110111 ;clear bit 3 sta bitflag ;save the result ;bit 3 of bitflag will be cleared. All other bits will remain unchanged. ; If they were 1 before, they will remain 1 (1 AND 1 = 1). If they ; were 0 before, they will remain 0 (0 AND 1 = 0). ORA When you ORA two values together, it compares them bit by bit. If the two bits are 0, the result will be 0. If either of the two bits are 1, the result will be 1. 0 ORA 0 = 0 0 ORA 1 = 1 1 ORA 0 = 1 1 ORA 1 = 1 Example: 11000110 ORA 00110111 -------- 11110111 Notice we only get a 0 in the result in the bit positions where the original values BOTH have 0. If either of the bits are 1, the result is 1. ORA is usually used to set bits in a bitflag: lda bitflag ora #%00001000 ;set bit 3 sta bitflag ;save ;here bit 3 will be set. If it was 0 before, it will be 1 now. If it was 1 before it will still be 1. ; All other bits will remain unchanged. If they were 0 before they will remain 0 (0 ORA 0 = 0). ; If they were 1 before they will remain 1 (1 ORA 0 = 1) EOR EOR compares bits and results in a 0 if the bits are the same, or 1 if they are different: 0 EOR 0 = 0 0 EOR 1 = 1 1 EOR 0 = 1 1 EOR 1 = 0 Example: 11010011 EOR 01110111 -------- 10100100 We get a 0 in the result in bit positions where the original values are either both 0 or both 1. Where AND is used to clear bits and ORA is used to set bits, EOR is used to toggle bits. In the EOR chart above, pay close attention to what happens if you EOR something with 1: 0 EOR 1 = 1 1 EOR 1 = 0 Notice the result is the opposite of the original value on the left, EORing by 1 toggles the bit. Off becomes on, on becomes off. lda bitflag eor #%00001000 ;toggle bit 3 sta bitflag ;here bit 3 will be toggled. If it was 1 before, it will be 0 now. If it was 0 before it will be 1 now. ; other bit will remain unchanged. If they were 0 before, they will remain 0 (0 EOR 0 = 0). ; If they were 1 before, they will remain 1 (1 EOR 0 = 1) I use EOR in my controller reading routines to determine if buttons are newly pressed (ie, pressed this frame, but were unpressed last frame). lda buttons_old ;last frame's button states. 1 = pressed last frame, 0 = unpressed eor #%11111111 ;toggle all bits. Now 1 = unpressed last frame, 0 = pressed and buttons ;AND with *this* frame's buttons. The result will only have 1's where the button was unpressed last frame, but pressed this frame. (1 AND 1 = 1) sta buttons_newly_pressed ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; These instructions are VERY powerful. You need to spend some time with them because we'll use them a lot. PLAYABLE CHARACTER MOVEMENT Okay, back to the program. You'll notice that half of the button's don't do anything. We'll use them later so I left them in for now. You'll also notice the order of buttons is such that if UP is pressed, it will skip the other direction buttons. You just have to pick the order that is best for you. Since all the D-PAD buttons do the same thing, we'll just use this as an example: ReadRight: LDA joypad1 ;Right AND #%00000001 BEQ ReadRightDone JSR RightMovement ReadRightDone: RTS Here we see the use of the AND function. So, this means that we are only looking at bit 0, which is the RIGHT button as described in our strobe_controllers routine. It tells the program to JSR the RightMovement routine. RightMovement: LDA #$02 STA enemy_direction ;put "RIGHT" into the direction that this meta sprite is traveling LDA sprite_RAM+3 ;load sprite X position CLC ADC #enemy_speed ;add the speed STA sprite_RAM+3 ;store the x position back in memory INC Enemy_Animation ;incriment the animation counter RTS Pretty simple, right? Then all we do is run the graphics and position updates, same as the NPCs, and you're done! OTHER STUFF The following is kind of an afterthought. I wasn't sure when or if I would add these tools in, but we may as well here because this is a pretty simple lesson. First, I got this from someone, who got it from someone, who stole it from someone. We all know that the NES is a computer. Not just a computer, but a 25 year old computer. It can only do so much per cycle before you over work the processor. So...how do we know how hard we are working the processor?? Bunnyboy does a pretty good job of explaining NMI timing vs. Main Program timing, we well skip the details here (you can also look on nesdev wiki or the sound tutorials). Basically, when NMI hits, you start a new "cycle". The NTSC systems run at 60 Hz, meaning that you have 1/60th of a second to run your program without doing something special. This is why we push/pop our registers in NMI and include the commands to skip the NMI routine if we are busy doing something. Anyway, if you're interested in exactly how much the NES can do, nesdev wiki has a pretty good table. So, when we start a new NMI, first we run the graphics updates (in NMI) and then we run our main program. Then it goes back to sleep until NMI wakes it up again and it starts all over. What say we stick something at the end of our main program that presents a visual indicator of how far close to the next NMI our program takes us? The closer to the top of the screen, the shorter the program. Or if the indicator is close to the bottom, you're close to over running your program and need to do something else. How about a white line across the screen? Easy to see and impliment: showCPUUsageBar: ldx #%00011111 ; sprites + background + monochrome (i.e. WHITE) stx $2001 ldy #21 ; add about 23 for each additional line (leave it on WHITE for one scan line) .loop dey bne .loop dex ; sprites + background + NO monochrome (i.e. #%00011110) stx $2001 rts We just call this at the end of our main program and it will work. However, this will turn the graphics back on, so we have to put some simple commands in to skip it when we are updating backgrounds. It should be noted that our program doesn't have any background that isn't monochrome and is pretty simple, so the usage bar won't show up yet. But as we get more and more complex routines, it will start to bounce around the top of the screen. GAME STATES This was covered briefly in another NA tutorial, but we'll impliment it here because I farted and it smells like tacos. We are going to add "PAUSE"!! How exciting. First, to cover a few things. Indirect jumps are pretty simple. You can't "JSR" to a pointer address. This necessitates the following pair of commands: JSR GameStateNMIIndirect -and- GameStateNMIIndirect: ;indirect jump to NMI routine JMP [NMI_Pointer] We have to do this for both the NMI and Main Program. This leaves a RTS in the stack for us to use when we get done with the code located at the pointer address. Next, we have to declare "Game States". You can pick whatever you want, but we are going to use this: ;00=Main Top Down View ;01=Paused GameStates: .word GameState0,GameState1 GameStateNMIs: .word GameStateNMI0,GameStateNMI1 You can see where we're going with this. We say load 00 or 01 into our game state, then JSR to a routine that loads the appropriate address into the "NMI_Pointer" or "Main_Pointer". Then just use our indirect jump in the NMI/Main Program routines. So, let's write the game state pointer update routine: GameStateUpdate: ;load the game state and NMI pointers LDA GameState STA GameStateOld ASL A ;multiply by two TAX LDA GameStates,x ;Load the Main Program Pointer STA Main_Pointer LDA GameStates+1,x STA Main_Pointer+1 LDA GameStateNMIs,x STA NMI_Pointer LDA GameStateNMIs+1,x STA NMI_Pointer+1 RTS The new variables: "GameState" - the current game state "GameStateOld" - if this is different than the current game state, trigger the Update routine Pointers - pointers as described above. Great! So, now what? Well, if we take our main program and cut all the meat out and put it in a "GameState0" routine and add a "JSR GameStateIndirect" in its place, we can use this technique. Same thing in the NMI. Now we will have a "Main_Program" and a "NMI" program AND we can have several "meat parts" of code split out into the GameState0,GameState1,etc. and GameStateNMI0,GameStateNMI1,etc. that we can call as needed any time we want to change how stuff is handled. Next, we need to put a call for our GameStateUpdate routine into the program somewhere. If we stick the following at the end of the Main_Program, we will have an automatic way of switching game states whenever we change the "GameState" variable. i.e. the code is stand alone! We don't have to call a routine at the time that we want to change the GS, we just wait until all of the main program is finished, then change. This way we complete all of our JMP and JSR routines and don't leave open return to scripts which would be bad. So, now our NMI and main programs would look like this: ;---------------------------------------------------------------------- ;-----------------------START MAIN PROGRAM----------------------------- ;---------------------------------------------------------------------- Forever: INC sleeping ;wait for NMI .loop LDA sleeping BNE .loop ;wait for NMI to clear out the sleeping flag LDA #$01 STA updating_background ;this is for when you are changing rooms or something, not really needed here ;it will skip the NMI updates so as not to mess with your room loading routines JSR strobe_controllers JSR GameStateIndirect LDA GameState CMP GameStateOld BEQ .next JSR GameStateUpdate .next LDA #$00 STA updating_background JMP Forever ;jump back to Forever, and go back to sleep ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;--------THE FUCKIN' NMI ROUTINE, RECOGNIZE BITCH!------------; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; NMI: PHA ;protect the registers TXA PHA TYA PHA nmi_start: LDA updating_background ;check to be sure that the main program isn't busy BNE skip_graphics_updates LDA #$02 STA $4014 ;set the high byte (02) of the RAM address, start the transfer JSR GameStateNMIIndirect LDA #$00 ;tell the ppu there is no background scrolling STA $2005 STA $2005 LDA #%00011110 ;enable sprites, enable background, no clipping on left side STA $2001 LDA #$00 STA sleeping ;wake up the main program STA updating_background+1 LDA updating_background+1 ;we put this in because the usage bar TURNS ON THE BACKGROUND BNE skip_graphics_updates JSR showCPUUsageBar skip_graphics_updates: PLA ;restore the registers TAY PLA TAX PLA RTI ;return from interrupt Simple, easy, and clean. Now all we have to do is put in code to change the variable "GameState" and we're set. So, in our "handle_input" subroutine, let's change START to this: ReadStart: LDA joypad1_pressed ; player 1 - start AND #%00010000 ; only look at bit 5 BEQ ReadStartDone LDA GameState STA GameState+1 LDA #PauseState STA GameState ReadStartDone: See, all it does is change the variable "GameState". Now that it is different than "GameStateOld", at the end of the main program, it will automatically change to the PauseState. (Note: PauseState is just a Constant that we declare so that we can change it once and not have to hunt through the program for every use of that state. This technique will save you a lot of time. I reccomend using it a lot.) We also see "GameState+1" for the first time. This is not really needed when you only have 2 states, but when you have like 10 and want to use pause in all of them you need a way to tell the pause state which state to un-pause to. Otherwise you could pause it on your top down view and when you un-pause, the program will switch to your map screen program and crash. That said, we write our PAUSED state NMI and main routines: ;---------------------------------------------------------------------------------- ;-----------------------MAIN PROGRAM GS #$01-PAUSED-------------------------------- ;---------------------------------------------------------------------------------- GameState1: ;ReadStart: LDA joypad1_pressed ; player 1 - start AND #%00010000 BEQ .ReadStartDone LDA GameState+1 STA GameState .ReadStartDone: RTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;--------NMI ROUTINE, PAUSED GAME STATE #$01-PAUSED-----------; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; GameStateNMI1: RTS Now, it is just sitting there licking its nuts waiting for you to push START again. When you do, it loads the saved state in to "GameState" and the pointers change back to the Main Top Down routines and game play continues. If you really wanted to, you could add something creative here like a giant ball sack that pops up on the screen when it's paused, but we're just going to have the game freeze. AND ANOTHER THING!! This is more of a personal preference than anything. As you get a bigger and bigger program, you will have to use the ".include" and ".incbin" commands more and more. This is all well and good, but what about when you have like 10 .include and 15 .incbin commands? There's a different file associated with each command...that's a lot of crap to dig through. If you are like me and like to organize your stuff into folders to keep it organized, just do this: .include "Subfiles/spritegraphics.asm" -or- .incbin "CHR_Files/SpriteMovement.chr" ;include the sprite graphics data Bitchin! END GAME That's about it!! Attached are all the files that go with this program. Open it, assemble it, take it apart, and learn well because we will build from here! Any questions, post or PM me. Until next time.... PlayableCharacter.zip
  14. BACKGROUND Sorry, this one got kind of long. This is the "front view" of one of the sprites. We'll go through the picture row by row. First row, we make the three views that we need to animate our sprite. Notice that the second and third frame are mirrors of each other. This will save space and save us the trouble of having to come up with more art. And you can't really tell it is a mirror while playing the game. Second row, this puts the frames in order. i.e. legs together, step forward with one leg, legs together, step forward with the other leg. So, we only need to come up with 2 frames to make a smooth, four frame animation. Third row, here we simply split the sprites into the individual tiles that will be encoded into the PPU. Forth row, if we look back up to the "attributes" that we have to assign to each sprite tile, we notice that we can "mirror" them. This allows us to not only use the mirror of each sprite for the various frames of our animation, but allows us to mirror each side of the sprite on its own. Here, we just delete all the sprite tiles that are not unique and we see that the number of actual tiles condenses down quickly. Fifth, row. Here we organize the tiles into a simple row. This is what we program into a graphics editor such as Tile Molester. META SPRITES The above picture shows a plain example of a "meta sprite". Here, we are going to keep it simple and use meta sprites that are 2 tiles x 2 tiles. In the Building Sprites section, I may have incorrectly used "sprite" and "meta sprite" interchangeably, but from here on, a "sprite" will refer to a 8 pixel x 8 pixel tile that is entered into the PPU and a "meta sprite" will be a 16 pixel x 16 pixel image made up of 4 sprites. "Most" games use the 16x16 meta sprites, but you could really apply the technique that follows to whatever meta sprite you want. First, the position of the sprites within the meta sprite. For the sake of example, we will number our sprites within the meta sprite as follows: 1,2,3,4. So as displayed on the screen, it would look like: 1 2 3 4 When we move these sprites around, we want to ALWAYS keep them in the same relative position to each other. If we don't, then there are potential problems with collision detection, movement, animation, etc. This technique should rule this problem out, but you never know. Basically, this is bad: 1 2 move right 2 1 -------------> 3 4 4 3 | | move | down | 3 4 2 1 This is good: 1 2 1 2 ---------> 3 4 3 4 | | | 1 2 3 4 You will notice in what follows that all position information is measured relative to the sprite #1. So, we need to keep the order the same throughout the process. Next, we will introduce the concept of "constants" in sprite manipulation. If we "hard code" in the addresses for sprite data, we would have to write a separate routine for each sprite and if we ever wanted to change the sprite address within the $0200 block of RAM, we would have to spend a large amount of time going back through our code and changing every place that we have an address. This is a PITA and can lead to difficult bugs. Let's fix this problem. If we declare a constant for the beginning address of our sprites, we need only change this number and the assembler will automatically update all the subsequent addresses. Observe: ;------------------------------------ sprite_RAM = $0200 ;------------------------------------ That's it. So, every where we want to put an address, we just put it relative to this constant. For the first sprite: sprite_RAM ;vertical position address sprite_RAM+1 ;tile number address sprite_RAM+2 ;attribute address sprite_RAM+3 ;horizontal position address Second sprite: sprite_RAM+4 ;vertical position address sprite_RAM+5 ;tile number address sprite_RAM+6 ;attribute address sprite_RAM+7 ;horizontal position address and so on. For example, sprite_RAM+7 means $0200+7 = $0207 to the assembler. If you have different sprite blocks you want to keep separate, like enemies, your hero, weapons, enemy weapons, etc., you can declare multiple constants and Bob's your uncle. Now that we have that figured out, we ask why we're doing it. Well, as I said before, everything is measured relative to the sprite #1 and as our sprites will always be in the same relative position as each other, we can write: ;------------------------------------ update_enemy_sprites: ;this routine updates the position of the meta sprites relative to each other LDA sprite_RAM ;vertical updates STA sprite_RAM+4 CLC ADC #$08 STA sprite_RAM+8 STA sprite_RAM+12 LDA sprite_RAM+3 ;horizontal updates STA sprite_RAM+11 CLC ADC #$08 STA sprite_RAM+7 STA sprite_RAM+15 RTS ;------------------------------------ This routine will update our sprites 1,2,3 and 4 relative to sprite 1, i.e. moving as a meta sprite. If we do this every frame, our sprite will move together and we will only have to specify the translation of sprite 1. This keeps everything neat, orderly, and together. MULTIPLE META SPRITES Now, say that we had multiple enemies going after us at once?? Do we have to write a separate routine for each meta sprite? No. We can use what is help from our friend, the "x" register. If we continue our sprite numbering we have: Meta Sprite 1 - sprites 1,2,3,4 - starting address $0200 Meta Sprite 2 - sprites 5,6,7,8 - starting address $0210 Meta Sprite 3 - sprites 9,10,11,12 - starting address $0220 Meta Sprite 4 - sprites 13,14,15,16 - starting address $0230 etc. Here, we'll construct a routine that will update 4 meta sprites with only a slight modification to the code block above. Look at the starting addresses of the sprites in the meta sprite groups above. Notice the pattern? Remember that in hex, things start at 0. So, if we change the numbering system a little bit: Meta Sprite 0 - sprites 1,2,3,4 - starting address $0200 Meta Sprite 1 - sprites 5,6,7,8 - starting address $0210 Meta Sprite 2 - sprites 9,10,11,12 - starting address $0220 Meta Sprite 3 - sprites 13,14,15,16 - starting address $0230 Now, if we keep in mind everything that we have learned so far about meta sprites and apply them to the other 3 meta sprites, we notice that the addresses are exactly the same...only offset by $10. So, naturally, if we want to keep our numbering scheme the same, we would declare some "updating constants" that would allow us to use loops for each meta sprite. ;------------------------------------ updateconstants: ;constants for the use of the sprite_RAM constant .db $00,$10,$20,$30 ;4 sprites for each meta sprite, so add $10 for each meta sprite we process ;------------------------------------ Notice that if we use the meta sprite numbers to look up values in the table and add them to our constant, sprite_RAM, we get the starting addresses for each meta sprite. It follows then that: -Load the meta sprite number, $03, into x -Look up meta sprite 3's update constant using "update constants" and x -Store this value, $30, in the x register -Add x to each value in the above "update_enemy_sprites" routine This allows us to update all 4 enemies using this one routine: ;------------------------------------ update_enemy_sprites: ;this routine updates the position of the meta sprites relative to each other LDX enemy_number ;this is a variable used to specify our "meta sprite number" within the loop LDA updateconstants,x TAX LDA sprite_RAM,x ;vertical updates STA sprite_RAM+4,x CLC ADC #$08 STA sprite_RAM+8,x STA sprite_RAM+12,x LDA sprite_RAM+3,x ;horizontal updates STA sprite_RAM+11,x CLC ADC #$08 STA sprite_RAM+7,x STA sprite_RAM+15,x RTS ;------------------------------------ Savvy? While this is a useful tool, it doesn't really do much if nothing moves and there is no animation. MOVING SPRITES Now that we know how to update sprites, and that we only need to move around the first sprite in our meta sprite, we can move onto more exciting things like moving the sprites around the screen. Again, in this write-up we will use the top down view, so the animation will have the front, back, left, and right side views. First, we need some way to specify to the program which way the sprite is currently moving. Here we will assign the following: Up - $00 Down - $01 Right - $02 Left - $03 This allows us to write the necessary code for each of the 4 directions and makes it easy to figure out which way things are moving. It follows then that we would need to reserve a variable for each meta sprite that specifies direction. (We will include the enemy_number here for completeness.) ;------------------------------------ enemy_number .rs 1 enemy_direction .rs 4 ;------------------------------------ Note that when we reserve multiple variables like this, it will allow us to pull the directions of the various enemies using the variable "enemy_number" while in a loop. Normally, a change in direction would be triggered by a collision or other random event, but here we don't have any of that. So I have inserted a basic "random" direction change routine to make the example more interesting. I think that you can figure out what it does, so I won't waste time on it. Enemy animation, which we will get into later, is usually only active when your dudes are moving. But as in this example they are always moving, we will just keep updating the "animation counter" all the time. If you expand on this program, you need to expand when the animation counter is updated, and when it is not, or your dude will do the Moon Walk while standing still. So, and this is a little redundant when the sprites are all moving all the time, but later we will need another variable for each enemy that specifies its position in the animation routine. Since it appears in the sprite movement routine, we will introduce it now. ;------------------------------------ Enemy_Animation .rs 4 ;------------------------------------ I assume that the reader has been through the tutorial on basic sprite movement, so I won't cover that. So, after we have all of this information, we write: ;------------------------------------- randomenemydirection: ;these are random numbers used with the random_direction2 to make enemies switch direction .db $57,$CD,$AF,$05,$BC ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; enemy_update: LDX enemy_number INC Enemy_Animation,x ;increment the counter for the animation routine LDA random_direction2 ;check random_direction2 with the values stored in randomenemydirection CMP randomenemydirection,x ;this is in place of the routine you would have in collision detection BEQ .down CMP randomenemydirection+1,x BNE .done .down LDA random_direction1 ;if the values match, switch direction the counter random_direction1 STA enemy_direction,x .done LDA enemy_direction,x ;for the various directions, move the sprites around the background BNE .next JMP enemy_up ;note JMP not JSR .next CMP #$01 BNE .next1 JMP enemy_down .next1 CMP #$02 BNE .next2 JMP enemy_right .next2 JMP enemy_left ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; enemy_up: LDX enemy_number ;move the sprite up LDY updateconstants,x LDA sprite_RAM,y SEC SBC #enemy_speed STA sprite_RAM,y RTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; enemy_down: LDX enemy_number ;move the sprite down LDY updateconstants,x LDA sprite_RAM,y CLC ADC #enemy_speed STA sprite_RAM,y RTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; enemy_right: LDX enemy_number ;move the sprite right LDY updateconstants,x LDA sprite_RAM+3,y CLC ADC #enemy_speed STA sprite_RAM+3,y RTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; enemy_left: LDX enemy_number ;move the sprite left LDY updateconstants,x LDA sprite_RAM+3,y SEC SBC #enemy_speed STA sprite_RAM+3,y RTS ;------------------------------------- Notes: -The enemy direction is used to specify which update we want to use. -Both the X and Y registers are used. -Normally for "enemy_speed" you wouldn't use a constant, you would have a variable for each meta sprite, but here we just keep it simple. This would be another thing you should mess around with (and is the reason for the y register usage). (If you had variables for the enemy_speed, you would need the "enemy_number", stored in x, to pull the values from RAM. The y value holds our "updateconstants".) -The random direction routine and redundant calls of the enemy_number are included to help you avoid errors when expanding the program. META SPRITE ANIMATION SET UP Before we can completely jump into animation, we need to specify a few things. Namely, how do we tell the program which animation frame to display given the specified direction and when to switch frames based on the afore mentioned "Enemy_Animation" counters. You may want to specify separate "reset" points for each enemy’s counter, but I've never really found it necessary. You can pick whatever resets you want, just use them so that the animation "looks right". So, we will write the following: ;------------------------------------- Enemy_Frame .rs 4 ;Animation Frame Number enemyFrames1 = $0C ;enemy's counter resets enemyFrames2 = $18 enemyFrames3 = $24 enemyFrames4 = $30 ;------------------------------------- Now that we have all this information in place, we use a simple series of compares, counter resets, and branches to keep our animation frames updated throughout the process. Note that this routine does not update the animation frame number every frame as it is not really needed. Go through the routine and figure out the animation sequence and how it relates to the picture above. Assume here that frame $00 = "legs together". So, we simply write: ;------------------------------------- Enemys_Animation: ;this routine updates the frame that is displayed for each enemy 1,2,1,or 3 LDX enemy_number LDA Enemy_Animation,x ;compare to constants CMP #enemyFrames1 ;you can change these around to make the animation faster or slower on the constants page BEQ .Animation1 CMP #enemyFrames2 BEQ .Animation2 CMP #enemyFrames3 BEQ .Animation1 CMP #enemyFrames4 BEQ .Animation3 JMP .AnimationDone .Animation1: ;load the various frames LDA #$00 STA Enemy_Frame,x JMP .AnimationDone .Animation2: LDA #$01 STA Enemy_Frame,x JMP .AnimationDone .Animation3: LDA #$02 STA Enemy_Frame,x .AnimationDone LDA Enemy_Animation,x ;reset the counter when it gets to the end CMP #enemyFrames4 BNE .AnimationFinished SEC SBC #enemyFrames4 STA Enemy_Animation,x .AnimationFinished RTS ;------------------------------------- You'll notice so far that this is simply a series of subroutines. I write this way to allow myself to edit certain parts of the overall routine as needed. i.e. if you had this all in one long ass sub-routine, you would have difficulty updating direction changes, variable movement, enemy weapons, hit/shoot poses, and on and on. Okay, now that we have a basic engine in place to move our meta sprites around as one and an engine to drive the basic animation, we can move on to some more complex stuff. FORMATTING GRAPHICS This is where it can get confusing as this is the "meat" of the whole process. Here we are going to put our graphics data in a format that can be read by the program and displayed on screen. You'll note that we have already been through movement, so we have covered the "vertical position" and "horizontal position" of the sprites. Now it is time to focus on the sprite "attributes" and "tile number". First, when working with graphics, sprites or background, I recommend putting them into a "demo" program so that you can view your tiles in the PPU Viewer. The reason for this is simple. Well, the screen shot didn't capture the mouse, but you can still observe my point. Reading tiles from the PPU viewer is quick and easy. Simply put your mouse over the tile in question and the emulator displays the tile number at the bottom. Let's look at the picture above again and our sprite numbering inside of the meta sprite shown in the graphic above. See how it goes 1,2,3,4? This is how we are going to specify the data in our meta sprites. If you load the PPU viewer from the attached program, it will be a little easier to follow along here. We are going to format our graphics as follows: tile 1, tile 2, tile 3, tile 4, attribute 1, attribute 2, attribute 3, attribute 4 We will need to do this for each frame, for each direction, for each enemy type we want to use. Let's start with the example in the picture above. This, combined with the PPU viewer, will give us: ;------------------------------------- .db $5C,$5C,$68,$68,%00000010,%01000010,%00000010,%01000010 ;Down, Frame 1 and 3 .db $5A,$5B,$66,$67,%00000010,%00000010,%00000010,%00000010 ;Down, Frame 2 .db $5B,$5A,$67,$66,%01000010,%01000010,%01000010,%01000010 ;Down, Frame 4 ;------------------------------------- Personally, I keep the tiles stored in hex, since that is how they are displayed in the PPU viewer, and the attributes stored as binary just to make it easier to read. To make this data format a little clearer, let's examine it a little closer. We have our meta sprite format from above: 1 2 3 4 If we look at frame 1 in the data stream above, we see that we are specifying the tiles to be displayed as: $5C $5C $68 $68 and the attributes as: %00000010 %01000010 %00000010 %01000010 If we look at it like this, we can see the mirroring in effect. Do this for the other two frames, and you will see an even better example of the mirroring in action!! If we do this for each of the 12 frames required to completely animate the meta sprite in all directions, we are left with a table that looks like this: ;------------------------------------- McBoobins_graphics: .db $5D,$5D,$5F,$5F,%00000010,%01000010,%00000010,%01000010 ;Up, Frame 1 and 3 .db $5D,$5E,$69,$6A,%00000010,%00000010,%00000010,%00000010 ;Up, Frame 2 .db $5E,$5D,$6A,$69,%01000010,%01000010,%01000010,%01000010 ;Up, Frame 4 .db $5C,$5C,$68,$68,%00000010,%01000010,%00000010,%01000010 ;Down, Frame 1 and 3 .db $5A,$5B,$66,$67,%00000010,%00000010,%00000010,%00000010 ;Down, Frame 2 .db $5B,$5A,$67,$66,%01000010,%01000010,%01000010,%01000010 ;Down, Frame 4 .db $57,$56,$63,$62,%01000010,%01000010,%01000010,%01000010 ;Right, Frame 1 and 3 .db $55,$54,$61,$60,%01000010,%01000010,%01000010,%01000010 ;Right, Frame 2 .db $59,$58,$65,$64,%01000010,%01000010,%01000010,%01000010 ;Right, Frame 4 .db $56,$57,$62,$63,%00000010,%00000010,%00000010,%00000010 ;Left, Frame 1 and 3 .db $54,$55,$60,$61,%00000010,%00000010,%00000010,%00000010 ;Left, Frame 2 .db $58,$59,$64,$65,%00000010,%00000010,%00000010,%00000010 ;Left, Frame 4 ;------------------------------------- On a personal note, and I suppose that I'll catch shit for doing this if I don't address it, the sprites used in this write-up are "dumbed down" versions of the sprites that Miss Clawful made for use in my personal game. I used them mainly because I had the above tables already built and was too lazy to go and find new sprites, build the tables, and make it work. I trust that everyone that actually uses this information will be original and make their own work. Okay, now that we have this built, and presumably, the tables for the other required enemies, we are ready to start the animation. If construction of this table is not making sense to you, please review it and make sure you understand. If not, please ask questions. Now, if we look at the above table we need to specify some way to pull graphic information from a certain point in the table.... Pretty simple, right? We just specify the starting position of every frame in the array for each frame of animation. We will use the variable defined in the "Enemys_Animation" routine to pick the required number from the table...but what about direction? We simply use the "enemy_direction" variable to specify which look up table we want to look in? Lost? This might make it clearer: ;------------------------------------- NM_up: .db $00,$08,$10 ;the indexes to the various frames in the spritegraphics file NM_down: .db $18,$20,$28 NM_right: .db $30,$38,$40 NM_left: .db $48,$50,$58 ;------------------------------------- For the moment assume that each of the numbers in that array is specifying the starting position of the frame in question and try to understand. If it's not clear, you'll have to forgive me, I'm not the best teacher. I'll assume you got it, so if not, ask questions. ANIMATING SPRITES Now that we have a fancy table set up with all of our graphics information and another array set up that holds the starting place in that table for each animation frame, we are ready to write a routine to use this stuff. You'll note that if we use the frame counter set up that we have above, it does not reset the counters when the direction changes...i.e. if your dude is taking a step when he changes directions, he will be taking a step when he starts in the other direction. You can set it up to reset the counters and force him to start walking when he switches directions, but there's really no need. It would make the code much more complex and you won't be able to tell the difference. Okay, for now, take it on faith that we need to load the appropriate value in that array up there into the Y register. To do that, it is pretty simple. Rather than explain it, I think that I will just post the code script, and you can go through it real quick: ;------------------------------------- Enemys_Sprite_Loading: ;this routine updates the sprite graphics every frame based on data set by the other routines LDX enemy_number LDA enemy_direction,x BEQ .next ;up ;find out which direction it is going CMP #$01 BEQ .next1 ;down CMP #$02 BEQ .next2 ;right JMP .next3 ;left .next ;UP LDA Enemy_Frame,x ;load the spritegraphics index based on the frame number set in the animation routine TAX ;some of this is redundant because I removed some of the more complex code here LDA NM_up,x TAY JMP enemyspritesupdate ;update graphics .next1 ;DOWN LDA Enemy_Frame,x TAX LDA NM_down,x TAY JMP enemyspritesupdate .next2 ;RIGHT LDA Enemy_Frame,x TAX LDA NM_right,x TAY JMP enemyspritesupdate .next3 ;LEFT LDA Enemy_Frame,x TAX LDA NM_left,x TAY JMP enemyspritesupdate ;------------------------------------- Pretty simple right? Well, here we have to go a little out of order. In this simple example, we lack background, room loading, collision detection, starting positions for sprites, etc. This is all beyond the scope of this document. So, what we need to do here is hard code some things you would normally load when entering a room. If we are going to use the same code for multiple enemy types, we are going to need pointers. If you don't know how to use pointers, there are several tutorials on this site that explain them. This isn't really necessary, as if we don't do it, the sprites will just start off screen and walk on...but we'll do it anyway. Specify starting positions for the sprites in the reset routine: ;------------------------------------- LDA #$80 ;here we set up any start points for enemies STA sprite_RAM ;in practice, these locations would be determined by your background STA sprite_RAM+3 LDA #$90 STA sprite_RAM+16 STA sprite_RAM+19 LDA #$A0 STA sprite_RAM+32 STA sprite_RAM+35 LDA #$B0 STA sprite_RAM+48 STA sprite_RAM+51 ;------------------------------------- Next, for the various enemy types, we will need to set up pointers to the graphics in the "spritegraphics" file. We need to define a pointer variable to compliment the enemy_number. You can just "multiply by 2" every time you use it, but here we're going to do this: ;------------------------------------- enemygraphicspointer .rs 02 ;pointer for the graphics updates enemy_pointer .rs $08 ;pointer for the graphics data for Enemies ;------------------------------------- Basically, the enemygraphicspointer is the pointer that the routine will use each time through the loop to find the graphics data. enemy_pointer is a series of numbers (or addresses) that we set up in our loading routine to make the various meta sprites look like we want them to. We will need two bytes for each enemy, so that is a total of 8 bytes. A little confusing, but it will clear up in a minute. Like I said before, you would do this in your room loading routine in practice, here we will hard code these addresses in the reset routine: ;------------------------------------- LDA #LOW(crewman_graphics) ;here we set up the pointer data for the different enemy types STA enemy_pointer ;in practice, you wouldn't hard code these, they would be part of the loading routines LDA #HIGH(crewman_graphics) ;note that this notation is probably WRONG in the actual program. STA enemy_pointer+1 LDA #LOW(Punisher_graphics) STA enemy_pointer+2 LDA #HIGH(Punisher_graphics) STA enemy_pointer+3 LDA #LOW(McBoobins_graphics) STA enemy_pointer+4 LDA #HIGH(McBoobins_graphics) STA enemy_pointer+5 LDA #LOW(ArseFace_graphics) STA enemy_pointer+6 LDA #HIGH(ArseFace_graphics) STA enemy_pointer+7 ;------------------------------------- The last subroutine we need will load the graphics pointer for the current enemy, read the graphics tables we have set up, and store them to the appropriate place in RAM. Then the next frame, the NMI routine will transfer them to the PPU and life will be grand. Again, this is pretty simple if you've been paying attention so far. Note that the y register was set up in the "Enemys_Sprite_Loading" routine. ;------------------------------------- enemyspritesupdate: ;this routine updates the tiles and attributes for the enemies LDX enemy_ptrnumber ;load in the pointer for the graphics data LDA enemy_pointer,x STA enemygraphicspointer INX LDA enemy_pointer,x STA enemygraphicspointer+1 subenemyspritesupdate: ;we put this here in case we want to have some sort of special update, LDX enemy_number ;like shooting graphics, it is not used here LDA updateconstants,x TAX LDA [enemygraphicspointer],y ;read the tile from the "spritegraphics" sub file and store it in memory STA sprite_RAM+1, x INY LDA [enemygraphicspointer],y STA sprite_RAM+5, x INY LDA [enemygraphicspointer],y STA sprite_RAM+9, x INY LDA [enemygraphicspointer],y STA sprite_RAM+13, x INY LDA [enemygraphicspointer],y STA sprite_RAM+2, x INY LDA [enemygraphicspointer],y STA sprite_RAM+6, x INY LDA [enemygraphicspointer],y STA sprite_RAM+10, x INY LDA [enemygraphicspointer],y STA sprite_RAM+14, x RTS ;------------------------------------- NMI AND THE MAIN PROGRAM So far, we have acquired all the tools we need to make our enemies run. As I said up top, all you need for NMI is: ;------------------------------------- LDA #$02 STA $4014 ;set the high byte (02) of the RAM address, start the transfer ;------------------------------------- The main program is pretty simple as well. Really, we just need to make a loop and it one time for each of our 4 enemies. -Set up the enemy number/enemy pointer -Loop it through our subroutines here -Run it once for each of our 4 (or however many you have) enemies Pretty simple: ;------------------------------------- UpdateENEMIES: LDA #$00 ;this loop updates the enemies one at a time in a loop STA enemy_number ;start with enemy zero STA enemy_ptrnumber .loop JSR enemy_update ;move the sprites based on which direction they are headed JSR Enemys_Animation ;find out which frame the enemy animation is on JSR Enemys_Sprite_Loading ;update the enemy meta tile graphics JSR update_enemy_sprites ;update position INC enemy_number ;increment the enemy number for the next trip through the loop INC enemy_ptrnumber ;these are addresses for the graphics data, so we need to keep it at 2x the enemy number INC enemy_ptrnumber LDA enemy_number CMP #$04 ;if it is 4, we have updated enemies 0,1,2,3 so we are done BNE .loop UpdateENEMIESdone: ;------------------------------------- ONE LAST THING I don't think that this has been introduced yet. It is pretty simple, again, but the code for the Batch File that runs NESASM3 is as follows: ;------------------------------------- NESASM3 SpriteMovement.asm -s pause ;------------------------------------- As simple as run NESASM3 on the file SpriteMovement.asm. -s tells the program that you want it to display the memory usage in each bank when the MS-DOS window pops up. This is extremely useful in larger programs. Then, pause simply tells the program that you are done. CONCLUSIONS It is my personal style, but you can do it however you want. I try to keep my playable character code separate from my NPC code. This is generally because when you get something like this big enough to where it does something ... well, interesting, the playable character code will be so different than the NPC code, that you will have a shit ton of compares or branches and it will just be a mess. However, the code is pretty similar. For "homework", I would say that you should: 1. Make your own characters and input them into these routines 2. Modify the code to accept input from the controllers 3. Add code so that your enemy can do something special if it runs into a wall or gets hit, like flip people the bird. 4. Vary the enemy speed and/or animation frame speed. 5. etc. Attached is a file showing the working example that I put together as well as various other things. I hope you enjoy this and it wasn't too long. 'Till next time.... SpriteMovement.zip
  15. MRN

    Collection Pics

    Very impressive, sir.
×
×
  • Create New...