Help! NESASM Sprite Animation Code Examples

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

Post Reply
Shauing
Posts: 15
Joined: Sun Mar 21, 2021 6:45 pm

Help! NESASM Sprite Animation Code Examples

Post by Shauing »

Hello there! My name is Shauing. I've been getting myself into NES programming to make a very simplistic and small game. But for now, I'm going one thing at a time.

I followed the Nerdy Nights tutorial up to where the sprites move and appear on screen. What I wanna do now is that when the character moves, it also displays a walking/running animation; I wanna start with two frames just to get the hang of it but I envision 6 frames in total.

I have been looking through lots of examples/tutorials from here and all around the web, but no real code examples that apply to NESASM3.1 which is what I'm using, and I know it's not very well-liked here, but that's how I started and I'm getting to know the stuff due to the Nerdy tutorial.

Myself with my very amateurish skills, I've only made it to either:
- Change the standing sprite to the first walking/running frame sprite but it doesn't move
- Make it move and display the first frame but by scrolling the 8 sprite tiles to the next eight (it's a 16x32 character, thus why it's 8 sprite tiles), and when stop pressing the button, it doesn't revert back to the standing sprite.
- Not moving but loop between two sprites when pressing a button, but it doesn't stop looping when the button is no longer pressed.

I assume I have a vague idea on how it's supposed to work: press a button/direction and it should load a new sprite made of 8 sprite tiles, move them at the same time and display them for lets say 4 frames, then load the next 8 sprite tiles that make the second walking sprite, move and display them for 4 frames then loop back to the first one and so on, until letting go of the button, so it goes back to the standing sprite.

Does anyone have code examples that could work on NESASM? I'm really on a corner right now, not sure of how to make it work.
Pokun
Posts: 2681
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: Help! NESASM Sprite Animation Code Examples

Post by Pokun »

It was a long time ago I read Nerdy Nights, but I think it's a bit early for animations before doing Pong. It may require structuring the program a bit better than Nerdy Nights does before the sound tutorial.

It shouldn't be impossible though, so I'm not going to stop you if you really want to try. You probably need to make a counter or something that counts down (or up) every frame, and change the sprite CHR when it reaches 0.
For knowing what animation frame to display, you may want to create a small "state machine" for each sprite (a system that uses a variable that holds the sprite's current state like: IDLE, WALK1, WALK2, CLIMB1, CLIMB2 etc). Nerdy Nights teaches about state machines later for the game state. State machines can be used for many things.

Nesasm shouldn't be that different from other assemblers.
Shauing
Posts: 15
Joined: Sun Mar 21, 2021 6:45 pm

Re: Help! NESASM Sprite Animation Code Examples

Post by Shauing »

Pokun wrote: Mon Mar 22, 2021 12:58 pm It was a long time ago I read Nerdy Nights, but I think it's a bit early for animations before doing Pong. It may require structuring the program a bit better than Nerdy Nights does before the sound tutorial.

It shouldn't be impossible though, so I'm not going to stop you if you really want to try. You probably need to make a counter or something that counts down (or up) every frame, and change the sprite CHR when it reaches 0.
For knowing what animation frame to display, you may want to create a small "state machine" for each sprite (a system that uses a variable that holds the sprite's current state like: IDLE, WALK1, WALK2, CLIMB1, CLIMB2 etc). Nerdy Nights teaches about state machines later for the game state. State machines can be used for many things.

Nesasm shouldn't be that different from other assemblers.
Hmm, so I should write a few variables (in this case, STAND, FRAME1, FRAME2, FRAME3, FRAME4)? But then how to actually load them and cycle between them plus keeping moving when pressing a button? That's my question.

On one of my really amateurish codes, I could load the first frame but the character wouldn't move:

Code: Select all

LatchController:
  LDA #$01
  STA $4016
  LDA #$00
  STA $4016       ; tell both the controllers to latch buttons


ReadLeft: 
  LDA $4016       ; player 1 - A
  LDA $4016       ; player 1 - B
  LDA $4016       ; player 1 - Select
  LDA $4016       ; player 1 - Start
  LDA $4016       ; player 1 - Up
  LDA $4016       ; player 1 - Down
  LDA $4016       ; player 1 - Left
  AND #%00000001  ; only look at bit 0
  BEQ ReadLeftDone   ; branch to ReadADone if button is NOT pressed (0)
                  ; add instructions here to do something when button IS pressed (1)					
  
  LDA $0203       ; load left sprite face X position
  SEC             ; make sure carry flag is set
  SBC #$01        ; A = A - 1
  STA $0203       ; save left sprite face X position
  STA $020B       ; save left sprite upper body X position
  STA $0213       ; save left sprite lower body X position
  STA $021B       ; save left sprite legs X position
  LDA $0207       ; load right sprite face X position
  SEC             ; make sure carry flag is set
  SBC #$01        ; A = A - 1
  STA $0207       ; save right sprite face X position
  STA $020F       ; save right sprite upper body X position
  STA $0217       ; save right sprite lower body sprite X position
  STA $021F       ; save right sprite legs sprite X position 

SpritesLoop2:
  LDA sprites2, x
  STA $0200, x
  INX
  CPX #$20
  BNE SpritesLoop2  
ReadLeftDone:        ; handling this button is done
Pokun
Posts: 2681
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: Help! NESASM Sprite Animation Code Examples

Post by Pokun »

Not variables, only one variable for holding the state. The caps words are the values. Variable values are always numbers, but you can give the numbers names using equates (constants). It's better than using "magic numbers".

Code: Select all

[header and stuff]

Constants:
SPSTATE_STAND     .equ 0
SPSTATE_FRAME1    .equ 1
SPSTATE_FRAME2    .equ 2
SPSTATE_FRAME3    .equ 3   ;sprite states

;RAM (variables):
  .rsset $0000
sp_state   .rs 1     ;sprite state machine variable
  
;ROM:
  .bank 0
  .org $C000
  RESET:
  [init code]
  
Forever:
  jmp Forever     ;jump back to Forever, infinite loop
  
NMI:
;Render:
  lda #$00
  sta $2003       ;set OAM address to $00
  lda #$02
  sta $4014       ;set the RAM page ($02) used as OAM buffer, start DMA transfer
  
;Logic:
  [controller handler]
  
;Buffer drawing:
  [sprite drawing code]
  
  rti
  
[vectors and stuff]
In the [controller handler] routine you change the values of the sp_state variable depending on player input and which frame it is. If no input you probably want to set it to SPSTATE_STAND.

In the [sprite drawing code] routine you draw the sprite (by writing to the $02xx RAM registers) based on the value of the sp_state variable (if it says SPSTATE_STAND you use the standing CHR etc).

In order for the sprite to animate, you have to continuously cycle the sp_state between all the animation frame states over a period of several frames. For that you need a counter that you decrement every frame and have a conditional piece of code that checks if it's 0 and then change the sprite state. The [sprite drawing code] will handle the rest for you.


It's better to have all information of the sprite (X-position, Y-position, animation frame etc) in variables that change based on input or other logic. Then update the $02xx RAM registers based on the values of those variables. I structured the code for you using labels "Render", "Logic" and "Buffer Drawing". In "Logic" you read controllers, process the data and change the states and other variables of the sprites accordingly, but you don't draw yet. Writes to $02xx are "Buffer Drawing" ($0200 to $02FF is a page of RAM that you are using as a buffer for the sprite drawing data) and are best kept separate from controller reading and processing. "Render" is where you send the data to the PPU to actually draw the buffered video data on the TV. It must always be first in the NMI (in your game only sprites are used, but background rendering also goes here). The order of the other two doesn't matter but it's common to do logic before drawing the buffer.

I think Nerdy Nights will get to this later when you make Pong.
Shauing
Posts: 15
Joined: Sun Mar 21, 2021 6:45 pm

Re: Help! NESASM Sprite Animation Code Examples

Post by Shauing »

Pokun wrote: Tue Mar 23, 2021 8:34 am Not variables, only one variable for holding the state. The caps words are the values. Variable values are always numbers, but you can give the numbers names using equates (constants). It's better than using "magic numbers".

Code: Select all

[header and stuff]

Constants:
SPSTATE_STAND     .equ 0
SPSTATE_FRAME1    .equ 1
SPSTATE_FRAME2    .equ 2
SPSTATE_FRAME3    .equ 3   ;sprite states

;RAM (variables):
  .rsset $0000
sp_state   .rs 1     ;sprite state machine variable
  
;ROM:
  .bank 0
  .org $C000
  RESET:
  [init code]
  
Forever:
  jmp Forever     ;jump back to Forever, infinite loop
  
NMI:
;Render:
  lda #$00
  sta $2003       ;set OAM address to $00
  lda #$02
  sta $4014       ;set the RAM page ($02) used as OAM buffer, start DMA transfer
  
;Logic:
  [controller handler]
  
;Buffer drawing:
  [sprite drawing code]
  
  rti
  
[vectors and stuff]
In the [controller handler] routine you change the values of the sp_state variable depending on player input and which frame it is. If no input you probably want to set it to SPSTATE_STAND.

Let me see if I understand. So let's say I press and hold right (LDA $4016 - AND #%00000001 - BNE Read R), I should load a value (LDA?) let's say #$01 and store it on sp_state (STA sp_state?) if frame value is at #$01 (CMP #$01 - BEQ SPSTATE_FRAME1?), load #$02 if frame value is #$02, and so on.

Pokun wrote: Tue Mar 23, 2021 8:34 am In the [sprite drawing code] routine you draw the sprite (by writing to the $02xx RAM registers) based on the value of the sp_state variable (if it says SPSTATE_STAND you use the standing CHR etc).
Here, I suppose after branching to the value sp_state has, we draw the sprites according to that (LDA #$00 STA $0201...)

Pokun wrote: Tue Mar 23, 2021 8:34 am In order for the sprite to animate, you have to continuously cycle the sp_state between all the animation frame states over a period of several frames. For that you need a counter that you decrement every frame and have a conditional piece of code that checks if it's 0 and then change the sprite state. The [sprite drawing code] will handle the rest for you.
Not sure where to put this exactly, but jump to a place with this counter starting from a high number (LDX #$10?) and compare it to #$00, then BNE when it reaches #$00, then load the new value #$02 and jump back above so that now frame2 will be ready to be read so that the sprite tiles corresponding to that frame2 displays.
Pokun wrote: Tue Mar 23, 2021 8:34 am It's better to have all information of the sprite (X-position, Y-position, animation frame etc) in variables that change based on input or other logic. Then update the $02xx RAM registers based on the values of those variables. I structured the code for you using labels "Render", "Logic" and "Buffer Drawing". In "Logic" you read controllers, process the data and change the states and other variables of the sprites accordingly, but you don't draw yet. Writes to $02xx are "Buffer Drawing" ($0200 to $02FF is a page of RAM that you are using as a buffer for the sprite drawing data) and are best kept separate from controller reading and processing. "Render" is where you send the data to the PPU to actually draw the buffered video data on the TV. It must always be first in the NMI (in your game only sprites are used, but background rendering also goes here). The order of the other two doesn't matter but it's common to do logic before drawing the buffer.
I'm trying hard to understand all this, but without actual code examples, I get a bit lost. I had to look elsewhere so I could at least start to get close to the idea but I think I'm still quite off.
Pokun
Posts: 2681
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: Help! NESASM Sprite Animation Code Examples

Post by Pokun »

You can't branch to SPSTATE_FRAME1 as that is just the value 1 in my example. If you jump to 1 you would be in RAM, and branching there would give you an error anyway. Also [controller handler] and [sprite drawing code] are two separate routines that works independently of each other. You don't want to jump into the middle of them from the other. They are just two routines that runs every frame no matter what. They communicate with each other only with the state machine. The [controller handler] routine sets the state and the [sprite drawing code] just draws the sprites and checks the state to know which CHR to use.


Here is some very crude code:

Code: Select all

;Constants:
CON_A             .equ %10000000
CON_B             .equ %01000000
CON_SELECT        .equ %00100000
CON_START         .equ %00010000
CON_UP            .equ %00001000
CON_DOWN          .equ %00000100
CON_LEFT          .equ %00000010
CON_RIGHT         .equ %00000001
SPSTATE_STAND     .equ 0
SPSTATE_FRAME1    .equ 1
SPSTATE_FRAME2    .equ 2
SPSTATE_FRAME3    .equ 3   ;sprite states
ANIMECNT_RELOAD   .equ $10 ;sprite animation counter reload value
CHR_STAND         .equ $20 ;change to whatever the CHR number is
CHR_FRAME1        .equ $21 ;change to whatever the CHR number is
CHR_FRAME2        .equ $22 ;change to whatever the CHR number is
CHR_FRAME3        .equ $23 ;change to whatever the CHR number is

;Variables:
  .rsset $0000
con_state         .rs 2    ;controller button states (A,B,SE,ST,U,D,L,R)
sp_animecnt       .rs 1    ;sprite animation counter
sp_state          .rs 1    ;sprite state machine variable
sp_x              .rs 1    ;sprite X-position
sp_y              .rs 1    ;sprite Y-position

......

RESET:
init:
  LDA #ANIMECNT_RELOAD
  STA sp_animecnt          ;initialize animiation counter
  LDA #SPSTATE_STAND
  STA sp_state             ;initialize animation frame to standing

......

NMI:
render:


......


logic:
  JSR con_read             ;subroutine that reads controller button states

con_handler1:              ;controller 1 handler
  LDA con_state+0
  AND #CON_RIGHT
  BEQ not_right
right:                     ;actions that happens when pressing RIGHT
  DEC sp_animecnt          ;decrement animation counter by 1
  BNE not_animechange      ;if animation counter != 0, don't change frame
animechange:
  LDA #ANIMECNT_RELOAD
  STA sp_animecnt          ;reload animation counter
  INC sp_state             ;change to next animation frame
  LDA sp_state
  CMP #SPSTATE_FRAME3      ;check that sp_state isn't larger than frame 3
  BCS not_animewrap        ;if !(sp_state > SPSTATE_FRAME3), don't wrap
animewrap:
  LDA #SPSTATE_FRAME1
  STA sp_state             ;else, wrap animation state back to frame 1
not_animewrap:
  ;Do nothing here.
not_animechange:
  JMP exit_right           ;exit the RIGHT button handler routine
not_right:                 ;actions that happens when not pressing RIGHT
  LDA #SPSTATE_STAND
  STA sp_state             ;change state to standing when not pressing RIGHT
exit_right:
  ;Check other buttons the same way.

......

buffer_drawing:

oambuffer_draw:            ;fill OAM buffer ($02xx) with sprite attributes
  LDA sp_state
  BNE not_frame0
frame0:
  LDA #CHR_STAND
  STA $0201
  JMP exit_oambuffer_draw
not_frame0:
  CMP #SPSTATE_FRAME1
  ;Continue checking for all animation frames.
exit_oambuffer_draw:
I haven't tested this or anything so I have no idea if it actually works, but hopefully it gives you some ideas of what I had in mind.

Basically this animates a sprite whenever the player holds the RIGHT button on the controller. It does this by decrementing a counter by 1 for every logic frame that the RIGHT button is held down. Every time the counter reaches 0 (which in the example is after $10 (16) frames of holding RIGHT), the animation frame is incremented by 1. Then it checks that the animation frame isn't higher than the highest value allowed (frame 3 in the example) and makes it wrap back to frame 1 if it is. CMP and BCS are used to do a greater-than type of comparison.
When the logic code is finished, the sprite's correct animation frame should be stored in sp_state (which I realized is a bad name for this variable, you might want to call it sp_animeframe or something).
I have no sprite movement code or anything here, this only advances the animation state when a button is pressed. I included variables for sp_x and sp_y as examples but I'm not actually using them for anything in the example.

Then the buffer drawing code starts. The only routine I have here is the OAM buffer draw routine which simply puts the correct CHR number in the OAM buffer ($02xx). It checks the sp_state to know which frame it is and fetches the corresponding CHR.
If you have the controller handling also moving the sprite (by incrementing or decrementing sp_x and sp_y) you also want to write sp_x and sp_y to $0203 (X) and $0200 (Y) here.


If this is too hard to understand it might be that I'm using too many things that Nerdy Nights haven't introduced to you yet. In that case I strongly suggest to press on and tackle it later when you have finished Pong. For example I suppose you don't know why I can read the controller state from a single variable instead of reading $4016. The con_read subroutine is handling that, and I never included the content of that subroutine, because it's a bit too long and complicated.
If you really want to see it, it's here:

Code: Select all

;*****************************************************************************
con_read:
;Reads controller I and II as well as III and IV (Famicom Expansion Port
;controllers) and treats III and IV as alternate I and II.
;All controllers are read until two adjacent reads are identical to avoid
;corrupted reads that may happen if the DPCM channel is used.
;It also calculates and saves 1-0- and 0-1-transitions for all controllers.
;Uses:
;temp+0        - Temporary storage
;temp+1        - Temporary storage
;con_prev+0    - Button input state of the previous frame for controller I
;con_prev+1    - Button input state of the previous frame for controller II
;Output:
;con_state+0   - Button input state for controller I (0=released 1=pressed)
;con_state+1   - Button input state for controller II (0=released 1=pressed)
;con_trig+0    - 0-1-transitions for controller I (1=newly pressed, else 0)
;con_trig+1    - 0-1-transitions for controller II (1=newly pressed, else 0)
;con_rel+0     - 1-0-transitions for controller I (1=newly released, else 0)
;con_rel+1     - 1-0-transitions for controller II (1=newly released, else 0)
;
;*****************************************************************************
@previous:
;Update previous input states to RAM.
  lda con_state+0
  sta con_prev+0
  lda con_state+1
  sta con_prev+1       ;save previous controller button states
  
  jsr @read            ;read controller input states
  
@compare:
;Reads controllers again and compare the two reads for DPCM safety.
  lda con_state+0
  sta temp+2
  lda con_state+1
  sta temp+3           ;save button states in RAM
  jsr @read            ;read controllers again
  lda temp+2
  cmp con_state+0
  bne @compare
  lda temp+3
  cmp con_state+1      ;compare the two reads of con I and II
  bne @compare         ;if not equal, read again
  
@edge_detection:
;Calculates newly pressed and newly released button transitions.
  ldx #$02             ;loop counter for 2 controllers
@loop_t:
  dex
  lda con_prev+0,x
  eor #$FF
  and con_state+0,x
  sta con_trig+0,x     ;calculate newly pressed buttons
  lda con_state+0,x
  eor #$FF
  and con_prev+0,x
  sta con_rel+0,x      ;calculate newly released buttons
  cpx #$00
  bne @loop_t
  
  rts
  
@read:
;Reads current controller button states.
  lda #$01
  sta $4016            ;set $4016.0
  lsr a
  sta $4016            ;clear $4016.0, controllers latched
  ldx #$08             ;loop counter for 8 buttons
@loop_r:
  lda $4016            ;get button data from D0 (con I) and D1 (con III)
  lsr a                ;shift button data from D0 into carry
  rol con_state+0      ;rotate carry into a RAM register (con I buttons)
  lsr a                ;shift button data from D1 into carry
  rol temp+0           ;rotate carry into a RAM register (con III buttons)
  lda $4017            ;get button data from D0 (con II) and D1 (con IV)
  lsr a                ;shift button data from D0 into carry
  rol con_state+1      ;rotate carry into a RAM register (con II buttons)
  lsr a                ;shift button data from D1 into carry
  rol temp+1           ;rotate carry into a RAM register (con IV buttons)
  dex
  bne @loop_r
@merge:                ;merge controller I with III and II with IV using OR
  lda temp+0
  ora con_state+0
  sta con_state+0      ;OR con III with con I
  lda temp+1
  ora con_state+1
  sta con_state+1      ;OR con IV with con II
  rts
;-----------------------------------------------------------------------------
I really suggest to try to incorporate your own controller reading code though. At least until you are ready to try a more sophisticated controller reading routine (after making Pong).
Post Reply