Jump to content
IGNORED

Game Engine Building #2: Playable Characters, Bit Testing, CPU Usage, and Game States


MRN

Recommended Posts

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.  face-icon-small-smile.gif  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...teabag.gif

PlayableCharacter.zip

  • Like 1
Link to comment
Share on other sites

×
×
  • Create New...