原文:https://github.com/SilverTiger/lwjgl3-tutorial/wiki/Input
译注:并没有逐字逐句翻译一切,只翻译了自己觉得有用的部分。另外此翻译仅供参考,请一切以原文为准。代码例子文件链接什么的都请去原链接查找。括号里的内容一般也是译注,供理解参考用。总目录传送门:http://blog.csdn.net/zoharwolf/article/details/49472857
LWJGL3里用GLFW处理输入很容易。本教程就看看怎样处理来自键盘、鼠标、手柄、摇杆的输入。
Processing events 处理事件
我们开始前你得先清楚,LWJGL3里需要调用glfwPollEvents()来获取已经接收到的事件。
另一种方法是调用glfwWaitEvents(),它会一直线程休眠等待,直到收到一个事件,但是期间也可以调用glfwPostEmptyEvent()来唤醒线程。如果你想用GLFW做一个文字冒险游戏的话,这个很好用。但是一般的实时游戏不应该这样空等待事件,所以我们还是应该在每帧轮询(poll)事件。(也就是用glfwPollEvents()更正确)
另外你可能会用glfwSetInputMode(window, mode, value),mode是GLFW_CURSOR, GLFW_STICKY_KEYS or GLFW_STICKY_MOUSE_BUTTONS其中之一,如果是GLFW_STICKY_KEYS和GLFW_STICKY_MOUSE_BUTTONS的话,那value应该是GLFW_TRUE 或 GLFW_FALSE。如果是With GLFW_CURSOR,那value可以是GLFW_CURSOR_NORMAL, GLFW_CURSOR_HIDDEN 或GLFW_CURSOR_DISABLED。
Keyboard input 键盘输入
Key input 按键输入
对于键盘输入,我们用 glfwGetKey(window, key)轮询按键状态,window就是窗口句柄,key是一个GLFW_KEY_<键名>这样的常量,比如GLFW_KEY_W 或 GLFW_KEY_SPACE,你也可以通过获取GLFW_KEY_UNKNOWN来处理一些特殊的按键比如E-mail键。通过调用glfwGetKey,你会得到键状态,看它是GLFW_PRESS还是GLFW_RELEASE。
int state = glfwGetKey(window, GLFW_KEY_UP);
if (state == GLFW_PRESS) {
moveUp();
}
但是轮询有一个问题,如果在轮询结束前按键就松开了,那么就可能会错过一个按过的键,有一个简单的解决办法,你可以用glfwSetInputMode(window, mode, value)来设置粘滞键。
如果我们设GLFW_STICKY_KEYS 为GLFW_TRUE,就算提前放开,键状态也将一直保持GLFW_PRESS直到你轮询到此键。如果你想要知道某个键是否曾被按过,这很有用。但是如果想知道按键顺序,那就没什么用了。
glfwSetInputMode(window, GLFW_STICKY_KEYS, GLFW_TRUE);
大部分情况,轮询输入已经够了,但是推荐使用回调函数。
当一个物理按键被按下、松开、重复按时,按键回调函数将被调用,我们可以简单地通过glfwSetKeyCallback(window, cbfun)来设置一个按键回调函数。
一般我们应该放一个函数指针在里面,但是JAVA并没有那种东西,不过LWJGL为此提供了一个GLFWKeyCallback 类。需要对此回调函数建一个强引用,这样它就不会被垃圾回收掉,所以得放一个private GLFWKeyCallback keyCallback在你的类中。
glfwSetKeyCallback(window, keyCallback = new GLFWKeyCallback() {
@Overridepublic void invoke(long window, int key, int scancode, int action, int mods) {
/* Do something */
}
}
如果你用的是JAVA8,你可以用匿名函数表达式,因为GLFWKeyCallback也提供了一个单独的抽象方法
glfwSetKeyCallback(window, keyCallback = GLFWKeyCallback((window, key, scancode, action, mods) -> {
/* Do something */
}));
你用哪个版本的都可以,现在来看看参数们。前两个很明确,window是窗口句柄,key是那些GLFW_KEY_<键名>。
scancode是系统指定的按键扫描码,仅在用GLFW_KEY_UNKNOWN时才有用。
键状态存储在action里,它将是GLFW_PRESS, GLFW_RELEASE or GLFW_REPEAT中的一个,mods是一个用来保存修饰键的位字段,它可能会包含GLFW_MOD_SHIFT, GLFW_MOD_CONTROL, GLFW_MOD_ALT and GLFW_MOD_SUPER
例如,你想要看是否ctrl+alt+F被按下,只需要像以下代码这样:
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 文本输入
另一个你可能想要实现的,就是比如在文字冒险游戏里的那种文本输入功能。
这个你只能用回调函数来实现了,没有方法可以轮询文本输入。这里我们使用GLFWCharCallback。
glfwSetCharCallback(window, charCallback = new GLFWCharCallback() {
@Overridepublic void invoke(long window, int codepoint) {
/* Do something */
}
}
// Lambda expression for Java 8
glfwSetCharCallback(window, charCallback = GLFWCharCallback((window, codepoint) -> {
/* Do something */
}));
用此回调函数,你可以收到一个输入的每个字符的unicode字符码,只需要把它们转换成String,这很简单。
System.out.println("Char typed: " + String.valueOf(Character.toChars(codepoint)));
// You could also do it with the String constructorSystem.out.println("Char typed: " + new String(Character.toChars(codepoint)));
如果你也想知道哪个修饰键被使用,你可以用GLFWCharModsCallback,跟字符回调函数相似,它会接收修饰键。
glfwSetCharModsCallback(window, charModsCallback = new GLFWCharModsCallback() {
@Overridepublic void invoke(long window, int codepoint, int mods) {
/* Do something */
}
}
// Lambda expression for Java 8
glfwSetCharModsCallback(window, charModsCallback = GLFWCharModsCallback((window, codepoint, mods) -> {
/* Do something */
}));
Clipboard input 剪贴板输入
GLFW里你甚至可以使用剪贴板,只需要调用 glfwGetClipboardString(window),设置剪贴板就和拿到剪贴板一样容易。
String clipboard = glfwGetClipboardString(window);
glfwSetClipboardString(window, "Some new String");
Mouse input 鼠标输入
Cursor modes 指针模式
跟键盘相似,我们也有两种方法处理鼠标输入。但那之前我们先看看不同的鼠标模式。
可以用glfwSetInputMode(window, GLFW_CURSOR, value)设置三种不同的鼠标模式。
- GLFW_CURSOR_NORMAL 默认的模式
- GLFW_CURSOR_HIDDEN 使鼠标在窗口上时不可见
- GLFW_CURSOR_DISABLED 使鼠标只能在窗口上并且隐藏,当你希望用鼠标控制摄像机镜头时,这功能很有用。
另外,指针模式也可以使用粘滞键,它跟键盘输入部分的粘滞键相似。
glfwSetInputMode(window, GLFW_STICKY_MOUSE_BUTTONS, GLFW_TRUE);
Button input 按键输入
轮询鼠标按键输入几乎和轮询键盘输入相同,调用glfwGetMouseButton(window, button),button值应是GLFW_MOUSE_BUTTON_<数字>, 范围是从GLFW_MOUSE_BUTTON_1到GLFW_MOUSE_BUTTON_8或GLFW_MOUSE_BUTTON_LAST,后面俩是一样的,另外还有左中右键。
跟键盘相似,状态有GLFW_PRESS 和GLFW_RELEASE。
int state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT);
if (state == GLFW_PRESS) {
showContextMenu();
}
你应该已经知道了,轮询时会漏掉按过的键,所以仍然要用回调函数。
看鼠标回调函数,它几乎和键盘的完全一致,最大的不同在于,它没有scancode参数,并且action只有GLFW_PRESS 和GLFW_RELEASE两种,没有重复按键,其他和键盘回调函数都一样。
glfwSetMouseButtonCallback(window, mouseButtonCallback = new GLFWMouseButtonCallback() {
@Overridepublic void invoke(long window, int button, int action, int mods) {
/* Do something */
}
}
// Lambda expression for Java 8
glfwSetMouseButtonCallback(window, mouseButtonCallback = GLFWMouseButtonCallback((window, button, action, mods) -> {
/* Do something */
}));
Cursor position 指针位置
当然,如果你做RTS游戏之类的,你可能也想知道指针的位置。这个也可以靠轮询或回调函数实现。
轮询指针位置调用glfwGetCursorPos(window, xpos, ypos),此函数什么也不会返回,因为它会将屏幕坐标值设置在xpos和ypos里。一般这里要用两个引用参数(译注:大概是指类似C++里的按引用传递参数,在JAVA里程序员并不能显示地标识这个),但是LWJGL我们用Buffer来代替。可以用ByteBuffer或DoubleBuffer,x值将在0和framebufferWidth之间,y值将在0和framebufferHeight之间,原点是屏幕左上角。
DoubleBuffer xpos = BufferUtils.createDoubleBuffer(1);
DoubleBuffer ypos = BufferUtils.createDoubleBuffer(1);
glfwGetCursorPos(window, xpos, ypos);
System.out.println("CursorPos: " + xpos.get() + "," + ypos.get());
如果你不想建个Buffer,你也可以用回调函数,只需要创建一个GLFWCursorPosCallback并将它分配给窗口。
glfwSetCursorPosCallback(window, cursorPosCallback = new GLFWCursorPosCallback() {
@Overridepublic void invoke(long window, double xpos, double ypos) {
/* Do something */
}
}
// Lambda expression for Java 8
glfwSetCursorPosCallback(window, cursorPosCallback = GLFWCursorPosCallback((window, xpos, ypos) -> {
/* Do something */
}));
Cursor enter events 指针进入事件
有时你想在指针进入或离开窗口时收到通知,在LWJGL和GLFW里,可以用指针进入回调函数实现。你知道怎样创建回调函数了,让我们开始吧。
glfwSetCursorEnterCallback(window, cursorEnterCallback = new GLFWCursorEnterCallback() {
@Overridepublic void invoke(long window, int entered) {
if (entered == GLFW_TRUE) {
/* Mouse entered */
} else {
/* Mouse left */
}
}
}
// Lambda expression for Java 8
glfwSetCursorEnterCallback(window, cursorEnterCallback = GLFWCursorEnterCallback((window, entered) -> {
if (entered == GLFW_TRUE) {
/* Mouse entered */
} else {
/* Mouse left */
}
}));
entered值不是布尔而是整型,但是它只能有两个值,进入窗口时是GLFW_TRUE ,离开窗口时是GLFW_FALSE
Scroll input 滚轮输入
通过滚轮回调函数可以获取滚轮输入,跟其他回调函数一样,要有强引用,防被垃圾回收。
glfwSetScrollCallback(window, scrollCallback = new GLFWScrollCallback() {
@Overridepublic void invoke(long window, double xoffset, double yoffset) {
/* Do something */
}
}
// Lambda expression for Java 8
glfwSetScrollCallback(window, scrollCallback = GLFWScrollCallback((window, xoffset, yoffset) -> {
/* Do something */
}));
两个offset是一样的,如果你向上滚滚轮,将得到一个大于0的yoffset,向下滚就是小于0的。
注意,这可能对不同的鼠标来说略有区别。(MACBOOK的鼠标?)
Path dropping 拖拽路径
另一个有趣的东西就是拖拽路径回调函数,当你将一或多个文件拖拽到窗口上时,它会被调用。
glfwSetDropCallback(window, dropCallback = new GLFWDropCallback() {
@Overridepublic void invoke(long window, int count, long names) {
/* Do something */
}
}
// Lambda expression for Java 8
glfwSetDropCallback(window, dropCallback = GLFWDropCallback((window, count, names) -> {
/* Do something */
}));
前俩参数不言而喻,window是句柄,count值告诉你拖进窗口的有多少路径。names值是一个指针指向一个数组,数组内是拖进来的文件UTF-8编码的路径名,如果你想要得到路径名,你要用Callbacks类。
String[] pathNames = Callbacks.dropCallbackNamesString(count, names);
for (int i = 0; i < count; i ++) {
System.out.println(pathNames[i]);
}
你仅能在GLFWDropCallback回调函数中使用此方法。
Cursor objects 指针对象
一些程序有它们自己的指针,用LWGJL和GLFW你也可以实现,你可以创建一些标准指针或者根据图片建一些自定义的指针。
创建标准指针很容易,只需要用glfwCreateStandardCursor(shape),shape可以是以下值:
- GLFW_ARROW_CURSOR: 通常的指针。跟默认指针相似。
- GLFW_IBEAM_CURSOR: 文本输入时的指针。
- GLFW_CROSSHAIR_CURSOR: 十字准星型指针。
- GLFW_HAND_CURSOR: 手型指针。
- GLFW_HRESIZE_CURSOR: 横向缩放指针。
- GLFW_VRESIZE_CURSOR: 纵向缩放指针。
创建指针后还要设置它,如果你想,可以让不同的窗口有不同的指针。
long cursor = glfwCreateStandardCursor(GLFW_CROSSHAIR_CURSOR);
glfwSetCursor(window, cursor);
但是你可能想创建一个自定义指针,为此你得用ImageIO读取图片并转换成ByteBuffer,然后用GLFW创建。我们在创建纹理的时候已经做过了
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 = BufferUtils.createByteBuffer(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();
注意这次你图片数据是RGBA格式,原点在左上角。现在为了用图片数据,我们需要用GLFWimage类分配内存。之后我们就可以创建GLFW指针了。
ByteBuffer glfwImage = GLFWimage.malloc(width, height, buffer);
long cursor = glfwCreateCursor(glfwImage, xhot, yhot);
glfwSetCursor(window, cursor);
xhot和yhot两个值指定了指针热点,窗口就是用此点来跟踪指针的坐标的。
如果你想用默认指针,只需要把它设成NULL。
glfwSetCursor(window, NULL);
程序结束时,你不再需要指针,应该销毁它。
glfwDestroyCursor(cursor);
Controller input 手柄输入
手柄没有回调函数,所以只能轮询。在GLFW里,它们被称为摇杆(joystick),但是也可以用那些方法处理手柄(gamepad)。
用手柄之前,先应该检查它有没有插上,需要调用glfwJoystickPresent(joy),joy值应该是GLFW_JOYSTICK_<数字>格式,从GLFW_JOYSTICK_1到GLFW_JOYSTICK_LAST或GLFW_JOYSTICK_16。如果插入了手柄,此方法将返回GLFW_TRUE,否则返回GLFW_FALSE。
int present = glfwJoystickPresent(GLFW_JOYSTICK_1);
if (present == GLFW_TRUE) {
System.out.println("Controller 1 is present!);}
Button states 按键状态
现在假如手柄已插入,我们可以用glfwGetJoystickButtons(joy)取按键状态,它将得到手柄上每个键的状态,GLFW_PRESS或GLFW_RELEASE。用LWJGL,它将返回给你一个ByteBuffer,你可以从中查出每个键的状态,但是对于不同的手柄,按键ID可能不同。
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 轴状态
想取到摇杆轴值,用glfwGetJoystickAxes(joy),它将得到每个轴的状态,状态值是FloatBuffer的形式,范围在-1.0和1.0之间。通常你并不会得到0,而是得到一个很小的浮点数,比如-1.5258789E-5,也就是-0.0000015258789,有时你就算按到底也得不到1.0。因此,用手柄时,你应该使用一个阙值,而不是简单的值相等判断。
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 手柄名称
如果想获取手柄名称,要用glfwGetJoystickName(joy)
String name = glfwGetJoystickName(GLFW_JOYSTICK_1);
System.out.println("Controller name is " + name + "!");
本教程的最后一课,将要讲游戏逻辑。