Wednesday, January 18, 2017

NESDev - End of Semester Sprint (Pt. 1)

I'm pretty late for this update, but it needs to be written.  The ultimate goal for Cloak n' Dagger (did I mention that was the name of the game?) was to have a working game by the end of the fall semester.  Originally I had aimed to have a full working game at the time of presentation; however, the design of the game got smaller and smaller, and I ran so low on time that I eventually decided "If I have two characters on a screen that move and jump, I'll be good".

Thankfully, I was right.  Ultimately, my project was well received among my classmates as well as my professor, even though my project had less hardware involved that the other projects (an L.E.D. voice-controlled chessboard, laser harp, and a smart mirror, to name a few...).

This blog is going to touch on the last week of the semester.  It will cover my process for porting the game onto a cartridge, some of the issues I ran into a long the way, and where the project currently is.

The Final Week

It was difficult to manage my time during the final sprint, as I had final exams for 4 additional courses to study for.  As previously mentioned, I had to change my goal for a final product; this meant taking focus away from certain issues and turning to new ones.

In the last post, I mentioned that I was trying to implement the GPU's mirroring feature in order to force a sprite to "turn" and face the direction they were moving.  I eventually decided to forgo that feature altogether, and moved my focus to adding a second player, jumping, and collision.  If I had the time, I would go so far as to add background tiles and change up the color palette from the "black and blue" that I had been using from the Nerdy Nights tutorials.

Player 2

Adding Player 2 was relatively easy.  I opted to simply copy/paste/modify the code for player 1, accounting for a change in sprites and change in controller port.  This simply meant using LDA $4017 instead of  LDA $4016 where necessary, and adding onto the sprite loop to account for "Dagger" (yeah, the characters have names now too).

LDX #$00              
LDA sprites, x        
STA $0200, x          
CPX #$60   ; Compare X to hex $60, decimal 96 (overshooting)
BNE LoadSpritesLoop

Here, I think I overshot the upper limit of sprite tile addresses quite a bit (trying to recall why...).  Essentially, I only aimed to use two characters, but that meant 12 sprites overall.  

  ;     vert tile attr horiz
  ; Y - TILE - ATTR - X
;----------------- Cloak Sprites -----------------
  .db $80, $00, $00, $40   ;headleft $0200 - $0203
  .db $80, $01, $00, $48   ;headright $0204 - $0207
  .db $88, $10, $00, $40   ;spine $0208 - $020B
  .db $88, $11, $00, $48   ;front $020C - $020F
  .db $90, $20, $00, $40 ;back leg $0210 - $0213
  .db $90, $21, $00, $48   ;front leg $0214 - $0217
;----------------- Dagger Sprites ----------------
  .db $80, $44, $00, $B0   ;headleft $0218 - $021B
  .db $80, $45, $00, $B8   ;headright $021C - $021F
  .db $88, $54, $00, $B0   ;front $0220 - $0223
  .db $88, $55, $00, $B8   ;spine $0224 - $0227
  .db $90, $64, $00, $B0   ;front leg $0228 - $022B
  .db $90, $65, $00, $B8   ;back leg $022C - $022F

Documentation is immensely important for this project.

For those who may be wondering, the "tile" address (column 2 in the above tables) simply lines up to the position of the tile when drawn in YY-CHR.  The software makes it easy to keep track of where each tile has been placed in memory.


Oh boy.

I initially thought that jumping would be fairly straight-forward.  I've figured out how to make a group of pixels move left and right at the same time, all I have to do is make them move up and down, right?


Turns out there were a few obvious points I didn't think about:
  • What goes up is supposed to come down.
  • You're not supposed to continuously jump in mid-air.
Both of these issues where fairly simple to solve, though my solutions were fairly buggy.  There is a lot of code and a bit of a confusing road-map that I will need to explain, so bear with me for a moment.

;--------------------------  P1 Read A ---------------------------
LDA $4016
AND #000001  
JMP ReadADone1  

LDA $0200
CMP #$80
BEQ APressed1
JMP ReadADone1

LDA #$00FF
STA DelayRegister
; ;;;;;;;;;; HEAD LEFT ;;;;;;;;;;
LDA $0200       ; load sprite X position
SBC #$02  
STA $0200 ; Save Sprite 1 X position
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

------Insert Recycled code for the rest of the sprites here...------

BIT $2002
BPL vblankjumping
LDA $0200
CMP #$32
BNE Jump1
BEQ FallDown

LDA #$00FF
STA DelayRegister

; ;;;;;;;;;; HEAD LEFT ;;;;;;;;;;
LDA $0200       ; load sprite X position
ADC #$02  
STA $0200 ; Save Sprite 1 X position
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

------Insert Recycled code for the rest of the sprites here...------

Needless to say, jumping took up a lot of my time during finals week.  There were several bumps I hit at this point that required additional research, but before I get into those, let me see if I can simplify the order in which this code is run.
  1. Check $4016 to see if A has been pressed.
    • If A has been pressed, jump to "A1Fix".
    • If A has not been pressed, move on to check if 'B' has been pressed ("ReadADone1").
  2. After jumping to "A1Fix" (to be explained), check if the character is touching the floor.
    • If the character is touching the floor, continue on to "APressed1".
    • If the character is not touching the floor, move on to check if 'B' has been pressed (ReadADone1).
  3. APressed1 will load FF into a register set aside for a delay.  The code then moves on to the actual "Jump".
  4. "Jump1" systematically moves all of the character's sprites up 2 pixels.
  5. The game waits for a VBlank, then compares the position of a sprite to a "ceiling".'
    • If the sprite has not hit the ceiling ($32, in this case) then the game loops back to "Jump1".
    • If the sprite has hit the ceiling, then the character falls, which is our "Jump1" in reverse.


This part of the jumping routine was used to fix an issue (Branch address out of range!) I was having when assembling the code.  It isn't an issue with the assembler, but a limitation with the processor.  As explained by users tokumaru and koitsu in this forum post, if the code is expected to "branch" to a subroutine that is rather far away, the processor will throw the above error.  However, we can use this trick of using the JMP command to branch rather than and actual branch statement to avoid the error.

This is simply a result of limitation differences between the two commands.  Any of the branch commands, such as BNE or BEQ consist of 2 bytes: 1 for the opcode itself and 1 byte for the operand.  The operand leaves availability for a signed 8-bit number, which means that the branch command is limited to travelling 127 bytes forward or backward from its location.

The JMP command, on the other hand, reserves 1 byte for the opcode and 2 bytes for an address to jump to.  As koitsu points out, a pretty important distinction here is that the JMP command observes an "absolute address" as the command argument, while a branch command will use a relative address for it's operand.  The relative address is often considered an offset from the opcode using it, which in this case will be very limited.  As the JMP command uses absolute addresses, the command is fully intended to jump anywhere in the processor's memory.  In this case, the JMP command can travel anywhere from $0000 to $FFFF.

In short, rather than checking to see if A has not been pressed (as we have done with all button presses), we check to see if it has been pressed pressed.  This allows use to use the branch command within it's limitations, while the branch is simply 2 lines away.  When the branch command finds that the button has not been pressed, it simply continues onto the next line, which utilizes the much winder address range of the JMP command.

That was a longer explanation than I intended.


The concept of the V-Blank state is something that I really should have tried harder to understand when I started out on this project, as it would have made programming jumps much easier (or any vertical movement, for that matter).  

Originally, I had realized that when I programmed jumping into the game, it might have been happening so fast that it wasn't visible to the human eye.  That is why, in the code above, you see remnants of me trying to apply a "delay" subroutine.  I assumed that by applying a delay that decremented the value in an address from a very high number, I could slow down the jump to be more visible.  This is the closest I could get...

It turns out, the highest value I was able to apply to my delay subroutine to count down from was $00FF, for the exact same reasons that I needed to apply "A1Fix".  The branching instruction I was trying to apply simply couldn't handle a higher value.

Thankfully, the nesdev forum users once again came to my rescue.  In user 9258's post, he discovered that he could only create a functioning jump by applying a small subroutine that waits for the V-Blank state.  The V-Blank state is a period of time in which the PPU is in-between drawing sprites onto the TV screen.  As you might remember, television sets during this time would use a "gun" that travels left to right, from the bottom to the top of the screen.  Relying on the period in which the screen would be refreshing this cycle is the best method for creating vertical moving sprites in a smooth and effective manner.

While I ended up with a jump I'm happy with, the user response to the forum post goes into much finer details about how to build a basic jump.  Fine tuning the jump is on my to-do list, as well as fixing up a lot of the other flawed methods I applied to making this game functional.


Another fun one.

Figuring out collision took a lot of research and a lot of thought, and I'm still pretty sure I'm doing it wrong.  I eventually came across this article, which addresses the fundamentals of collisions among multiple sprites.  I am planning on revisiting it and actually trying to apply the mathematical logic behind it, as at the time of implementing collision, my head was pretty weighed down with the stress of finals.  Eventually, I settled for simply establishing collision between the front sides of each character, narrowing down the number of tiles and directions that would need to be accounted for.

I decided to take a similar approach to my jumping logic, where I control sprite movement based on sprite location.  In this case, the code would compare the positions of the sprites of player 1 to the sprites of player 2, and if there was overlap, all sprites would be pushed backwards.

;--------------------- Collision Checks -------------------

LDA $0207
CMP $021B
BCS CFace2DFaceY
JMP NoCollision
LDA $0204
CMP $0218
BEQ CFace2DBackX
JMP NoCollision
LDA $0207
CMP $021F
BCC Cloak_CollisionFront

LDA $021B
CMP $0207
BCS DFace2CFaceY
JMP NoCollision
LDA $0218
CMP $0204
BEQ DFace2CBackX
JMP NoCollision
LDA $021F
CMP $0207
BCC Dagger_CollisionFront
  RTI             ; return from interrupt

As I tested this concept more and more, the above code was the first working result I could produce.  Unfortunately, i do not have any record as to why the overlap of sprites was required (again, collision is something that will be fine-tuned with jumping), but as you may be able to follow, these subroutines check to see that one set of sprites has overlapped the sprites of the other player, then only forces the sprites away from each other when reaching the "back" set of sprites of the other player.  If those conditions are met, another set of subroutines are called, which simply move the sprites in the intended direction:

LDA $021B
SBC #$04
STA $0207
LDA $0223
SBC #$04
STA $020F
LDA $022B
SBC #$04
STA $0217
LDA $0207
SBC #$07
STA $0203
LDA $020F
SBC #$07
STA $020B
LDA $0217
SBC #$07
STA $0213


Overall, this was the result I settled on before moving on to the hardware implementation of the project:


This blog post has kind of worn me out.  As I begin to tinker with Cloak n' Dagger again, I will probably go through this post (and previous ones) to add info or fix mistakes.  For now, I'm moving on to the next post, which will discuss the process of porting my prototype to a cartridge.


No comments:

Post a Comment