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.

Ads

#1 2016-08-06 13:01:54

bbsfoo
Member
Registered: 2016-02-08
Posts: 16

How to quickly move the cursor, but not too fast.

I'm writing a menu system, but when I press a key, the cursor too fast (passing a number of menu items).The ideal situation should be like the following video. When press a key, the cursor should immediately move to the next menu item, and pause, and fast-moving, but not too fast.How to do this?


https://youtu.be/xMJzovzmbPs

    while(1)
    {


        if (wait_pad(JOYPAD_RIGHT))
        {
            menu_next_page();
            //wait_pad_up();
        }
       

        if (wait_pad(JOYPAD_LEFT))
        {
            menu_previous_page();
            //wait_pad_up();
        }
       
               
        if (wait_pad(JOYPAD_UP))
        {
            menu_previous_item();
            //wait_pad_up();
        }
       
        if (wait_pad(JOYPAD_DOWN))
        {
            menu_next_item();
            //wait_pad_up();
        }
       

       
        __asm__("halt");
        __asm__("nop");
    }

Last edited by bbsfoo (2016-08-06 13:06:52)

Offline

 

#2 2016-08-06 14:28:56

Xephyr
Member
From: France
Registered: 2015-02-26
Posts: 59

Re: How to quickly move the cursor, but not too fast.

You could add a timer and check if the selected key is still pressed after this timer or not maybe?

Offline

 

#3 2016-08-07 02:16:00

DonaldHays
Member
From: Seattle
Registered: 2016-08-01
Posts: 36
Website

Re: How to quickly move the cursor, but not too fast.

Hi there!

So first, let's look at why you're experiencing the behavior you are.

Your code is an infinite loop that `halt`s at the end. Presumably, the VBlank interrupt is already enabled so that execution resumes after encountering the `halt`. This means your loop will run once per VBlank, or 60 times per second. Your code is very "stateless": it'll do the same logic every time it runs, and that logic involves checking if a button is pressed, and doing an action if it is. So every 1/60th of a second, your code says, "are they holding the Up button? If so, select the previous item." There's nothing to check "did they start holding the Up button on this pass through the loop?" So as long as you're holding a button, the corresponding action will run once every 60th of a second, and so you see the cursor scrolling crazy fast. To select a specific item, the user would need to release the button on the exact frame where that item became selected. Except for speed runners, you generally don't want to be asking your users to be making frame-perfect inputs!

So clearly we need to keep track of information between passes through the loop, and that information will determine how we interpret the state of the joypad. So what do we need to keep track of? Well, first, let's work out exactly what logic we desire:
1) When the user first presses a button, its action should be immediately performed
2) After a duration of time (I'm going to say half a second, but you can change as you desire), if the user hasn't lifted the button, you should run the action again
3) As long as they keep the button held down, repeat the last step, but with subsequent durations waiting for less time (I'll say a tenth of a second)
4) As long as the button is held down, you should ignore presses of the other three direction buttons
5) Once the button is lifted, you should go back to the first step, waiting for a button to be pressed

So we should keep track of:
- If a button has been pressed (to know whether or not to begin the process specified by (1), and whether or not to ignore buttons in (4))
- Which button was pressed (to do the action in (2), and to be able to detect when it's lifted in (5))
- How much time to wait until triggering the button's action again (to handle the waits in (2) and (3))

Let's create some variables to track this information.

Code:

UINT8 is_button_pressed;
UINT8 pressed_button;
UINT8 time_until_action;

In your function, before running your while loop, we need to give the `is_button_pressed` variable a sensible default value. We can ignore the uninitialized values of `pressed_button` and `time_until_action` for now, because we'll write them later before we try to read them.

Code:

is_button_pressed = 0;

Now let's write the stub of our while loop.

Code:

is_button_pressed = 0;
while(1) {
  
  __asm__("halt");
  __asm__("nop");
}

So first, we'll do different things depending on whether or not the button was already pressed when we began a pass through the loop, so let's put an `if` check on the variable. We'll also read from the joypad and store it in a temporary variable.

Code:

UINT8 current_joypad;
is_button_pressed = 0;
while(1) {
  current_joypad = joypad();
  
  if(is_button_pressed == 1) {
    // There was already a button pressed!
    // Note that `is_button_pressed` is a separate variable from `current_joypad`,
    // and isn't automatically changed when `current_joypad` changes.
  } else {
    // There wasn't already a button pressed.
  }
  
  __asm__("halt");
  __asm__("nop");
}

Now let's handle (1), the case where the user begins pressing a button. We will make note of the fact that a button was pressed, which button was pressed, setup the timer for the next execution, and call the button's action. Also note that I'm going to replace the previous `else` with an `else if` to detect specifically the presses of the direction pad buttons. Finally, note that this will also take care of (4). While a button is already pressed, we automatically won't check any of the other buttons.

Code:

UINT8 current_joypad;
is_button_pressed = 0;
while(1) {
  current_joypad = joypad();
  
  if(is_button_pressed == 1) {
    // There was already a button pressed!
    // Note that `is_button_pressed` is a separate variable from `current_joypad`,
    // and isn't automatically changed when `current_joypad` changes.
  } else if((current_joypad & (J_LEFT | J_RIGHT | J_UP | J_DOWN)) != 0) {
    is_button_pressed = 1;
    time_until_action = 30; // 60 frames per second * 0.5 seconds = 30 frames

    if(current_joypad & J_LEFT) {
      pressed_button = J_LEFT;
      menu_previous_page();
    } else if(current_joypad & J_RIGHT) {
      pressed_button = J_RIGHT;
      menu_next_page();
    } else if(current_joypad & J_UP) {
      pressed_button = J_UP;
      menu_previous_item();
    } else if(current_joypad & J_DOWN) {
      pressed_button = J_DOWN;
      menu_next_item();
    }
  }
  
  __asm__("halt");
  __asm__("nop");
}

Now let's handle (5): what happens when the user releases a button. That's going to happen inside the first `if` block (where we know there was a button already being pressed). Inside that `if` block, if the button is still being pressed, we want to do one thing, but if it has been released we want to do something else, so we'll do another `if` statement inside of it. When they release the button, we want to simply set `is_button_pressed` to 0. We don't need to do anything with the values of `pressed_button` and `time_until_action`, because the code that reads them won't run again until the next time a button is pressed (which will rewrite their values).

Code:

UINT8 current_joypad;
is_button_pressed = 0;
while(1) {
  current_joypad = joypad();
  
  if(is_button_pressed == 1) {
    if(current_joypad & pressed_button) {
      // They're still pressing the button!
    } else {
      is_button_pressed = 0;
    }
  } else if((current_joypad & (J_LEFT | J_RIGHT | J_UP | J_DOWN)) != 0) {
    is_button_pressed = 1;
    time_until_action = 30; // 60 frames per second * 0.5 seconds = 30 frames

    if(current_joypad & J_LEFT) {
      pressed_button = J_LEFT;
      menu_previous_page();
    } else if(current_joypad & J_RIGHT) {
      pressed_button = J_RIGHT;
      menu_next_page();
    } else if(current_joypad & J_UP) {
      pressed_button = J_UP;
      menu_previous_item();
    } else if(current_joypad & J_DOWN) {
      pressed_button = J_DOWN;
      menu_next_item();
    }
  }
  
  __asm__("halt");
  __asm__("nop");
}

Finally, we want to decrease our timer value. Once the timer runs out, we will reset the timer and call the appropriate action for our pressed button, handling (2) and (3).

Code:

UINT8 current_joypad;
is_button_pressed = 0;
while(1) {
  current_joypad = joypad();
  
  if(is_button_pressed == 1) {
    if(current_joypad & pressed_button) {
      time_until_action = time_until_action - 1;

      if(time_until_action == 0) { // The timer has run out!
        time_until_action = 6; // 60 frames per second * 0.1 seconds = 6 frames

        if(pressed_button == J_LEFT) {
          menu_previous_page();
        } else if(pressed_button == J_RIGHT) {
          menu_next_page();
        } else if(pressed_button == J_UP) {
          menu_previous_item();
        } else if(pressed_button == J_DOWN) {
          menu_next_item();
        }
      }
    } else {
      is_button_pressed = 0;
    }
  } else if((current_joypad & (J_LEFT | J_RIGHT | J_UP | J_DOWN)) != 0) {
    is_button_pressed = 1;
    time_until_action = 30; // 60 frames per second * 0.5 seconds = 30 frames

    if(current_joypad & J_LEFT) {
      pressed_button = J_LEFT;
      menu_previous_page();
    } else if(current_joypad & J_RIGHT) {
      pressed_button = J_RIGHT;
      menu_next_page();
    } else if(current_joypad & J_UP) {
      pressed_button = J_UP;
      menu_previous_item();
    } else if(current_joypad & J_DOWN) {
      pressed_button = J_DOWN;
      menu_next_item();
    }
  }
  
  __asm__("halt");
  __asm__("nop");
}

So hopefully that makes sense! Also, hopefully there's no bugs in it, because that would be pretty embarrassing for me! Also, I did use some function and variable names I was familiar with from GBDK, which may be different than what you're using, so you might have to munge what I wrote a bit. Finally, some folks may notice a few places for optimization (such as merging `is_button_pressed` and `pressed_button`, or replacing the last `else if` in the directional compound `if` statements with just an `else`), but I tried to focus the code on simplicity and clarity. Good luck!

Last edited by DonaldHays (2016-08-07 02:57:29)

Offline

 

#4 2016-08-09 13:58:45

bbsfoo
Member
Registered: 2016-02-08
Posts: 16

Re: How to quickly move the cursor, but not too fast.

DonaldHays wrote:

Hi there!

So first, let's look at why you're experiencing the behavior you are.

Your code is an infinite loop that `halt`s at the end. Presumably, the VBlank interrupt is already enabled so that execution resumes after encountering the `halt`. This means your loop will run once per VBlank, or 60 times per second. Your code is very "stateless": it'll do the same logic every time it runs, and that logic involves checking if a button is pressed, and doing an action if it is. So every 1/60th of a second, your code says, "are they holding the Up button? If so, select the previous item." There's nothing to check "did they start holding the Up button on this pass through the loop?" So as long as you're holding a button, the corresponding action will run once every 60th of a second, and so you see the cursor scrolling crazy fast. To select a specific item, the user would need to release the button on the exact frame where that item became selected. Except for speed runners, you generally don't want to be asking your users to be making frame-perfect inputs!

So clearly we need to keep track of information between passes through the loop, and that information will determine how we interpret the state of the joypad. So what do we need to keep track of? Well, first, let's work out exactly what logic we desire:
1) When the user first presses a button, its action should be immediately performed
2) After a duration of time (I'm going to say half a second, but you can change as you desire), if the user hasn't lifted the button, you should run the action again
3) As long as they keep the button held down, repeat the last step, but with subsequent durations waiting for less time (I'll say a tenth of a second)
4) As long as the button is held down, you should ignore presses of the other three direction buttons
5) Once the button is lifted, you should go back to the first step, waiting for a button to be pressed

So we should keep track of:
- If a button has been pressed (to know whether or not to begin the process specified by (1), and whether or not to ignore buttons in (4))
- Which button was pressed (to do the action in (2), and to be able to detect when it's lifted in (5))
- How much time to wait until triggering the button's action again (to handle the waits in (2) and (3))

Let's create some variables to track this information.

Code:

UINT8 is_button_pressed;
UINT8 pressed_button;
UINT8 time_until_action;

In your function, before running your while loop, we need to give the `is_button_pressed` variable a sensible default value. We can ignore the uninitialized values of `pressed_button` and `time_until_action` for now, because we'll write them later before we try to read them.

Code:

is_button_pressed = 0;

Now let's write the stub of our while loop.

Code:

is_button_pressed = 0;
while(1) {
  
  __asm__("halt");
  __asm__("nop");
}

So first, we'll do different things depending on whether or not the button was already pressed when we began a pass through the loop, so let's put an `if` check on the variable. We'll also read from the joypad and store it in a temporary variable.

Code:

UINT8 current_joypad;
is_button_pressed = 0;
while(1) {
  current_joypad = joypad();
  
  if(is_button_pressed == 1) {
    // There was already a button pressed!
    // Note that `is_button_pressed` is a separate variable from `current_joypad`,
    // and isn't automatically changed when `current_joypad` changes.
  } else {
    // There wasn't already a button pressed.
  }
  
  __asm__("halt");
  __asm__("nop");
}

Now let's handle (1), the case where the user begins pressing a button. We will make note of the fact that a button was pressed, which button was pressed, setup the timer for the next execution, and call the button's action. Also note that I'm going to replace the previous `else` with an `else if` to detect specifically the presses of the direction pad buttons. Finally, note that this will also take care of (4). While a button is already pressed, we automatically won't check any of the other buttons.

Code:

UINT8 current_joypad;
is_button_pressed = 0;
while(1) {
  current_joypad = joypad();
  
  if(is_button_pressed == 1) {
    // There was already a button pressed!
    // Note that `is_button_pressed` is a separate variable from `current_joypad`,
    // and isn't automatically changed when `current_joypad` changes.
  } else if((current_joypad & (J_LEFT | J_RIGHT | J_UP | J_DOWN)) != 0) {
    is_button_pressed = 1;
    time_until_action = 30; // 60 frames per second * 0.5 seconds = 30 frames

    if(current_joypad & J_LEFT) {
      pressed_button = J_LEFT;
      menu_previous_page();
    } else if(current_joypad & J_RIGHT) {
      pressed_button = J_RIGHT;
      menu_next_page();
    } else if(current_joypad & J_UP) {
      pressed_button = J_UP;
      menu_previous_item();
    } else if(current_joypad & J_DOWN) {
      pressed_button = J_DOWN;
      menu_next_item();
    }
  }
  
  __asm__("halt");
  __asm__("nop");
}

Now let's handle (5): what happens when the user releases a button. That's going to happen inside the first `if` block (where we know there was a button already being pressed). Inside that `if` block, if the button is still being pressed, we want to do one thing, but if it has been released we want to do something else, so we'll do another `if` statement inside of it. When they release the button, we want to simply set `is_button_pressed` to 0. We don't need to do anything with the values of `pressed_button` and `time_until_action`, because the code that reads them won't run again until the next time a button is pressed (which will rewrite their values).

Code:

UINT8 current_joypad;
is_button_pressed = 0;
while(1) {
  current_joypad = joypad();
  
  if(is_button_pressed == 1) {
    if(current_joypad & pressed_button) {
      // They're still pressing the button!
    } else {
      is_button_pressed = 0;
    }
  } else if((current_joypad & (J_LEFT | J_RIGHT | J_UP | J_DOWN)) != 0) {
    is_button_pressed = 1;
    time_until_action = 30; // 60 frames per second * 0.5 seconds = 30 frames

    if(current_joypad & J_LEFT) {
      pressed_button = J_LEFT;
      menu_previous_page();
    } else if(current_joypad & J_RIGHT) {
      pressed_button = J_RIGHT;
      menu_next_page();
    } else if(current_joypad & J_UP) {
      pressed_button = J_UP;
      menu_previous_item();
    } else if(current_joypad & J_DOWN) {
      pressed_button = J_DOWN;
      menu_next_item();
    }
  }
  
  __asm__("halt");
  __asm__("nop");
}

Finally, we want to decrease our timer value. Once the timer runs out, we will reset the timer and call the appropriate action for our pressed button, handling (2) and (3).

Code:

UINT8 current_joypad;
is_button_pressed = 0;
while(1) {
  current_joypad = joypad();
  
  if(is_button_pressed == 1) {
    if(current_joypad & pressed_button) {
      time_until_action = time_until_action - 1;

      if(time_until_action == 0) { // The timer has run out!
        time_until_action = 6; // 60 frames per second * 0.1 seconds = 6 frames

        if(pressed_button == J_LEFT) {
          menu_previous_page();
        } else if(pressed_button == J_RIGHT) {
          menu_next_page();
        } else if(pressed_button == J_UP) {
          menu_previous_item();
        } else if(pressed_button == J_DOWN) {
          menu_next_item();
        }
      }
    } else {
      is_button_pressed = 0;
    }
  } else if((current_joypad & (J_LEFT | J_RIGHT | J_UP | J_DOWN)) != 0) {
    is_button_pressed = 1;
    time_until_action = 30; // 60 frames per second * 0.5 seconds = 30 frames

    if(current_joypad & J_LEFT) {
      pressed_button = J_LEFT;
      menu_previous_page();
    } else if(current_joypad & J_RIGHT) {
      pressed_button = J_RIGHT;
      menu_next_page();
    } else if(current_joypad & J_UP) {
      pressed_button = J_UP;
      menu_previous_item();
    } else if(current_joypad & J_DOWN) {
      pressed_button = J_DOWN;
      menu_next_item();
    }
  }
  
  __asm__("halt");
  __asm__("nop");
}

So hopefully that makes sense! Also, hopefully there's no bugs in it, because that would be pretty embarrassing for me! Also, I did use some function and variable names I was familiar with from GBDK, which may be different than what you're using, so you might have to munge what I wrote a bit. Finally, some folks may notice a few places for optimization (such as merging `is_button_pressed` and `pressed_button`, or replacing the last `else if` in the directional compound `if` statements with just an `else`), but I tried to focus the code on simplicity and clarity. Good luck!

thank you very much!
your information is very useful.
my menu system works!
look at this  https://youtu.be/E8NZEhAv9BA

Offline

 

#5 2016-08-09 16:50:40

DonaldHays
Member
From: Seattle
Registered: 2016-08-01
Posts: 36
Website

Re: How to quickly move the cursor, but not too fast.

bbsfoo wrote:

thank you very much!
your information is very useful.
my menu system works!
look at this  https://youtu.be/E8NZEhAv9BA

Awesome! Glad it helped!

Offline

 

Board footer

Powered by PunBB
© Copyright 2002–2005 Rickard Andersson