W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
編寫:heray1990 - 原文:http://developer.android.com/training/game-controllers/controller-input.html
在系統(tǒng)層面上,Android 會以 Android 按鍵碼值和坐標值的形式來報告來自游戲控制器的輸入事件。在我們的游戲應用里,我們可以接收這些碼值和坐標值,并將它們轉化成特定的游戲行為。
當玩家將一個游戲控制器通過有線連接或者無線配對到 Android 設備時,系統(tǒng)會自動檢測控制器,將它設置成輸入設備并且開始報告它的輸入事件。我們的游戲應用可以通過在活動的 Activity 或者被選中的 View 里調用下面這些回調方法,來接收上述輸入事件(要么在 Activity,要么在 View 中實現(xiàn)實現(xiàn)這些回調方法,不要兩個地方都實現(xiàn)回調)。
建議的方法是從與用戶交互的 View 對象捕獲事件。請查看下面回調函數(shù)的對象,來獲取關于接收到輸入事件的類型:
KeyEvent:描述方向按鍵(D-pad)和游戲按鍵事件的對象。按鍵事件伴隨著一個表示特定按鍵觸發(fā)的按鍵碼值(key code),如 DPAD_DOWN 或者 BUTTON_A。我們可以通過調用 getKeyCode() 或者從按鍵事件回調方法(如 onKeyDown())來獲得按鍵碼值。
MotionEvent:描述搖桿和肩鍵運動的輸入。動作事件伴隨著一個動作碼(action code)和一系列坐標值(axis values)。動作碼表示出現(xiàn)變化的狀態(tài),例如搖動一個搖桿。坐標值描述了特定物理操控的位置和其它運動屬性,例如 AXIS_X 或者 AXIS_RTRIGGER。我們可以通過調用 getAction() 來獲得動作碼,通過調用 getAxisValue() 來獲得坐標值。
這節(jié)課主要介紹如何通過實現(xiàn)上述的 View 回調方法與處理 KeyEvent 和 MotionEvent 對象,來處理常用控制器(游戲鍵盤按鍵、方向按鍵和搖桿)的輸入。
<a name="input=>
在報告輸入事件的時候,Android 不會區(qū)分游戲控制器事件與非游戲控制器事件。例如,一個觸屏動作會產(chǎn)生一個表示觸摸表面上 X 坐標的 AXIS_X,但是一個搖桿動作產(chǎn)生的 AXIS_X 則表示搖桿水平移動的位置。如果我們的游戲關注游戲控制器的輸入,那么我們應該首先檢測相應的事件來源類型。
通過調用 getSources() 來獲得設備上支持的輸入類型的位字段,來判斷一個已連接的輸入設備是不是一個游戲控制器。我們可以通過測試以查看下面的字段是否被設置:
下面的一小段代碼介紹了一個 helper 方法,它的作用是讓我們檢驗已接入的輸入設備是否是游戲控制器。如果檢測到是游戲控制器,那么這個方法會獲得游戲控制器的設備 ID。然后,我們應該將每個設備 ID 與游戲中的玩家關聯(lián)起來,并且單獨處理每個已接入的玩家的游戲操作。想更詳細地了解關于在一臺Android設備中同時支持多個游戲控制器的方法,請見支持多個游戲控制器。
public ArrayList getGameControllerIds() {
ArrayList gameControllerDeviceIds = new ArrayList();
int[] deviceIds = InputDevice.getDeviceIds();
for (int deviceId : deviceIds) {
InputDevice dev = InputDevice.getDevice(deviceId);
int sources = dev.getSources();
// Verify that the device has gamepad buttons, control sticks, or both.
if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
|| ((sources & InputDevice.SOURCE_JOYSTICK)
== InputDevice.SOURCE_JOYSTICK)) {
// This device is a game controller. Store its device ID.
if (!gameControllerDeviceIds.contains(deviceId)) {
gameControllerDeviceIds.add(deviceId);
}
}
}
return gameControllerDeviceIds;
}
另外,我們可能想去檢查已接入的單個游戲控制器的輸入性能。這種檢查在某些場合會很有用,例如,我們希望游戲只用到兼容的物理操控。
用下面這些方法檢測一個游戲控制器是否支持一個特定的按鍵碼或者坐標碼:
Figure 1介紹了 Android 如何將按鍵碼和坐標值映射到實際的游戲手柄上。
Figure 1. 一個常用的游戲手柄的外形
上圖的標注對應下面的內(nèi)容:
游戲手柄產(chǎn)生的通用的按鍵碼包括 BUTTON_A、BUTTON_B、BUTTON_SELECT 和 BUTTON_START。當按下 D-pad 中間的交叉按鍵時,一些游戲控制器會觸發(fā) DPAD_CENTER 按鍵碼。我們的游戲可以通過調用 getKeyCode() 或者從按鍵事件回調(如onKeyDown())得到按鍵碼。如果一個事件與我們的游戲相關,那么將其處理成一個游戲動作。Table 1列出供大多數(shù)通用游戲手柄按鈕使用的推薦游戲動作。
Table 1. 供游戲手柄使用的推薦游戲動作
游戲動作 | 按鍵碼 |
在主菜單中啟動游戲,或者在游戲過程中暫停/取消暫停 | BUTTON_START |
顯示菜單 | BUTTON_SELECT 和 KEYCODE_MENU |
跟Android導航設計指導中的Back導航行為一樣 | KEYCODE_BACK |
返回到菜單中上一項 | BUTTON_B |
確認選擇,或者執(zhí)行主要的游戲動作 | BUTTON_A 和 DPAD_CENTER |
* 我們的游戲不應該依賴于Start、Select或者Menu按鍵的存在。
Tip: 可以考慮在游戲中提供一個配置界面,使得用戶可以個性化游戲控制器與游戲動作的映射。
下面的代碼介紹了如何重寫 onKeyDown() 來將 BUTTON_A 和 DPAD_CENTER 按鈕結合到一個游戲動作。
public class GameView extends View {
...
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
boolean handled = false;
if ((event.getSource() & InputDevice.SOURCE_GAMEPAD)
== InputDevice.SOURCE_GAMEPAD) {
if (event.getRepeatCount() == 0) {
switch (keyCode) {
// Handle gamepad and D-pad button presses to
// navigate the ship
...
default:
if (isFireKey(keyCode)) {
// Update the ship object to fire lasers
...
handled = true;
}
break;
}
}
if (handled) {
return true;
}
}
return super.onKeyDown(keyCode, event);
}
private static boolean isFireKey(int keyCode) {
// Here we treat Button_A and DPAD_CENTER as the primary action
// keys for the game.
return keyCode == KeyEvent.KEYCODE_DPAD_CENTER
|| keyCode == KeyEvent.KEYCODE_BUTTON_A;
}
}
Note: 在 Android 4.2(API level 17)和更低版本的系統(tǒng)中,系統(tǒng)默認會把 BUTTON_A 當作 Android Back(返回)鍵。如果我們的應用支持這些 Android 版本,請確保將 BUTTON_A 轉換成主要的游戲動作。引用 Build.VERSION.SDK_INT 值來決定設備上當前的 Android SDK 版本。
四方向的方向鍵(D-pad)在很多游戲控制器中是一種很常見的物理控制。Android 將 D-pad 的上和下按鍵按壓報告成 AXIS_HAT_Y 事件(范圍從-1.0(上)到1.0(下)),將 D-pad 的左或者右按鍵按壓報告成 AXIS_HAT_X 事件(范圍從-1.0(左)到1.0(右))。
一些游戲控制器會將 D-pad 按壓報告成一個按鍵碼。如果我們的游戲有檢測 D-pad 的按壓,那么我們應該將坐標值事件和 D-pad 按鍵碼當成一樣的輸入事件,如 table 2 介紹的一樣。
Table 2. D-pad 按鍵碼和坐標值的推薦默認游戲動作。
游戲動作 | D-pad 按鍵碼 | 坐標值 |
向上 | KEYCODE_DPAD_UP | AXIS_HAT_Y (從 0 到 -1.0) |
向下 | KEYCODE_DPAD_DOWN | AXIS_HAT_Y (從 0 到 1.0) |
向左 | KEYCODE_DPAD_LEFT | AXIS_HAT_X (從 0 到 -1.0) |
向右 | KEYCODE_DPAD_RIGHT | AXIS_HAT_X (從 0 到 1.0) |
下面的代碼介紹了通過一個 helper 類,來檢查從一個輸入事件來決定 D-pad 方向的坐標值和按鍵碼。
public class Dpad {
final static int UP = 0;
final static int LEFT = 1;
final static int RIGHT = 2;
final static int DOWN = 3;
final static int CENTER = 4;
int directionPressed = -1; // initialized to -1
public int getDirectionPressed(InputEvent event) {
if (!isDpadDevice(event)) {
return -1;
}
// If the input event is a MotionEvent, check its hat axis values.
if (event instanceof MotionEvent) {
// Use the hat axis value to find the D-pad direction
MotionEvent motionEvent = (MotionEvent) event;
float xaxis = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X);
float yaxis = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y);
// Check if the AXIS_HAT_X value is -1 or 1, and set the D-pad
// LEFT and RIGHT direction accordingly.
if (Float.compare(xaxis, -1.0f) == 0) {
directionPressed = Dpad.LEFT;
} else if (Float.compare(xaxis, 1.0f) == 0) {
directionPressed = Dpad.RIGHT;
}
// Check if the AXIS_HAT_Y value is -1 or 1, and set the D-pad
// UP and DOWN direction accordingly.
else if (Float.compare(yaxis, -1.0f) == 0) {
directionPressed = Dpad.UP;
} else if (Float.compare(yaxis, 1.0f) == 0) {
directionPressed = Dpad.DOWN;
}
}
// If the input event is a KeyEvent, check its key code.
else if (event instanceof KeyEvent) {
// Use the key code to find the D-pad direction.
KeyEvent keyEvent = (KeyEvent) event;
if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
directionPressed = Dpad.LEFT;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) {
directionPressed = Dpad.RIGHT;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) {
directionPressed = Dpad.UP;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) {
directionPressed = Dpad.DOWN;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) {
directionPressed = Dpad.CENTER;
}
}
return directionPressed;
}
public static boolean isDpadDevice(InputEvent event) {
// Check that input comes from a device with directional pads.
if ((event.getSource() & InputDevice.SOURCE_DPAD)
!= InputDevice.SOURCE_DPAD) {
return true;
} else {
return false;
}
}
}
我們可以在任意想要處理 D-pad 輸入(例如,在 onGenericMotionEvent() 或者 onKeyDown() 回調函數(shù))的地方使用這個 helper 類。
例如:
Dpad mDpad = new Dpad();
...
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
// Check if this event if from a D-pad and process accordingly.
if (Dpad.isDpadDevice(event)) {
int press = mDpad.getDirectionPressed(event);
switch (press) {
case LEFT:
// Do something for LEFT direction press
...
return true;
case RIGHT:
// Do something for RIGHT direction press
...
return true;
case UP:
// Do something for UP direction press
...
return true;
...
}
}
// Check if this event is from a joystick movement and process accordingly.
...
}
當玩家移動游戲控制器上的搖桿時,Android 會報告一個包含 ACTION_MOVE 動作碼和更新?lián)u桿在坐標軸的位置的 MotionEvent。我們的游戲可以使用 MotionEvent 提供的數(shù)據(jù)來確定是否發(fā)生搖桿的動作。
注意到搖桿移動會在單個對象中批處理多個移動示例。MotionEvent 對象包含每個搖桿坐標當前的位置和每個坐標軸上的多個歷史位置。當用 ACTION_MOVE 動作碼(例如搖桿移動)來報告移動事件時,Android 會高效地批處理坐標值。由坐標值組成的不同的歷史值比當前的坐標值要舊,比之前報告的任意移動事件要新。詳情見 MotionEvent 參考文檔。
我們可以使用歷史信息,根據(jù)搖桿輸入更精確地表達游戲對象的活動。調用 getAxisValue() 或者 getHistoricalAxisValue() 來獲取現(xiàn)在和歷史的值。我們也可以通過調用 getHistorySize() 來找到搖桿事件的歷史點號碼。
下面的代碼介紹了如何重寫 onGenericMotionEvent() 回調函數(shù)來處理搖桿輸入。我們應該首先處理歷史坐標值,然后處理當前值。
public class GameView extends View {
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
// Check that the event came from a game controller
if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) ==
InputDevice.SOURCE_JOYSTICK &&
event.getAction() == MotionEvent.ACTION_MOVE) {
// Process all historical movement samples in the batch
final int historySize = event.getHistorySize();
// Process the movements starting from the
// earliest historical position in the batch
for (int i = 0; i < historySize; i++) {
// Process the event at historical position i
processJoystickInput(event, i);
}
// Process the current movement sample in the batch (position -1)
processJoystickInput(event, -1);
return true;
}
return super.onGenericMotionEvent(event);
}
}
在使用搖桿輸入之前,我們需要確定搖桿是否居中,然后計算相應的坐標移動距離。一般搖桿會有一個平面區(qū),即在坐標 (0, 0) 附近一個值范圍內(nèi)的坐標點都被當作是中點。如果 Android 系統(tǒng)報告坐標值掉落在平面區(qū)內(nèi),那么我們應該認為控制器處于靜止(即沿著 x、y 兩個坐標軸都是靜止的)。
下面的代碼介紹了一個用于計算沿著每個坐標軸的移動距離的 helper 方法。我們將在后面討論的 processJoystickInput()
方法中調用這個 helper 方法。
private static float getCenteredAxis(MotionEvent event,
InputDevice device, int axis, int historyPos) {
final InputDevice.MotionRange range =
device.getMotionRange(axis, event.getSource());
// A joystick at rest does not always report an absolute position of
// (0,0). Use the getFlat() method to determine the range of values
// bounding the joystick axis center.
if (range != null) {
final float flat = range.getFlat();
final float value =
historyPos < 0 ? event.getAxisValue(axis):
event.getHistoricalAxisValue(axis, historyPos);
// Ignore axis values that are within the 'flat' region of the
// joystick axis center.
if (Math.abs(value) > flat) {
return value;
}
}
return 0;
}
將它們都放在一起,下面是我們?nèi)绾卧谟螒蛑刑幚頁u桿移動:
private void processJoystickInput(MotionEvent event,
int historyPos) {
InputDevice mInputDevice = event.getDevice();
// Calculate the horizontal distance to move by
// using the input value from one of these physical controls:
// the left control stick, hat axis, or the right control stick.
float x = getCenteredAxis(event, mInputDevice,
MotionEvent.AXIS_X, historyPos);
if (x == 0) {
x = getCenteredAxis(event, mInputDevice,
MotionEvent.AXIS_HAT_X, historyPos);
}
if (x == 0) {
x = getCenteredAxis(event, mInputDevice,
MotionEvent.AXIS_Z, historyPos);
}
// Calculate the vertical distance to move by
// using the input value from one of these physical controls:
// the left control stick, hat switch, or the right control stick.
float y = getCenteredAxis(event, mInputDevice,
MotionEvent.AXIS_Y, historyPos);
if (y == 0) {
y = getCenteredAxis(event, mInputDevice,
MotionEvent.AXIS_HAT_Y, historyPos);
}
if (y == 0) {
y = getCenteredAxis(event, mInputDevice,
MotionEvent.AXIS_RZ, historyPos);
}
// Update the ship object based on the new x and y values
}
為了支持除了單個搖桿之外更多復雜的功能,按照下面的做法:
處理兩個控制器搖桿。很多游戲控制器左右兩邊都有搖桿。對于左搖桿,Android 會報告水平方向的移動為 AXIS_X 事件,垂直方向的移動為 AXIS_Y 事件。對于右搖桿,Android 會報告水平方向的移動為 AXIS_Z 事件,垂直方向的移動為 AXIS_RZ 事件。確保在代碼中處理兩個搖桿。
處理肩鍵按壓(但需要提供另一種輸入方法)。一些控制器會有左右肩鍵。如果存在這些按鍵,那么 Android 報告左肩鍵按壓為一個 AXIS_LTRIGGER 事件,右肩鍵按壓為一個 AXIS_RTRIGGER 事件。在 Android 4.3(API level 18)中,一個產(chǎn)生了 AXIS_LTRIGGER 事件的控制器也會報告一個完全一樣的 AXIS_BRAKE 坐標值。同樣,AXIS_RTRIGGER 對應 AXIS_GAS。Android 會報告模擬按鍵按壓為從 0.0(釋放)到 1.0(按下)的標準值。并不是所有的控制器都有肩鍵,所以需要允許玩家用其它按鈕來執(zhí)行那些游戲動作。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: