• Just a reminder that providing specifics on, sharing links to, or naming websites where ROMs can be accessed is against the rules. If your post has any of this information it will be removed.
  • Ever thought it'd be cool to have your art, writing, or challenge runs featured on PokéCommunity? Click here for info - we'd love to spotlight your work!
  • Our weekly protagonist poll is now up! Vote for your favorite Trading Card Game 2 protagonist in the poll by clicking here.
  • Welcome to PokéCommunity! Register now and join one of the best fan communities on the 'net to talk Pokémon and more! We are not affiliated with The Pokémon Company or Nintendo.

[Other] Write-up for doing 60fps overworld in pokered

  • 566
    Posts
    5
    Years
    • Seen Apr 29, 2025
    A while ago, I was able to implement a 60 fps overworld option in my rom hack Shin Pokemon. It is based on an older commit of pokered disassembly from 2018, but most things are still in the same place. Anyway, I've been wanting to write down how I did this for others to follow. With any luck this will be picked up by search engine crawlers, and some poor sap years from now won't have to waste hours on Google sifting through dead links and vague hints.

    Alright! Let's jump right in.

    Go to home/overworld.asm and scroll down to OverworldLoop and OverworldLoopLessDelay. Here's the key. Notice that DelayFrame is called in OverworldLoop then you fall through to OverworldLoopLessDelay and call DelayFrame again. This is what causes the overworld to run at 30 fps. The gameboy is locked to 60 fps, but the overworld is updating every 2 frames.
    OverworldLoop::
    call DelayFrame
    OverworldLoopLessDelay::
    call DelayFrame
    call LoadGBPal

    Comment-out that first delay. Congrats, you now have a 60 FPS overworld. Recompile and fire up the game.
    OverworldLoop::
    ;call DelayFrame ;60fps
    OverworldLoopLessDelay::
    call DelayFrame
    call LoadGBPal

    What's that? All the movement and animation speeds for overworld objects are messed up and twice as fast? Well, I didn't expect you to be so picky. Fine then. Let's continue on and do things properly.

    Go to wram.asm and scroll down to wSpriteStateData2. This is a data structure used by overworld objects. That's tuff that can move around on a map. The 'x' in each address refers to the number of the object on the loaded map. The player character is always object 'x' = 0, so each map can have 15 other objects (1 through F) on it at a time. The really important thing here is address C2xA. That's an unused part of the structure, so comment that as being used for 60FPS. We're going to snag it for our purposes. Specifically, it is going to toggle between 0 and 1 to indicate if object data needs to update or be skipped. This is because, at 60 fps, many things need to only update every other frame.
    wSpriteStateData2:: ; c200
    ; more data for all sprites on the current map
    ; holds info for 16 sprites with $10 bytes each
    ; player sprite is always sprite 0
    ; C2x0: walk animation counter (counting from $10 backwards when moving)
    ; C2x1:
    ; C2x2: Y displacement (initialized at 8, supposed to keep moving sprites from moving too far)
    ; C2x3: X displacement (initialized at 8, supposed to keep moving sprites from moving too far)
    ; C2x4: Y position (in 2x2 tile grid steps, topmost 2x2 tile has value 4)
    ; C2x5: X position (in 2x2 tile grid steps, leftmost 2x2 tile has value 4)
    ; C2x6: movement byte 1 (determines whether a sprite can move, $ff:not moving, $fe:random movements, others unknown)
    ; C2x7: (?) (set to $80 when in grass, else $0; may be used to draw grass above the sprite)
    ; C2x8: delay until next movement (counted downwards, status (c1x1) is set to ready if reached 0)
    ; C2x9: backup storage of facing direction (0: down, 4: up, 8: left, $c: right)
    ; C2xA 60fps
    ; C2xB
    ; C2xC
    ; C2xD: picture ID
    ; C2xE: sprite image base offset (in video ram, player always has value 1, used to compute c1x2)
    ; C2xF

    The 60FPS setting needs to be able to be turned on and off. All that's needed is 1 bit in an unused byte of wram. I used bit 4 of the wram address titled wUnusedD721 out of convenience. 60FPS mode will be active when this bit = 1. Jot this down in your notes. Create the Check60fps function at the bottom of the home/overworld.asm file to easily check for this, and you can now add this toggle to OverworldLoop like so. We'll be doing stuff like this a lot.
    OverworldLoop::
    ;call DelayFrame ;60fps
    call Check60fps
    call z, DelayFrame
    OverworldLoopLessDelay::
    call DelayFrame
    call LoadGBPal
    Check60fps:
    ld a, [wUnusedD721]
    bit 4, a
    ret

    Now let's do one of the easier bits. Time to fix jumping ledges. Go to engine/overworld/player_animations.asm and scroll down to the end. You're going to add a new function called Ledge60fps. This function takes the jumping player's incremented Y-coord index as an input in the A register. Running at double framerate means this should only increment every other frame.
    Ledge60fps:
    push hl
    ld h, $c2
    ld l, $0a ;point HL to C20A, the address of the player object 60FPS byte, which holds either 0 or 1
    sub [hl] ;subtract the value in C20A from the incremented Y index in A, which undoes the increment every other frame
    pop hl
    ret

    Now scroll up to the function _HandleMidJump and call Ledge60fps just after A gets incremented at the beginning.
    _HandleMidJump:
    ld a, [wPlayerJumpingYScreenCoordsIndex]
    ld c, a
    inc a
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - only update every other tick
    call Ledge60fps
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    Go back to home/overworld.asm because there is more stuff to do. Time to fix basic player movement.
    Scroll down to OverworldLoopLessDelay.noCollision and notice the counter value of $08.
    .noCollision
    ld a, $08
    ld [wWalkCounter], a
    jr .moveAhead2

    Modify this so that 60FPS mode doubles this to $10.
    .noCollision
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - counter is doubled
    call Check60fps
    ld a, $8
    jr z, .pc60fpsCounter
    ld a, $10
    .pc60fpsCounter
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld [wWalkCounter], a
    jr .moveAhead2

    Scroll down to AdvancePlayerSprite.afterUpdateMapCoords and notice the counter value of $07.
    .afterUpdateMapCoords
    ld a, [wWalkCounter] ; walking animation counter
    cp $07
    jp nz, .scrollBackgroundAndSprites

    Modify this so that 60FPS mode doubles this to $0F.
    .afterUpdateMapCoords
    ld a, [wWalkCounter] ; walking animation counter
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - counter is doubled
    push bc
    ld b, a
    call Check60fps
    ld a, b
    ld b, $07
    jr z, .pc60fpsCounterComp
    ld b, $0F
    .pc60fpsCounterComp
    cp b
    pop bc
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    jp nz, .scrollBackgroundAndSprites

    Scroll down to AdvancePlayerSprite.scrollBackgroundAndSprites and notice how some X and Y delta values are shifted left in order to double them.
    .scrollBackgroundAndSprites
    ld a, [wSpriteStateData1 + 3] ; delta Y
    ld b, a
    ld a, [wSpriteStateData1 + 5] ; delta X
    ld c, a
    sla b
    sla c
    ld a, [hSCY]

    Modify this so that 60FPS mode does not double them.
    .scrollBackgroundAndSprites
    ld a, [wSpriteStateData1 + 3] ; delta Y
    ld b, a
    ld a, [wSpriteStateData1 + 5] ; delta X
    ld c, a
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - halve the x & y deltas
    call Check60fps
    jr nz, .xy60fpsEnd
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    sla b
    sla c
    .xy60fpsEnd
    ld a, [hSCY]


    Doing great so far!

    Walking animations currently run at double-speed. Let's fix that.


    Open up engine/overworld/movement.asm and scroll down to the bottom to create a new function called sprite60fps.
    sprite60fps:
    push hl
    push af
    ld h, $c2
    ld l, $0a
    ld a, [H_CURRENTSPRITEOFFSET]
    add l
    ld l, a ;HL now points to C2xA
    ld a, [wUnusedD721]
    bit 4, a
    ld a, [hl] ;load into A the object 60FPS byte, which holds either 0 or 1
    jr nz, .is60fps
    xor a ;Xor A with itself to clear it to zero if 60FPS mode is inactive
    jr .end
    .is60fps
    xor $01 ;Xor A with $01 to toggle it between 0 or 1 if 60FPS mode is active
    .end
    ld [hl], a ;update C2xA with the new toggled value in A
    ld b, a ;Store the new toggled value in B
    pop af
    pop hl
    ret
    What this will do is toggle the value in the C2xA address for the x'th object being referenced every frame. If 60FPS mode is inactive, it will keep this value as 0. If it is active, it will be a value of 1 every other frame. This value is also returned in the B register.

    No go to UpdatePlayerSprite.moving where you will call the sprite60fps function at a specific spot.
    .moving
    ld a, [wd736]
    bit 7, a ; is the player sprite spinning due to a spin tile?
    jr nz, .skipSpriteAnim
    ld a, [H_CURRENTSPRITEOFFSET]
    add $7
    ld l, a
    ld a, [hl]
    inc a
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - only update every other tick
    call sprite60fps
    sub b
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld [hl], a
    cp 4
    jr nz, .calcImageIndex
    ld a, [hl] followed by inc a is updating the animation counter. You only want to do this every other frame. Calling sprite60fps and subtracting B from A does this for you by undoing the last increment of A every other frame.

    NPCs update their movement twice as fast, so this needs to be corrected.
    Go to UpdateSpriteMovementDelay and modify it as such.
    UpdateSpriteMovementDelay:
    ld h, $c2
    ld a, [H_CURRENTSPRITEOFFSET]
    add $6
    ld l, a
    ld a, [hl] ; c2x6: movement byte 1
    inc l
    inc l
    cp $fe
    jr nc, .tickMoveCounter ; values $fe or $ff
    ld [hl], $0
    jr .moving
    .tickMoveCounter
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - make the delay counter update every other tick
    ld a, [hl]
    call sprite60fps
    add b
    ld [hl], a
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    dec [hl] ; c2x8: frame counter until next movement
    jr nz, notYetMoving
    .moving

    UpdateSpriteInWalkingAnimation also needs to have several things update every other frame. Modify it as such.
    UpdateSpriteInWalkingAnimation:
    ld a, [H_CURRENTSPRITEOFFSET]
    add $7
    ld l, a
    ld a, [hl] ; c1x7 (counter until next walk animation frame)
    inc a
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - updated xy every other tick
    call sprite60fps
    push bc
    sub b
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld [hl], a ; c1x7 += 1
    cp $4
    jr nz, .noNextAnimationFrame
    xor a
    ld [hl], a ; c1x7 = 0
    inc l
    ld a, [hl] ; c1x8 (walk animation frame)
    inc a
    and $3
    ld [hl], a ; advance to next animation frame every 4 ticks (16 ticks total for one step)
    .noNextAnimationFrame
    ld a, [H_CURRENTSPRITEOFFSET]
    add $3
    ld l, a
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - updated xy every other tick
    pop bc
    push bc
    ld a, b
    and a
    jr nz, .xydone
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld a, [hli] ; c1x3 (movement Y delta)
    ld b, a
    ld a, [hl] ; c1x4 (screen Y position)
    add b
    ld [hli], a ; update screen Y position
    ld a, [hli] ; c1x5 (movement X delta)
    ld b, a
    ld a, [hl] ; c1x6 (screen X position)
    add b
    ld [hl], a ; update screen X position
    .xydone
    ld a, [H_CURRENTSPRITEOFFSET]
    ld l, a
    inc h
    ld a, [hl] ; c2x0 (walk animation counter)
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - make the delay decounter update every other tick
    pop bc
    add b ;60fps
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    dec a
    ld [hl], a ; update walk animation counter
    ret nz

    Finally the last part. DoScriptedNPCMovement needs updates for the rare instances where the player character does scripted movement following behind a NPC. And example of this is when you follow Oak into his lab at the start of the game.
    DoScriptedNPCMovement:
    ; This is an alternative method of scripting an NPC's movement and is only used
    ; a few times in the game. It is used when the NPC and player must walk together
    ; in sync, such as when the player is following the NPC somewhere. An NPC can't
    ; be moved in sync with the player using the other method.
    ld a, [wd730]
    bit 7, a
    ret z

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - update animations every other frame and halve movement
    ld de, $00
    ld a, [wUnusedD721]
    bit 4, a
    jr z, .not60fps
    call sprite60fps
    ld e, b
    ld d, $01
    .not60fps
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    ld hl, wd72e
    bit 7, [hl]
    set 7, [hl]
    jp z, InitScriptedNPCMovement
    ld hl, wNPCMovementDirections2
    ld a, [wNPCMovementDirections2Index]
    add l
    ld l, a
    jr nc, .noCarry
    inc h
    .noCarry
    ld a, [hl]
    ; check if moving up
    cp NPC_MOVEMENT_UP
    jr nz, .checkIfMovingDown
    call GetSpriteScreenYPointer
    ld c, SPRITE_FACING_UP
    ld a, -2

    add d ;60fps

    jr .move
    .checkIfMovingDown
    cp NPC_MOVEMENT_DOWN
    jr nz, .checkIfMovingLeft
    call GetSpriteScreenYPointer
    ld c, SPRITE_FACING_DOWN
    ld a, 2

    sub d ;60fps

    jr .move
    .checkIfMovingLeft
    cp NPC_MOVEMENT_LEFT
    jr nz, .checkIfMovingRight
    call GetSpriteScreenXPointer
    ld c, SPRITE_FACING_LEFT
    ld a, -2

    add d ;60fps

    jr .move
    .checkIfMovingRight
    cp NPC_MOVEMENT_RIGHT
    jr nz, .noMatch
    call GetSpriteScreenXPointer
    ld c, SPRITE_FACING_RIGHT
    ld a, 2

    sub d ;60fps

    jr .move
    .noMatch
    cp $ff
    ret
    .move
    ld b, a
    ld a, [hl]
    add b
    ld [hl], a
    ld a, [H_CURRENTSPRITEOFFSET]
    add $9
    ld l, a
    ld a, c
    ld [hl], a ; facing direction
    call AnimScriptedNPCMovement
    ld hl, wScriptedNPCWalkCounter
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;60fps - every other frame, do not decrement walk counter
    ld a, [hl]
    add e
    ld [hl], a
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    dec [hl]
    ret nz
    ld a, 8
    ld [wScriptedNPCWalkCounter], a
    ld hl, wNPCMovementDirections2Index
    inc [hl]
    ret

    And finish things off with some minor updates to AdvanceScriptedNPCAnimFrameCounter.
    AdvanceScriptedNPCAnimFrameCounter:
    ld a, [H_CURRENTSPRITEOFFSET]
    add $7
    ld l, a
    ld a, [hl] ; intra-animation frame counter
    inc a

    sub e ;60fps

    ld [hl], a
    cp 4
    ret nz
    xor a
    ld [hl], a ; reset intra-animation frame counter
    inc l
    ld a, [hl] ; animation frame counter
    inc a
    and $3
    ld [hl], a
    ld [hSpriteAnimFrameCounter], a
    ret


    And there you go! Setting bit 4 of wUnusedD721 will put the game into 60FPS mode. I'll leave in-game activation to you.
     
    Back
    Top