編寫:heray1990 - 原文:http://developer.android.com/training/game-controllers/controller-input.html
在系統(tǒng)層面上,Android 會(huì)以 Android 按鍵碼值和坐標(biāo)值的形式來報(bào)告來自游戲控制器的輸入事件。在我們的游戲應(yīng)用里,我們可以接收這些碼值和坐標(biāo)值,并將它們轉(zhuǎn)化成特定的游戲行為。
當(dāng)玩家將一個(gè)游戲控制器通過有線連接或者無線配對(duì)到 Android 設(shè)備時(shí),系統(tǒng)會(huì)自動(dòng)檢測(cè)控制器,將它設(shè)置成輸入設(shè)備并且開始報(bào)告它的輸入事件。我們的游戲應(yīng)用可以通過在活動(dòng)的 Activity 或者被選中的 View 里調(diào)用下面這些回調(diào)方法,來接收上述輸入事件(要么在 Activity,要么在 View 中實(shí)現(xiàn)實(shí)現(xiàn)這些回調(diào)方法,不要兩個(gè)地方都實(shí)現(xiàn)回調(diào))。
建議的方法是從與用戶交互的 View 對(duì)象捕獲事件。請(qǐng)查看下面回調(diào)函數(shù)的對(duì)象,來獲取關(guān)于接收到輸入事件的類型:
KeyEvent:描述方向按鍵(D-pad)和游戲按鍵事件的對(duì)象。按鍵事件伴隨著一個(gè)表示特定按鍵觸發(fā)的按鍵碼值(key code),如 DPAD_DOWN 或者 BUTTON_A。我們可以通過調(diào)用 getKeyCode() 或者從按鍵事件回調(diào)方法(如 onKeyDown())來獲得按鍵碼值。
MotionEvent:描述搖桿和肩鍵運(yùn)動(dòng)的輸入。動(dòng)作事件伴隨著一個(gè)動(dòng)作碼(action code)和一系列坐標(biāo)值(axis values)。動(dòng)作碼表示出現(xiàn)變化的狀態(tài),例如搖動(dòng)一個(gè)搖桿。坐標(biāo)值描述了特定物理操控的位置和其它運(yùn)動(dòng)屬性,例如 AXIS_X 或者 AXIS_RTRIGGER。我們可以通過調(diào)用 getAction() 來獲得動(dòng)作碼,通過調(diào)用 getAxisValue() 來獲得坐標(biāo)值。
這節(jié)課主要介紹如何通過實(shí)現(xiàn)上述的 View 回調(diào)方法與處理 KeyEvent 和 MotionEvent 對(duì)象,來處理常用控制器(游戲鍵盤按鍵、方向按鍵和搖桿)的輸入。
<a name="input=>
在報(bào)告輸入事件的時(shí)候,Android 不會(huì)區(qū)分游戲控制器事件與非游戲控制器事件。例如,一個(gè)觸屏動(dòng)作會(huì)產(chǎn)生一個(gè)表示觸摸表面上 X 坐標(biāo)的 AXIS_X,但是一個(gè)搖桿動(dòng)作產(chǎn)生的 AXIS_X 則表示搖桿水平移動(dòng)的位置。如果我們的游戲關(guān)注游戲控制器的輸入,那么我們應(yīng)該首先檢測(cè)相應(yīng)的事件來源類型。
通過調(diào)用 getSources() 來獲得設(shè)備上支持的輸入類型的位字段,來判斷一個(gè)已連接的輸入設(shè)備是不是一個(gè)游戲控制器。我們可以通過測(cè)試以查看下面的字段是否被設(shè)置:
下面的一小段代碼介紹了一個(gè) helper 方法,它的作用是讓我們檢驗(yàn)已接入的輸入設(shè)備是否是游戲控制器。如果檢測(cè)到是游戲控制器,那么這個(gè)方法會(huì)獲得游戲控制器的設(shè)備 ID。然后,我們應(yīng)該將每個(gè)設(shè)備 ID 與游戲中的玩家關(guān)聯(lián)起來,并且單獨(dú)處理每個(gè)已接入的玩家的游戲操作。想更詳細(xì)地了解關(guān)于在一臺(tái)Android設(shè)備中同時(shí)支持多個(gè)游戲控制器的方法,請(qǐng)見支持多個(gè)游戲控制器。
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;
}
另外,我們可能想去檢查已接入的單個(gè)游戲控制器的輸入性能。這種檢查在某些場(chǎng)合會(huì)很有用,例如,我們希望游戲只用到兼容的物理操控。
用下面這些方法檢測(cè)一個(gè)游戲控制器是否支持一個(gè)特定的按鍵碼或者坐標(biāo)碼:
Figure 1介紹了 Android 如何將按鍵碼和坐標(biāo)值映射到實(shí)際的游戲手柄上。
http://wiki.jikexueyuan.com/project/android-training-geek/images/game-controller-profiles.png" alt="game-controller-profiles" title="Figure 1. Profile for a generic game controller." />
Figure 1. 一個(gè)常用的游戲手柄的外形
上圖的標(biāo)注對(duì)應(yīng)下面的內(nèi)容:
游戲手柄產(chǎn)生的通用的按鍵碼包括 BUTTON_A、BUTTON_B、BUTTON_SELECT 和 BUTTON_START。當(dāng)按下 D-pad 中間的交叉按鍵時(shí),一些游戲控制器會(huì)觸發(fā) DPAD_CENTER 按鍵碼。我們的游戲可以通過調(diào)用 getKeyCode() 或者從按鍵事件回調(diào)(如onKeyDown())得到按鍵碼。如果一個(gè)事件與我們的游戲相關(guān),那么將其處理成一個(gè)游戲動(dòng)作。Table 1列出供大多數(shù)通用游戲手柄按鈕使用的推薦游戲動(dòng)作。
Table 1. 供游戲手柄使用的推薦游戲動(dòng)作
游戲動(dòng)作 | 按鍵碼 |
在主菜單中啟動(dòng)游戲,或者在游戲過程中暫停/取消暫停 | BUTTON_START |
顯示菜單 | BUTTON_SELECT 和 KEYCODE_MENU |
跟Android導(dǎo)航設(shè)計(jì)指導(dǎo)中的Back導(dǎo)航行為一樣 | KEYCODE_BACK |
返回到菜單中上一項(xiàng) | BUTTON_B |
確認(rèn)選擇,或者執(zhí)行主要的游戲動(dòng)作 | BUTTON_A 和 DPAD_CENTER |
* 我們的游戲不應(yīng)該依賴于Start、Select或者M(jìn)enu按鍵的存在。
Tip: 可以考慮在游戲中提供一個(gè)配置界面,使得用戶可以個(gè)性化游戲控制器與游戲動(dòng)作的映射。
下面的代碼介紹了如何重寫 onKeyDown() 來將 BUTTON_A 和 DPAD_CENTER 按鈕結(jié)合到一個(gè)游戲動(dòng)作。
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)默認(rèn)會(huì)把 BUTTON_A 當(dāng)作 Android Back(返回)鍵。如果我們的應(yīng)用支持這些 Android 版本,請(qǐng)確保將 BUTTON_A 轉(zhuǎn)換成主要的游戲動(dòng)作。引用 Build.VERSION.SDK_INT 值來決定設(shè)備上當(dāng)前的 Android SDK 版本。
四方向的方向鍵(D-pad)在很多游戲控制器中是一種很常見的物理控制。Android 將 D-pad 的上和下按鍵按壓報(bào)告成 AXIS_HAT_Y 事件(范圍從-1.0(上)到1.0(下)),將 D-pad 的左或者右按鍵按壓報(bào)告成 AXIS_HAT_X 事件(范圍從-1.0(左)到1.0(右))。
一些游戲控制器會(huì)將 D-pad 按壓報(bào)告成一個(gè)按鍵碼。如果我們的游戲有檢測(cè) D-pad 的按壓,那么我們應(yīng)該將坐標(biāo)值事件和 D-pad 按鍵碼當(dāng)成一樣的輸入事件,如 table 2 介紹的一樣。
Table 2. D-pad 按鍵碼和坐標(biāo)值的推薦默認(rèn)游戲動(dòng)作。
游戲動(dòng)作 | D-pad 按鍵碼 | 坐標(biāo)值 |
向上 | 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) |
下面的代碼介紹了通過一個(gè) helper 類,來檢查從一個(gè)輸入事件來決定 D-pad 方向的坐標(biāo)值和按鍵碼。
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() 回調(diào)函數(shù))的地方使用這個(gè) 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.
...
}
當(dāng)玩家移動(dòng)游戲控制器上的搖桿時(shí),Android 會(huì)報(bào)告一個(gè)包含 ACTION_MOVE 動(dòng)作碼和更新?lián)u桿在坐標(biāo)軸的位置的 MotionEvent。我們的游戲可以使用 MotionEvent 提供的數(shù)據(jù)來確定是否發(fā)生搖桿的動(dòng)作。
注意到搖桿移動(dòng)會(huì)在單個(gè)對(duì)象中批處理多個(gè)移動(dòng)示例。MotionEvent 對(duì)象包含每個(gè)搖桿坐標(biāo)當(dāng)前的位置和每個(gè)坐標(biāo)軸上的多個(gè)歷史位置。當(dāng)用 ACTION_MOVE 動(dòng)作碼(例如搖桿移動(dòng))來報(bào)告移動(dòng)事件時(shí),Android 會(huì)高效地批處理坐標(biāo)值。由坐標(biāo)值組成的不同的歷史值比當(dāng)前的坐標(biāo)值要舊,比之前報(bào)告的任意移動(dòng)事件要新。詳情見 MotionEvent 參考文檔。
我們可以使用歷史信息,根據(jù)搖桿輸入更精確地表達(dá)游戲?qū)ο蟮幕顒?dòng)。調(diào)用 getAxisValue() 或者 getHistoricalAxisValue() 來獲取現(xiàn)在和歷史的值。我們也可以通過調(diào)用 getHistorySize() 來找到搖桿事件的歷史點(diǎn)號(hào)碼。
下面的代碼介紹了如何重寫 onGenericMotionEvent() 回調(diào)函數(shù)來處理搖桿輸入。我們應(yīng)該首先處理歷史坐標(biāo)值,然后處理當(dāng)前值。
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);
}
}
在使用搖桿輸入之前,我們需要確定搖桿是否居中,然后計(jì)算相應(yīng)的坐標(biāo)移動(dòng)距離。一般搖桿會(huì)有一個(gè)平面區(qū),即在坐標(biāo) (0, 0) 附近一個(gè)值范圍內(nèi)的坐標(biāo)點(diǎn)都被當(dāng)作是中點(diǎn)。如果 Android 系統(tǒng)報(bào)告坐標(biāo)值掉落在平面區(qū)內(nèi),那么我們應(yīng)該認(rèn)為控制器處于靜止(即沿著 x、y 兩個(gè)坐標(biāo)軸都是靜止的)。
下面的代碼介紹了一個(gè)用于計(jì)算沿著每個(gè)坐標(biāo)軸的移動(dòng)距離的 helper 方法。我們將在后面討論的 processJoystickInput()
方法中調(diào)用這個(gè) 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桿移動(dòng):
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
}
為了支持除了單個(gè)搖桿之外更多復(fù)雜的功能,按照下面的做法:
處理兩個(gè)控制器搖桿。很多游戲控制器左右兩邊都有搖桿。對(duì)于左搖桿,Android 會(huì)報(bào)告水平方向的移動(dòng)為 AXIS_X 事件,垂直方向的移動(dòng)為 AXIS_Y 事件。對(duì)于右搖桿,Android 會(huì)報(bào)告水平方向的移動(dòng)為 AXIS_Z 事件,垂直方向的移動(dòng)為 AXIS_RZ 事件。確保在代碼中處理兩個(gè)搖桿。