Skip to content
Heiko Brumme edited this page Jan 12, 2017 · 5 revisions

Handling input in LWJGL3 is easily done by using GLFW, for this tutorial we will take a look how to handle input from keyboard, mouse and controllers like joysticks or gamepads.

Processing events

Before we start you should know, that in LWJGL3 we need to call glfwPollEvents() to get the events that have already been received.
Another way to get events is by calling glfwWaitEvents() which will wait until a event is received, until then the thread will be sleeping, but you could wake it up by calling glfwPostEmptyEvent(). This could be useful if you want to make a text adventure with GLFW. But for a real time application you shouldn't wait for events, so we will just poll the events after each frame.

Another thing you might want to use is glfwSetInputMode(window, mode, value).
The mode must be one of GLFW_CURSOR, GLFW_STICKY_KEYS or GLFW_STICKY_MOUSE_BUTTONS.
For GLFW_STICKY_KEYS and GLFW_STICKY_MOUSE_BUTTONS the value can be one of GLFW_TRUE and GLFW_FALSE.
With GLFW_CURSOR the value can be one of GLFW_CURSOR_NORMAL, GLFW_CURSOR_HIDDEN or GLFW_CURSOR_DISABLED.

Keyboard input

Key input

For keyboard input we can poll the key states with glfwGetKey(window, key) where window is the window handle and key is something like GLFW_KEY_<key> for example GLFW_KEY_W or GLFW_KEY_SPACE, you could also get GLFW_KEY_UNKNOWN if you push a special key like the E-mail key. With the call to glfwGetKey you will get the key state which is one of GLFW_PRESS and GLFW_RELEASE.

int state = glfwGetKey(window, GLFW_KEY_UP);
if (state == GLFW_PRESS) {
    moveUp();
}

But there is a problem with polling, you could miss a pressed key if it gets released before it gets polled, but there's a simple solution for it, you could just set sticky keys with the help of glfwSetInputMode(window, mode, value).
If we set GLFW_STICKY_KEYS to GLFW_TRUE the key state will be GLFW_PRESS until you poll that key even if it has already been released. It is useful if you only want to know if a key was pressed and it isn't important in which order the keys where pressed.

glfwSetInputMode(window, GLFW_STICKY_KEYS, GLFW_TRUE);

Most of the time polling the input is sufficient, but it is recommended to use callbacks.
The key callback is invoked when a physical key is pressed or when it gets released, also when it is repeated, we can easily set a key callback via glfwSetKeyCallback(window, cbfun). Normally you would put a function pointer in there, but in Java we don't have something like that, but luckily LWJGL provides a GLFWKeyCallback class for this. But you need a strong reference to the callback, so that it won't get garbage collected, so just put a reference like private GLFWKeyCallback keyCallback in your class.

glfwSetKeyCallback(window, keyCallback = new GLFWKeyCallback() {

    @Override
    public void invoke(long window, int key, int scancode, int action, int mods) {
        /* Do something */
    }
}

Alternatively you could also use lambda expressions because the GLFWKeyCallback provides also a single abstract method (or simply SAM).

glfwSetKeyCallback(window, keyCallback = GLFWKeyCallback.create((window, key, scancode, action, mods) -> {
    /* Do something */
}));

Its up to you which version you want to use, but now let's have a look at the variables. The first two should be clear by now window is the window in which the event was received and key is GLFW_KEY_<key> like before.
The scancode is the system-specific scancode of the key, but you need it only if the key is GLFW_KEY_UNKNOWN.
The state of the key is stored in action which is one of GLFW_PRESS, GLFW_RELEASE or GLFW_REPEAT and finally the mods is a bitfield of modifier keys that where pressed, it can contain GLFW_MOD_SHIFT, GLFW_MOD_CONTROL, GLFW_MOD_ALT and GLFW_MOD_SUPER.
For example you want to see if Control + Alt + F was pressed you would check it like in the following code.

int ctrlAlt = GLFW_MOD_ALT | GLFW_MOD_CONTROL;

if ((mods & ctrlAlt) == ctrlAlt && key == GLFW_KEY_F && action == GLFW_PRESS) {
    System.out.println("Control + Alt + F was pressed!");
}

Text input

Another thing you might want to use, for example in a text adventure is text input.
But this time you can only do it with callbacks, there is no method for polling text input. Here we make use of the GLFWCharCallback.

glfwSetCharCallback(window, charCallback = new GLFWCharCallback() {

    @Override
    public void invoke(long window, int codepoint) {
        /* Do something */
    }
}

// Lambda expression (alternative)
glfwSetCharCallback(window, charCallback = GLFWCharCallback.create((window, codepoint) -> {
    /* Do something */
}));

With this callback you receive an Unicode code point for each char that is typed, you just have to convert it to a String, which is easily done.

System.out.println("Char typed: " + String.valueOf(Character.toChars(codepoint)));

// You could also do it with the String constructor
System.out.println("Char typed: " + new String(Character.toChars(codepoint)));

If you wish to also know which key modifiers were used you can use the GLFWCharModsCallback, it is like the char callback, but it will also receive the modifier keys.

glfwSetCharModsCallback(window, charModsCallback = new GLFWCharModsCallback() {

    @Override
    public void invoke(long window, int codepoint, int mods) {
        /* Do something */
    }
}

// Lambda expression (alternative)
glfwSetCharModsCallback(window, charModsCallback = GLFWCharModsCallback.create((window, codepoint, mods) -> {
    /* Do something */
}));

Clipboard input

You could even make use of the clipboard with GLFW, this is simply done by calling glfwGetClipboardString(window) and setting the clipboard is as easy as getting the clipboard.

String clipboard = glfwGetClipboardString(window);
glfwSetClipboardString(window, "Some new String");

Mouse input

Cursor modes

Like with the keyboard we have also two ways to handle mouse input. But first let's take a look at the different cursor modes.

We can set three different cursor modes via glfwSetInputMode(window, GLFW_CURSOR, value), those are the following:

  • GLFW_CURSOR_NORMAL is the default mode
  • GLFW_CURSOR_HIDDEN makes the cursor invisible if it is over the window
  • GLFW_CURSOR_DISABLED will capture the cursor to the window and hides it, useful if you want to make a mouse motion camera control

In addition to the cursor modes we could also make use of sticky buttons, they will work like the sticky keys that are described in the keyboard input part.

glfwSetInputMode(window, GLFW_STICKY_MOUSE_BUTTONS, GLFW_TRUE);

Button input

Polling the mouse button input is almost the same as polling keyboard input, you just have to call glfwGetMouseButton(window, button), where button is something like GLFW_MOUSE_BUTTON_<num>, it goes from GLFW_MOUSE_BUTTON_1 to GLFW_MOUSE_BUTTON_8 or GLFW_MOUSE_BUTTON_LAST, those both are the same, but there are also the buttons for left, middle and right.
As with keyboard input you get the state of the button which is GLFW_PRESS or GLFW_RELEASE.

int state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT);
if (state == GLFW_PRESS) {
    showContextMenu();
}

You already know that you could miss a button press when polling input, but for mpuse input we could also use callbacks!
If we look at the mouse button callback we will see that it is almost similar to the key callback again, the biggest difference is, that the mouse button callback doesn't have a scancode parameter and the action is one of GLFW_PRESS or GLFW_RELEASE, so no repeated button press. The rest of the callback is similar to the key callback.

glfwSetMouseButtonCallback(window, mouseButtonCallback = new GLFWMouseButtonCallback() {

    @Override
    public void invoke(long window, int button, int action, int mods) {
        /* Do something */
    }
}

// Lambda expression (alternative)
glfwSetMouseButtonCallback(window, mouseButtonCallback = GLFWMouseButtonCallback.create((window, button, action, mods) -> {
    /* Do something */
}));

Cursor position

Of course you may also want to know the cursor position if you are creating a RTS game for example. For this we can use polling and a callback.

Polling the cursor position is done by calling glfwGetCursorPos(window, xpos, ypos), that function returns nothing, this is because it will set xpos and ypos to the cursors screen coordinates. Normally you would put two references in there, but with LWJGL we will use two Buffer instead. For this you can choose between a ByteBuffer and a DoubleBuffer.
The value you will get from xpos is between 0 and framebufferWidth, ypos is between 0 and framebufferHeight with its origin at the top left corner.

DoubleBuffer xpos = stack.mallocDouble(1);
DoubleBuffer ypos = stack.mallocDouble(1);
glfwGetCursorPos(window, xpos, ypos);

System.out.println("CursorPos: " + xpos.get() + "," + ypos.get());

If you don't like creating a Buffer for getting the position you could use a callback again. For this you just have to create a GLFWCursorPosCallback and assign it to your window.

glfwSetCursorPosCallback(window, cursorPosCallback = new GLFWCursorPosCallback() {

    @Override
    public void invoke(long window, double xpos, double ypos) {
        /* Do something */
    }
}

// Lambda expression (alternative)
glfwSetCursorPosCallback(window, cursorPosCallback = GLFWCursorPosCallback.create((window, xpos, ypos) -> {
    /* Do something */
}));

Cursor enter events

Sometime you want to be notified when the cursor enters or leaves the window, with LWJGL and GLFW this can get done with the cursor enter callback. You know already how to create a callback, so here it comes.

glfwSetCursorEnterCallback(window, cursorEnterCallback = new GLFWCursorEnterCallback() {

    @Override
    public void invoke(long window, int entered) {
        if (entered == GLFW_TRUE) {
            /* Mouse entered */
        } else {
            /* Mouse left */
        }
    }
}

// Lambda expression (alternative)
glfwSetCursorEnterCallback(window, cursorEnterCallback = GLFWCursorEnterCallback.create((window, entered) -> {
    if (entered == GLFW_TRUE) {
        /* Mouse entered */
    } else {
        /* Mouse left */
    }
}));

As you can see the entered value is an int instead of a boolean. But it can have only two values, GLFW_TRUE if the cursor entered the window or GLFW_FALSE if the cursor left the window.

Scroll input

Getting the scroll input is done by a scroll callback, which will notify you if the mouse scroll gets changed, as with every callback before we need to have a strong reference to it, so it won't get garbage collected!

glfwSetScrollCallback(window, scrollCallback = new GLFWScrollCallback() {

    @Override
    public void invoke(long window, double xoffset, double yoffset) {
        /* Do something */
    }
}

// Lambda expression (alternative)
glfwSetScrollCallback(window, scrollCallback = GLFWScrollCallback.create((window, xoffset, yoffset) -> {
    /* Do something */
}));

Both offsets behave the same, for example if you move your scroll up you will get a value > 0 for yoffset, moving it down will give you a negative value.
Note that this might vary for any other mouse.

Path dropping

Another interesting thing is the path drop callback. It will get invoked when one or more dragged files are dropped on the window.

glfwSetDropCallback(window, dropCallback = new GLFWDropCallback() {

    @Override
    public void invoke(long window, int count, long names) {
        /* Do something */
    }
}

// Lambda expression (alternative)
glfwSetDropCallback(window, dropCallback = GLFWDropCallback.create((window, count, names) -> {
    /* Do something */
}));

The first two values are pretty self-explanatory, window should be clear and the count value tells you how many paths you dropped into the window. Now the names value is a pointer to the array of UTF-8 encoded path names of the dropped files, if you want to get the path names you have use the GLFWDropCallback class.

String[] pathNames = GLFWDropCallback.getNames(count, names);
for (int i = 0; i < count; i ++) {
    System.out.println(pathNames[i]);
}

You should use this method only inside a GLFWDropCallback invocation.

Cursor objects

Some applications have their own cursors, with LWGJL and GLFW you could do the same, for this you can create some standard cursors or a custom cursor from an image file.

Creating a standard cursor is easilty done by calling glfwCreateStandardCursor(shape) where shape can be one of the following values:

  • GLFW_ARROW_CURSOR: The regular arrow cursor. This is similar to the default cursor.
  • GLFW_IBEAM_CURSOR: The text input I-beam cursor shape.
  • GLFW_CROSSHAIR_CURSOR: The crosshair shape.
  • GLFW_HAND_CURSOR: The hand shape.
  • GLFW_HRESIZE_CURSOR: The horizontal resize arrow shape.
  • GLFW_VRESIZE_CURSOR: The vertical resize arrow shape.

After creating the cursor you just have to set it, you could even have different cursor shapes for every window if you want to.

long cursor = glfwCreateStandardCursor(GLFW_CROSSHAIR_CURSOR);
glfwSetCursor(window, cursor);

But you may created a custom cursor you want to use, for this we could just load that image with ImageIO and convert it to a ByteBuffer to create a GLFW cursor out of it, we did this already when creating a texture.

InputStream in = new FileInputStream("cursor.png");
BufferedImage image = ImageIO.read(in);

int width = image.getWidth();
int height = image.getHeight();

int[] pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);

ByteBuffer buffer = stack.malloc(width * height * 4);
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        int pixel = pixels[y * imageWidth + x];
        /* Red component */
        buffer.put((byte) ((pixel >> 16) & 0xFF));
        /* Green component */
        buffer.put((byte) ((pixel >> 8) & 0xFF));
        /* Blue component */
        buffer.put((byte) (pixel & 0xFF));
        /* Alpha component */
        buffer.put((byte) ((pixel >> 24) & 0xFF));
    }
}
buffer.flip();

Note that this time you have the image data in RGBA format with its origin in the top left corner. Now for using that image data we need to use the GLFWImage class to allocate memory. After that we can create the GLFW cursor.

ByteBuffer glfwImage = GLFWImage.malloc(width, height, buffer);

long cursor = glfwCreateCursor(glfwImage, xhot, yhot);
glfwSetCursor(window, cursor);

The two values xhot and yhot specify the hotspot of the cursor, that's the point of the cursor which the window refers for tracking the cursor's position.

If you want to use the default cursor again you just have to set it to NULL.

glfwSetCursor(window, NULL);

When ending your application or if you don't need the cursor anymore you should destroy it.

glfwDestroyCursor(cursor);

Controller input

Controllers don't have callbacks, so if you want to use them you have to rely on polling the controller input. In GLFW they are referred as joystick, but you can use also gamepad with that methods.

Before using a controller you should check if it is present, this can be done by calling glfwJoystickPresent(joy) where joy is a value like GLFW_JOYSTICK_<num> this goes from GLFW_JOYSTICK_1 to GLFW_JOYSTICK_LAST which is the same as GLFW_JOYSTICK_16. This method will return GLFW_TRUE if the cursor is present or GLFW_FALSE if not.

int present = glfwJoystickPresent(GLFW_JOYSTICK_1);
if (present == GLFW_TRUE) {
    System.out.println("Controller 1 is present!);
}

Button states

Now that we know that the controller is present we can get the button states by calling glfwGetJoystickButtons(joy), this will get you the state of every button on the controller, where each state is one of GLFW_PRESS or GLFW_RELEASE. With LWJGL this will return you a ByteBuffer. After that you can just check the state of every button, but the button IDs may vary for different controllers.

ByteBuffer buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_1);

int buttonID = 1;
while (buttons.hasRemaining()) {
    int state = buttons.get();
    if (state == GLFW_PRESS) {
        System.out.println("Button " + buttonID + " is pressed!");
    }
    buttonID++;
}

Axis states

Getting the axis states is done almost similar by calling glfwGetJoystickAxes(joy), that call will give you the state of every axis as a FloatBuffer with values between -1.0 and 1.0. Most of the time you won't get a value of 0.0 and get instead some small floating point number like -1.5258789E-5 which is -0.0000015258789, sometimes you won't even get a value of 1.0 when it is at full range. So when using controllers you should define a threshold and shouldn't use equality checks.

FloatBuffer axes = glfwGetJoystickAxes(GLFW_JOYSTICK_1);

int axisID = 1;
while (axes.hasRemaining()) {
    float state = axes.get();
    if (state < -0.95f || state > 0.95f) {
        System.out.println("Axis " + axisID + " is at full-range!");
    } else if (state < -0.5f || state > 0.5f) {
        System.out.println("Axis " + axisID + " is at mid-range!");
    }
    axisID++;
}

Controller name

If you want to get the name of the controller you just have to call glfwGetJoystickName(joy).

String name = glfwGetJoystickName(GLFW_JOYSTICK_1);
System.out.println("Controller name is " + name + "!");

Next Steps

There is just one last step for the tutorials, in the next part we will look into game logic.


Source

References