Gameboy Development Forum

Discussion about software development for the old-school Gameboys, ranging from the "Gray brick" to Gameboy Color
(Launched in 2008)

You are not logged in.

#1 2021-12-26 16:12:16

kfmush
New member
Registered: 2021-12-25
Posts: 9

Smooth and consistent diagonal movement in GBDK

Okay, so I just posted another question, but this one I've been pondering for a minute and haven't thought to ask, because I found a hacky solution that works, but it's not perfect.

What I want is a player-controlled sprite (or any sprite, I guess) to move smoothly in diagonal directions at the same speed that they move in cardinal directions.

Basically, you can just make some if statements for each direction and, if the player pushes two adjacent directions, the sprite will move diagonally.  The problem is that the sprites diagonal speed is it's cardinal speed times the square root of 2 (~1.414), which is noticeably faster.

My solution was to add if statements for each diagonal key presses that add a second delay timer for each one.  The value of this delay is the value of the delay for cardinal directions divided by the square root of 2 (and rounded to nearest integer).

This causes the sprite's diagonal speed to be comparable to it's cardinal speed. However, the diagonal movement is relatively choppy.

Here's what I've come up with:

Code:

// CARDINALS -------------------------------------------- */
// LEFT
if (joypad() & J_LEFT){ 
    
    // background collision check                  
    if (collidableTL1(p1XY[0] - 1 , p1XY[1])) {
    if (collidableBL1(p1XY[0] - 1 , p1XY[1])) {
        p1XY[0] -= 1;    
        move_sprite(0,p1XY[0],p1XY[1]);
    }}

    // water collision check
    if (waterTL1(p1XY[0] - 1 , p1XY[1])) {  
    if (waterBL1(p1XY[0] - 1 , p1XY[1])) {   
        move_sprite(0,pstartXY[0], pstartXY[1]);
        p1XY[0] = pstartXY[0];
        p1XY[1] = pstartXY[1];
    }}
}

// RIGHT
if (joypad() & J_RIGHT) { 

    if (collidableTR1(p1XY[0] + 1 , p1XY[1])) { 
    if (collidableBR1(p1XY[0] + 1 , p1XY[1])) {
        p1XY[0] += 1;    
        move_sprite(0,p1XY[0],p1XY[1]);
    }}

    if (waterTR1(p1XY[0] + 1 , p1XY[1])) { 
    if (waterBR1(p1XY[0] + 1 , p1XY[1])) {    
        move_sprite(0,pstartXY[0], pstartXY[1]);
        p1XY[0] = pstartXY[0];
        p1XY[1] = pstartXY[1];
    }}
}

// UP
if(joypad() & J_UP){    

    if(collidableTL1(p1XY[0] , p1XY[1] - 1)) {
    if(collidableTR1(p1XY[0] , p1XY[1] - 1)) {
        p1XY[1] -= 1;    
        move_sprite(0,p1XY[0],p1XY[1]);
    }}

    if(waterTL1(p1XY[0] , p1XY[1] - 1)) { 
    if(waterTR1(p1XY[0] , p1XY[1] - 1)) {   
    move_sprite(0,pstartXY[0], pstartXY[1]);
        p1XY[0] = pstartXY[0];
        p1XY[1] = pstartXY[1];
    }}
}

// DOWN
if(joypad() & J_DOWN){

    if(collidableBL1(p1XY[0] , p1XY[1] + 1)) {
    if(collidableBR1(p1XY[0] , p1XY[1] + 1)) {
        p1XY[1] += 1;                               
        move_sprite(0,p1XY[0],p1XY[1]);
    }}

    if(waterBL1(p1XY[0] , p1XY[1] + 1)) {
    if(waterBR1(p1XY[0] , p1XY[1] + 1)) {
        move_sprite(0,pstartXY[0], pstartXY[1]);
        p1XY[0] = pstartXY[0];
        p1XY[1] = pstartXY[1];
    }}
}

// control movement speed (bigger = slower)
delay(28);


// DIAGONALS -------------------------------------------- */
// slows NW movement to match 4-way speed
if(joypad() & J_LEFT) { if(joypad() & J_UP) {      
    delay(20);  // value should be the previous delay value divided by the square root of 2, rounded to nearest integer.
}}

// slows NE movement to match 4-way speed
if(joypad() & J_RIGHT) { if(joypad() & J_UP) {      
    delay(20);
}}

// slows SE movement to match 4-way speed
if(joypad() & J_RIGHT) { if(joypad() & J_DOWN) {    
    delay(20);
}}

// slows SW movement to match 4-way speed
if(joypad() & J_LEFT) { if(joypad() & J_DOWN) {     
    delay(20);
}
}

----------------------------------------------

One could also make a timer using wait_vbl_done instead of delay, which is a faster and less process-or intensive method, but it has very little fidelity and you don't get a lot of options for movement speed:

Code:

void fastclock(uint8_t numloops){
    for(i = 0; i < numloops; i++){
        wait_vbl_done();
    }
}

One other benefit that method has is that the sprite movement will be very smooth and have no wobble, since it's waiting for a screen refresh to update.

Am I doing this right?  I know this isn't how Link's Awakening does it, since the diagonal movement isn't choppy, so I know there's a better way, maybe in Assembly.  I don't know Assembly, though.  I can learn, but I'm pretty green to coding, in general.

Last edited by kfmush (2021-12-26 16:16:19)

Offline

 

#2 2021-12-27 04:02:32

0x7f
Member
Registered: 2019-11-24
Posts: 65

Re: Smooth and consistent diagonal movement in GBDK

My advise is not to over complicate things. In Link's Awakening it seems to me the player moves 1 pixel per frame in the main direction and only 1 pixel every second frame in the other direction.

Offline

 

#3 2021-12-27 11:09:47

kfmush
New member
Registered: 2021-12-25
Posts: 9

Re: Smooth and consistent diagonal movement in GBDK

That makes sense.  I definitely don't want to overcompensate things, but I'm learning (my background is in art, not programming), so I only know what I know so far.  But that definitely helps me imagine a different way to do it, just gotta figure out the new code.  Thanks a lot!

That's not a perfect scaling of speed on the diagonals, but the difference really isn't noticeable when playing TLoZ:LA.  If I set the diagonal delay to be half the cardinal delay, it has the same effect (done differently) and I honestly couldn't notice any difference in my game.  I just used the square root of 2 because my code easily allowed it and it's technically the most accurate speed for diagonals relative to cardinals.

Offline

 

#4 2021-12-27 11:54:32

kfmush
New member
Registered: 2021-12-25
Posts: 9

Re: Smooth and consistent diagonal movement in GBDK

I just noticed that Link's diagonal movement in Link's Awakening is slightly choppy compared to the diagonal movement!  (I guess that's exciting because it makes me feel less incompetent)  Thinking about what you described, I kept wondering how it would be smooth, since it still skips frames for movement.  I booted up Link's awakening and it does kind of hitch in the diagonals, you just don't notice it because of the animations.  Maybe once I get my sprites animated it won't matter as much.

Edit: I better see what you mean about simplicity.  I don't need the whole set of If statements if I put the delay for each direction.  My error was having the delay outside each direction.  However, it feels too slow and extra choppy -- you can see the character move up and then left each cycle, for instance.  And going back to LoZ:LA, that game feels proportionate in both the cardinals and diagonals.  In my code, it feels extra slow and extra choppy.  Maybe scrolling the sprite instead of moving it would be smoother?

(I also didn't realize I could do if/and statements with &&...  I had tried before and it didn't work, so I thought it was a limitation of C and I had to bury if statements within if statements to get if/and. That simplifies things a lot)

Edit 2: In Link's Awakening, Link definitely moves 1 pixel, cardinally, and 1 in both directions, diagonally, and waits for a screen update.  This feels good because Link is a 16 x 16 sprite.  My sprite is 8 x 8, so that feels really fast, so I have to slow it down to less than every screen refresh, which unavoidably makes diagonals feel choppy, no matter what I do. I'm gonna move on! I can't find any way to make diagonals smoother than what I originally posted.  Anything I try makes the code more complicated and less effective.

Edit 3,: I haven't let it go... lol.  The delay for diagonals should be the difference of  the cardinal delay times the square root of 2 and the cardinal delay. (I'm bad at math's...)

This feels perfect and not at all choppy on the diagonals!:

Code:

//LEFT
        if (joypad() & J_LEFT){  
                p1XY[0] -= 1;    
                move_sprite(0,p1XY[0],p1XY[1]);
        }

        // RIGHT
        if (joypad() & J_RIGHT) {
                p1XY[0] += 1;    
                move_sprite(0,p1XY[0],p1XY[1]);
        }

        // UP
        if(joypad() & J_UP){ 
                p1XY[1] -= 1;    
                move_sprite(0,p1XY[0],p1XY[1]);
        }

        // DOWN
        if(joypad() & J_DOWN){
                p1XY[1] += 1;                               
                move_sprite(0,p1XY[0],p1XY[1]);
        }
        
        // control movement speed (bigger = slower)
        delay(36); // "cardinal"


        // DIAGONALS -------------------------------------------- */

        // slows movement to match 4-way speed
        if((joypad() & J_LEFT && joypad() & J_UP) 
        || (joypad() & J_LEFT && joypad() & J_DOWN)
        || (joypad() & J_RIGHT && joypad() & J_UP)
        || (joypad() & J_RIGHT && joypad() & J_DOWN)) {

            delay(15);  // "diagonal"
                         // for "cardinal" delay = x & "diagonal" delay = y: y ≈ x(√2) - x

        }

Last edited by kfmush (2021-12-27 15:08:11)

Offline

 

#5 2022-01-04 17:58:30

kfmush
New member
Registered: 2021-12-25
Posts: 9

Re: Smooth and consistent diagonal movement in GBDK

I said I'd leave it alone, but I've been thinking a lot about what 0x7f said about the way Link moved and simplicity and I realized my delays in the control settings would delay my entire game script, so I've spent the last few days learning stuff and I've rewritten a lot.  Now I have an animation counter and the directional controls update the player's location on every 2nd frame -- for cardinal directions -- and every 3rd frame -- for diagonal directions.

My code doesn't look much simpler, but I think the logic being fed to the computer is much simpler(?).  It's turned into a set of more complicated if statements, but BGB doesn't report the code is any less efficient this way.

Still, the speed diagonals feel mostly consistent to the speed of the cardinals and I have two collision checks and BGB says the controls take between roughly 10 & 15 % of the CPU (I don't know if that's good or bad, though).  And, since the player's position will be updated with the positions and animations of everything else is, the timings of the controls shouldn't interfere with any other functions in the loop. 

I'm gonna post the code below.  I'm happy for criticism and feedback or for some other novice to use in their own code.  I am relatively new to coding and have been learning so much, so I'm always happy to have a hint towards a better way. This is a ton of fun... I love figuring these things out.

Here is the code for the animation timer:

Code:

void animcounter(uint8_t numframes) {
    if (framecount == numframes) {
        framecount = 0;                            //"framecount" declared at top of file.
    }
    framecount++;
    wait_vbl_done();
}

Here is the code for controls:

Code:

void playercontrols() {

    // soft reset to menu (temporary)
    if(joypad() & J_START) {
        ilevel = 0;
        darkfadeoutslow();
        main();
    }

    // unlocks movement if locked (temp)
    if(unlockmovement == 0) {
        if(joypad() & J_A) {
            unlockmovement = 1;
            waitpadup();
        }
    }

    if(unlockmovement == 1) {
        // locks movement (temp)
        if(joypad() & J_A) {
            unlockmovement = 0;
            waitpadup();
        }

        // DIAGONALS
        if((joypad() & J_LEFT) && (joypad() & J_UP)) {      // checks if diagonals are pressed
            if(framecount %3 == 0) {                        // checks for every third frame
                if(!impassablenw1(p1xy[0] - 1 , p1xy[1])      // collision check for left movement
                && !impassablesw1(p1xy[0] - 1 , p1xy[1])) {                
                    p1xy[0] -= 1;                           // update player location left-wise 1 pixel
                }
                if(!impassablenw1(p1xy[0] , p1xy[1] - 1)      // collision check for up movement
                && !impassablene1(p1xy[0] , p1xy[1] - 1)) {   
                    p1xy[1] -= 1;                           // update player location up-wise 1 pixel
                }
            }   
        }
        
        if((joypad() & J_LEFT) && (joypad() & J_DOWN)) {
            if(framecount %3 == 0) {
                if(!impassablenw1(p1xy[0] - 1 , p1xy[1]) 
                && !impassablesw1(p1xy[0] - 1 , p1xy[1])) {                
                    p1xy[0] -= 1;
                }
                if(!impassablesw1(p1xy[0] , p1xy[1] + 1) 
                && !impassablese1(p1xy[0] , p1xy[1] + 1)) {
                    p1xy[1] += 1;
                }
            }
        }
        
        if((joypad() & J_RIGHT) && (joypad() & J_UP)) {
            if(framecount %3 == 0) {
                if(!impassablene1(p1xy[0] + 1 , p1xy[1]) 
                && !impassablese1(p1xy[0] + 1 , p1xy[1])) {  
                    p1xy[0] += 1;
                }
                if(!impassablenw1(p1xy[0] , p1xy[1] - 1) 
                && !impassablene1(p1xy[0] , p1xy[1] - 1)) {
                    p1xy[1] -= 1;
                }
            } 
        }
        
        if((joypad() & J_RIGHT) && (joypad() & J_DOWN)) {
            if(framecount %3 == 0) {
                if(!impassablene1(p1xy[0] + 1 , p1xy[1]) 
                && !impassablese1(p1xy[0] + 1 , p1xy[1])) {  
                    p1xy[0] += 1;
                }
                if(!impassablesw1(p1xy[0] , p1xy[1] + 1) 
                && !impassablese1(p1xy[0] , p1xy[1] + 1)) {
                    p1xy[1] += 1;
                }
            } 
        }
        
        // CARDINALS
        else if((joypad() & J_LEFT) && !(joypad() & J_UP) && !(joypad() & J_DOWN)) {    // checks for left input without up or down input
            if(framecount %2 == 0) {                                                    // checks for every second frame
                if(!impassablenw1(p1xy[0] - 1 , p1xy[1])                                  // top-left corner collision check
                && !impassablesw1(p1xy[0] - 1 , p1xy[1])) {
                    p1xy[0] -= 1;                                                       // update player location left-wise
                }
            }
        }

        else if((joypad() & J_RIGHT) && !(joypad() & J_UP) && !(joypad() & J_DOWN)) { 
            if(framecount %2 == 0) {
                if(!impassablene1(p1xy[0] + 1 , p1xy[1]) 
                && !impassablese1(p1xy[0] + 1 , p1xy[1])) {  
                    p1xy[0] += 1;
                }
            }
        }

        else if((joypad() & J_UP) && !(joypad() & J_LEFT) && !(joypad() & J_RIGHT)) {    
            if(framecount %2 == 0) {
                if(!impassablenw1(p1xy[0] , p1xy[1] - 1) 
                && !impassablene1(p1xy[0] , p1xy[1] - 1)) {
                    p1xy[1] -= 1;
                }
            }
        }
        else if((joypad() & J_DOWN) && !(joypad() & J_LEFT) && !(joypad() & J_RIGHT)){
            if(framecount %2 == 0) {
                if(!impassablesw1(p1xy[0] , p1xy[1] + 1) 
                && !impassablese1(p1xy[0] , p1xy[1] + 1)) {
                    p1xy[1] += 1;
                }
            }
        }
        
    }
}

And here is the main loop from a level:

Code:

while(1){
        playercontrols();
        watercollision();
        move_sprite(0,p1xy[0],p1xy[1]);
        animcounter(96);      // I picked 96 because of how many factors it has.  Easy to have different spaced timings that don't stutter when the counter loops.
    }

The code for collisions takes up a lot of space, but it's easy to find examples online.  I'll share it if anyone asks.


EDIT:  So, I have learned that computers struggle with division (makes sense, me too).  But as long as it's dividing a power of 2, the performance impact is almost none (I guess because it's base2 math, but I'm still wrapping my head around it). So, I'm having to be careful not to check remainders in the counter for numbers that aren't powers of 2.  I'll have to leave the diagonals at remainder 3 because it just feels right gameplay wise.  I've cleaned things up a lot and I think I have plenty of overhead, still.

EDIT 2: The solution to diagonals was to create a separate timer to call only when the diagonals are pressed that counts to 3 and loops back around. Then, set the movement to update on the second count. But now diagonal movement uses as much CPU as cardinals. Nice.

Code:

if((joypad() & J_LEFT) && (joypad() & J_DOWN)) {
            stepcounter(2);
            if(stepcount == 2) {
...

Edit: i get the maths now... every power of two ads a zero to the last power of 2 in binary.  So the computer simply adds or subtracts that many zeros from the other number, instead of going through long division/multiplication.

Last edited by kfmush (2022-01-08 03:33:46)

Offline

 

Board footer

Powered by PunBB
© Copyright 2002–2005 Rickard Andersson