原文: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 + "!");
本教程的最後一課,將要講遊戲邏輯。