Input Handling : Keypress with Repeat Rate

There are a lot of ways to deal with user input in your game. This tutorial will show one of the solutions that you can use on certain parts of the game. Like when your prompting the user to exit the game or not, while in the main menu, or when you want the user to choose from some options on the screen. As a warning, we're going to limit the code to using the getKeyStates() method only.


The Problem : It's Like The Keys Get Stuck..
Continues key presses are good while playing the game because the game can respond at the heat of the moment and match the users super reflexes. But not during selection screens. Why so? Let's say your game has a frame rate of 15 frames per second. That's also the speed at which it can act every time the user presses a key on the phone.

Imagine that in your game the second item from the top of the list is the View Instructions item and the current selected or highlighted item is the New Game item which is the first item on the menu. The user, trying to get to the Instructions item, presses the down button and the selection moves down from one item to the other at an alarming rate of 15 times in one second. The confused user will not be able to accurately pick items from the menu.





Better yet, imagine typing a document in a text editor and every time you hit the keyboard you get 15 of each character you type in. It would be like driving a car without breaks (No, I'm not drawing that for you!!!).


Making the Keys Toggle
In this tutorial we're going to add some interactivity to the spinning globe we made in the last tutorial : Clipping Images or Displaying Only Parts of an Image. We'll change the code so that the user can adjust the speed at which the globe spins. So load the project in NetBeans and open the clsCanvas code.

Let's declare some constants and variables. Insert this code under the clsCanvas class declaration:


public class clsCanvas extends GameCanvas implements Runnable {

// key repeat rate in milliseconds
public static final int keyDelay = 250;

//key constants
public static final int upKey = 0;
public static final int leftKey = 1;
public static final int downKey = 2;
public static final int rightKey = 3;
public static final int fireKey = 4;

//key states for up, left, down, right, and fire key
private boolean[] isDown = {
false, false, false, false, false
};

//last time the key changed state
private long[] keyTick = {
0, 0, 0, 0, 0
};

//lookup table for key constants :P
private int[] keyValue = {
GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED,
GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED,
GameCanvas.FIRE_PRESSED
};




The value assigned to the constant keyDelay will determine how fast a key state changes from being pressed to not pressed while holding down a key. In other words, this will be the repeat rate. The higher the number, the slower the repeat rate gets and vice versa.

The next five constants we declared are for the 5 standard game keys Up, Left, Down, Right, and the Fire key. We just defined them so our code won't become confusing, so instead of using numbers we can use "meaningful words". Also, someone told me about 12-14 years ago that it was good practice to define constants for numbers or other fixed values your going to use repeatedly all over your code.

The boolean array isDown[] is going to hold the state for each of the standard keys we've defined while the long array keyTick[] will hold the time when a key last changed from being down to up.

The last array keyValue[], an integer array to hold the actual key codes for each key. We defined this so we can loop through each key in our key detection code which you will see in the next code section.

We'll add a new method to our clsCanvas class called checkKeys(). Add the code above the run() method:


public void checkKeys(int iKey, long currTick){
long elapsedTick = 0;

//loop through the keys
for (int i = 0; i < 5; i++){

// by default, key not pressed by user
isDown[i] = false;

// is user pressing the key
if ((iKey & keyValue[i]) != 0){
elapsedTick = currTick - keyTick[i];

//is it time to toggle key state?
if (elapsedTick >= keyDelay){

// save the current time
keyTick[i] = currTick;

// toggle the state to down or pressed
isDown[i] = true;
}
}
}
}


public void run() {



The checkKeys() method takes the value returned by the getKeyStates() method in the iKey parameter and the current time in milliseconds in the currTick parameter. It then loops through all the keys in the keyValue[] array too see if a certain key is being pressed. It then toggles the value of the appropriate element in the isDown[] array depending on how long it's been since the key was in the pressed state. It also makes sure that the isDown[] array is updated as to which keys are not being pressed.

Let's change our run() method so that when the user presses the left key the spinning animation speeds up and when the user presses the right key the animation slows down. First let's get rid of the old way we detect the fire key so that it makes use of the new code we just added. So delete or comment the following lines from the run() method, from inside the while loop:


/*
if ((iKey & GameCanvas.FIRE_PRESSED) != 0){
isRunning = false;
}
*/



Next, insert the following code under the line where we store the current time in lCurrTick so we can pass that value to the checkKeys() method:


lCurrTick = System.currentTimeMillis();

checkKeys(iKey, lCurrTick);

if (isDown[leftKey]){
if (lDelay > 0){
lDelay-=10;
}
} else if (isDown[rightKey]){
if (lDelay < 1000){
lDelay+=10;
}
} else if (isDown[fireKey]){
isRunning = false;
}




Add this next code just before the second call to setClip() so we can display some feedback when the keys are pressed:


g.drawString("lDelay : " + Long.toString(lDelay), 2, 108, Graphics.TOP | Graphics.LEFT);
if (isDown[upKey]) {
g.drawString("up key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
} else if (isDown[leftKey]) {
g.drawString("left key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
} else if (isDown[downKey]) {
g.drawString("down key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
} else if (isDown[rightKey]) {
g.drawString("right key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);

}

//clip the drawing area to a single frame
g.setClip(50, 50, 16, 16);



The last code we added displays the value of lDelay so we know if it's actually getting changed. It also displays notification whenever one of the directional keys are pressed. It doesn't display feedback for the Fire key because when that is pressed the MIDlet is terminated.

Here's the completed clsCanvas source code:


package MyGame;

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;

public class clsCanvas extends GameCanvas implements Runnable {

// key repeat rate in milliseconds
public static final int keyDelay = 250;

//key constants
public static final int upKey = 0;
public static final int leftKey = 1;
public static final int downKey = 2;
public static final int rightKey = 3;
public static final int fireKey = 4;

//key states for up, left, down, right, and fire key
private boolean[] isDown = {
false, false, false, false, false
};

//last time the key changed state
private long[] keyTick = {
0, 0, 0, 0, 0
};

//lookup table for key constants :P
private int[] keyValue = {
GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED,
GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED,
GameCanvas.FIRE_PRESSED
};

private boolean isRunning = true;
private Graphics g;
private midMain fParent;
private Image imgEarth;

public clsCanvas(midMain m) {
super(true);
fParent = m;
setFullScreenMode(true);
}

public void start(){
Thread runner = new Thread(this);
runner.start();
}

public void load(){
try{
// try to load the image file
imgEarth = Image.createImage("/images/earthstrip.png");
}catch(Exception ex){
// exit the app if it fails to load the image
isRunning = false;
return;
}
}

public void unload(){
// make sure the object get's destroyed
imgEarth = null;
}

public void checkKeys(int iKey, long currTick){
long elapsedTick = 0;

//loop through the keys
for (int i = 0; i < 5; i++){

// by default, key not pressed by user
isDown[i] = false;

// is user pressing the key
if ((iKey & keyValue[i]) != 0){
elapsedTick = currTick - keyTick[i];

//is it time to toggle key state?
if (elapsedTick >= keyDelay){

// save the current time
keyTick[i] = currTick;

// toggle the state to down or pressed
isDown[i] = true;
}
}
}
}

public void run() {
int iKey = 0;

int imgX = 50; // x coordinate of the image
int frameIndex = 0; // current frame to be drawn
long lDelay = 250; //time to pause between frames in milliseconds
long lStart = 0; //time we last changed frames in milliseconds
long lCurrTick = 0; // current system time in milliseconds;

load();
g = getGraphics();
while(isRunning){

iKey = getKeyStates();
/*
if ((iKey & GameCanvas.FIRE_PRESSED) != 0){
isRunning = false;
}
*/

lCurrTick = System.currentTimeMillis();

checkKeys(iKey, lCurrTick);

if (isDown[leftKey]){
if (lDelay > 0){
lDelay-=10;
}
} else if (isDown[rightKey]){
if (lDelay < 1000){
lDelay+=10;
}
} else if (isDown[fireKey]){
isRunning = false;
}

if ((lCurrTick-lStart) >= lDelay){
lStart = lCurrTick; // save the current time
if (frameIndex < 8) {
frameIndex++; // skip to the next frame
} else {
frameIndex = 0; // go back to first frame
}
imgX = 50 - (frameIndex * 16); // compute x relative to clip rect
}

//restore the clipping rectangle to full screen
g.setClip(0, 0, getWidth(), getHeight());

//set drawing color to black
g.setColor(0x000000);
//fill the whole screen
g.fillRect(0, 0, getWidth(), getHeight());
// set drawing color to white
g.setColor(0xffffff);
//display the key code last pressed
g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT);

g.drawString("Frame : " + Integer.toString(frameIndex), 2, 68, Graphics.TOP | Graphics.LEFT);
g.drawString("X : " + Integer.toString(imgX), 2, 88, Graphics.TOP | Graphics.LEFT);

g.drawString("lDelay : " + Long.toString(lDelay), 2, 108, Graphics.TOP | Graphics.LEFT);
if (isDown[upKey]) {
g.drawString("up key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
} else if (isDown[leftKey]) {
g.drawString("left key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
} else if (isDown[downKey]) {
g.drawString("down key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
} else if (isDown[rightKey]) {
g.drawString("right key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);

}

//clip the drawing area to a single frame
g.setClip(50, 50, 16, 16);
//draw the image
g.drawImage(imgEarth, imgX, 50, Graphics.TOP | Graphics.LEFT);

flushGraphics();

try{
Thread.sleep(30);
} catch (Exception ex){

}
}
g = null;
unload();
fParent.destroyApp(false);
fParent = null;
}
}



Great! Now you can hit F6 and view the result of your work.

Output on Sun Java WTK 2.5.1




Ponts to Ponder
There are a few more things you can do to the sample code presented in this tutorial. For instance, you can add support for the other game keys or move the checkKeys() method in it's own class along with the supporting variables and even make it a static method. You can also change the value of keyDelay to that which suits you. Finally, with a few modification you can use the same technique when handling key codes from keyPressed() and keyReleased() call back methods.

Although the sample code used here is quite usable, it doesn't mean you can't do it in your own way. What matters most is that you have a general understanding of the problem and how to solve it in a simple but effective manner.

Something is not working? I got it totally wrong? You got a question? Feel free to post a comment. XD

4 comments   |   post a comment
hello,
i got a question 4 ya.. by finishing this tutorial i checked every code in classes but when i am trying to run my application it gives me totaly white screen. Meanwhile, when the application is trying to exit it shows the screen which should be shown on screen for a sec. then app. is exiting...

what is wrong with it ?
said...
The tutorial on da whole is vry enlightening.But did not understand how the kval array was initialised.........ie....Gamecanvas objects like LEFT_PRESSED etc. nd how did we check whether or not a key was pressed.
said...
Hello! First of all, great blog! but i have encountered a problem with this one.

Mine is functioning, but it does not increase or decrease the delay even if i press the left or right buttons. also, it does not exit the application (anymore) if i press the fire or the number 5.

From my observation, we created a method called "checkKeys" but we really didn't called that. (unless i missed it)

Thanks for you help!
said...
ah i got it, we just need to add this like after we get the iKey and lCurrTick:

checkKeys(iKey, lCurrTick);

and it works :)