-
Notifications
You must be signed in to change notification settings - Fork 65
Input
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.
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
.
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!");
}
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 */
}));
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");
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);
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 */
}));
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 */
}));
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.
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.
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.
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);
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!);
}
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++;
}
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++;
}
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 + "!");
There is just one last step for the tutorials, in the next part we will look into game logic.
This tutorial and it's source code is licensed under the MIT license.
Written by Heiko Brumme, Copyright © 2014-2018