在系統層級,Android 會回報遊戲控制器輸入的事件代碼 例如 Android 按鍵碼和軸值您可以在遊戲中接收這些代碼 並轉換為特定的遊戲內動作
玩家透過實際連線或無線配對遊戲控制器時,
若是 Android 裝置,系統就會自動偵測到控制器
視為輸入裝置,並開始回報輸入事件。您的遊戲可獲得
方法是在活動中實作下列回呼方法,
Activity
或聚焦 View
(建議
實作 Activity
或
View
(兩者只能擇一):
- 來自「
Activity
」:dispatchGenericMotionEvent(android.view. MotionEvent)
呼叫可處理一般動作事件,例如搖桿動作。
dispatchKeyEvent(android.view.KeyEvent)
呼叫處理重要事件,例如新聞稿或發布新聞稿 遊戲手把或 D-Pad 按鈕。
- 來自「
View
」:onGenericMotionEvent(android.view.MotionEvent)
呼叫可處理一般動作事件,例如搖桿動作。
onKeyDown(int, android.view.KeyEvent)
用於處理按下實體按鍵 (如遊戲手把) 的按下按鈕 D-Pad 按鈕。
onKeyUp(int, android.view.KeyEvent)
呼叫此呼叫可處理實體按鍵,例如遊戲手把或 D-Pad 按鈕。
建議的方法是從
與使用者互動的特定 View
物件。
檢查回呼提供的下列物件以取得資訊
關於收到的輸入事件類型:
KeyEvent
- 描述方向性的物件
D-Pad 和遊戲手把按鈕事件。重要事件與
金鑰程式碼:指出觸發的特定按鈕,例如
DPAD_DOWN
或BUTTON_A
。您可以取得 呼叫getKeyCode()
或從金鑰碼取得按鍵碼 以及onKeyDown()
。 MotionEvent
- 一種物件,用於說明搖桿和肩膀觸發的輸入內容
動作。動作事件旁邊有一個動作代碼和一組
軸的值。動作程式碼會指明
例如搖桿移動軸值用來說明位置和其他
特定物理控制的移動屬性,如
AXIS_X
或AXIS_RTRIGGER
。您可以在 方法是呼叫getAction()
,並使用 正在呼叫getAxisValue()
。
本課程將說明如何處理最常見的
實體控制系統 (遊戲手把按鈕、方向鍵以及
遊戲畫面)。
View
回呼方法和處理中
KeyEvent
和 MotionEvent
物件。
確認遊戲控制器已連線
回報輸入事件時,Android 無法區分
區分非遊戲控制器裝置所引發事件與發生事件
加入遊戲控制器中舉例來說,觸控螢幕動作會產生
代表 X 的 AXIS_X
事件
觸控表面的座標,但搖桿會產生一個
代表搖桿 X 位置的 AXIS_X
事件。如果
您的遊戲非常重視處理遊戲控制器的輸入內容,請先檢查
表示輸入事件是來自相關來源類型
若要驗證已連接的輸入裝置是否為遊戲控制器,請呼叫
getSources()
來取得
裝置支援的輸入來源類型方便您測試
設定的欄位如下:
SOURCE_GAMEPAD
的來源類型表示 表示輸入裝置有遊戲手把按鈕 (例如BUTTON_A
)。請注意,這個來源 類型未嚴格指出遊戲控制器是否有 D-Pad 按鈕 但大多數遊戲板通常都有方向控制項SOURCE_DPAD
的來源類型表示 輸入裝置有 D-pad 按鈕 (例如DPAD_UP
)。- 「
SOURCE_JOYSTICK
」的來源類型 表示輸入裝置具有類比控制桿 (例如 可記錄AXIS_X
沿其他動作的搖桿 和AXIS_Y
)。
下列程式碼片段是輔助方法,可讓您檢查 所連接的輸入裝置即為遊戲控制器。如果存在,則方法會擷取 遊戲控制器的裝置 ID。隨即會將每部裝置建立關聯 在遊戲中使用玩家 ID,以及處理每個連結遊戲中的遊戲動作 分別對應。進一步瞭解如何支援多個遊戲控制器 在同一部 Android 裝置上同時連線兩個應用程式,請參閱 支援多個遊戲控制器。
Kotlin
fun getGameControllerIds(): List<Int> { val gameControllerDeviceIds = mutableListOf<Int>() val deviceIds = InputDevice.getDeviceIds() deviceIds.forEach { deviceId -> InputDevice.getDevice(deviceId).apply { // Verify that the device has gamepad buttons, control sticks, or both. if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) { // This device is a game controller. Store its device ID. gameControllerDeviceIds .takeIf { !it.contains(deviceId) } ?.add(deviceId) } } } return gameControllerDeviceIds }
Java
public ArrayList<Integer> getGameControllerIds() { ArrayList<Integer> gameControllerDeviceIds = new ArrayList<Integer>(); 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; }
此外,建議你檢查個別輸入功能 使用已連線的遊戲控制器。這項功能非常實用 您想讓遊戲只使用實體控制項組合的輸入項目 很容易理解
偵測連結是否支援特定按鍵碼或軸碼 就必須使用以下技術:
- 在 Android 4.4 (API 級別 19) 以上版本中,您可以檢查金鑰程式碼是否
主要用於連線遊戲控制器的
hasKeys(int...)
。 - 在 Android 3.1 (API 級別 12) 以上版本中,您可以找到所有可用的軸
您必須先呼叫
getMotionRanges()
。然後,在每個 已傳回InputDevice.MotionRange
個物件,呼叫getAxis()
可取得其軸 ID。
處理遊戲手把按鈕的按下動作
圖 1 顯示 Android 如何將按鍵碼和軸值對應至實際 。
![](https://developer.android.com/static/images/training/game-controller-profiles.png?authuser=9&hl=zh-tw)
圖 1. 一般遊戲控制器的設定檔。
圖中的圖說如下:
按下遊戲手把按鈕產生的常見按鍵碼包括
BUTTON_A
,
BUTTON_B
,
BUTTON_SELECT
,
和 BUTTON_START
。部分遊戲
當使用者按下 D-Pad 跨列的中心點時,控制器也會觸發 DPAD_CENTER
按鍵碼。您的
遊戲可以呼叫 getKeyCode()
來檢查按鍵程式碼
或來自重要事件回呼
onKeyDown()
,
如果代表的是與您遊戲相關的事件,則會視為
遊戲動作。表 1 列出最常用的遊戲操作
遊戲手把按鈕。
表 1. 遊戲手把的建議遊戲動作 按鈕。
遊戲動作 | 按鈕按鍵碼 |
---|---|
在主選單中啟動遊戲,或在遊戲期間暫停/取消暫停 | BUTTON_START *。 |
顯示選單 | BUTTON_SELECT *。
和KEYCODE_MENU * |
與 Android Back 瀏覽行為相同,請參閱 導覽設計 指南。 | KEYCODE_BACK |
返回選單中的上一個項目 | BUTTON_B |
確認選取項目或執行主要遊戲動作 | 「BUTTON_A 」和
DPAD_CENTER |
* 遊戲不應仰賴「開始」、「選取」或「選單」 按鈕。
提示: 建議您提供設定畫面 讓使用者能自訂遊戲控制器的對應關係。 遊戲動作。
下列程式碼片段說明如何覆寫
onKeyDown()
到
將 BUTTON_A
和
DPAD_CENTER
按鈕曾按下
使用者想要透過遊戲動作進行互動
Kotlin
class GameView(...) : View(...) { ... override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { var handled = false if (event.source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD) { if (event.repeatCount == 0) { when (keyCode) { // Handle gamepad and D-pad button presses to navigate the ship ... else -> { keyCode.takeIf { isFireKey(it) }?.run { // Update the ship object to fire lasers ... handled = true } } } } if (handled) { return true } } return super.onKeyDown(keyCode, event) } // Here we treat Button_A and DPAD_CENTER as the primary action // keys for the game. private fun isFireKey(keyCode: Int): Boolean = keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_BUTTON_A }
Java
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; } }
注意: 在 Android 4.2 (API) 上
第 17 級) 以下,系統就會
BUTTON_A
做為 Android
預設使用 Back 鍵。如果您的應用程式支援這些 Android 系統
版本時,請務必
以「BUTTON_A
」做為主要遊戲
動作。判斷目前的 Android SDK
請參閱
Build.VERSION.SDK_INT
的值。
處理方向鍵輸入
4 向方向鍵 (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 按鍵代碼與輸入事件相同,如表 2 所述。
表 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 的值) |
以下程式碼片段顯示的是輔助類別,可用來檢查帽子 軸和按鍵碼值,藉此判斷 D-Pad 的方向。
Kotlin
class Dpad { private var directionPressed = -1 // initialized to -1 fun getDirectionPressed(event: InputEvent): Int { if (!isDpadDevice(event)) { return -1 } // If the input event is a MotionEvent, check its hat axis values. (event as? MotionEvent)?.apply { // Use the hat axis value to find the D-pad direction val xaxis: Float = event.getAxisValue(MotionEvent.AXIS_HAT_X) val yaxis: Float = event.getAxisValue(MotionEvent.AXIS_HAT_Y) directionPressed = when { // Check if the AXIS_HAT_X value is -1 or 1, and set the D-pad // LEFT and RIGHT direction accordingly. xaxis.compareTo(-1.0f) == 0 -> Dpad.LEFT xaxis.compareTo(1.0f) == 0 -> Dpad.RIGHT // Check if the AXIS_HAT_Y value is -1 or 1, and set the D-pad // UP and DOWN direction accordingly. yaxis.compareTo(-1.0f) == 0 -> Dpad.UP yaxis.compareTo(1.0f) == 0 -> Dpad.DOWN else -> directionPressed } } // If the input event is a KeyEvent, check its key code. (event as? KeyEvent)?.apply { // Use the key code to find the D-pad direction. directionPressed = when(event.keyCode) { KeyEvent.KEYCODE_DPAD_LEFT -> Dpad.LEFT KeyEvent.KEYCODE_DPAD_RIGHT -> Dpad.RIGHT KeyEvent.KEYCODE_DPAD_UP -> Dpad.UP KeyEvent.KEYCODE_DPAD_DOWN -> Dpad.DOWN KeyEvent.KEYCODE_DPAD_CENTER -> Dpad.CENTER else -> directionPressed } } return directionPressed } companion object { internal const val UP = 0 internal const val LEFT = 1 internal const val RIGHT = 2 internal const val DOWN = 3 internal const val CENTER = 4 fun isDpadDevice(event: InputEvent): Boolean = // Check that input comes from a device with directional pads. event.source and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD } }
Java
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()
回呼)。
例如:
Kotlin
private val dpad = Dpad() ... override fun onGenericMotionEvent(event: MotionEvent): Boolean { if (Dpad.isDpadDevice(event)) { when (dpad.getDirectionPressed(event)) { Dpad.LEFT -> { // Do something for LEFT direction press ... return true } Dpad.RIGHT -> { // Do something for RIGHT direction press ... return true } Dpad.UP -> { // Do something for UP direction press ... return true } ... } } // Check if this event is from a joystick movement and process accordingly. ... }
Java
Dpad dpad = 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 = dpad.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 會回報一位玩家
MotionEvent
,其中包含
ACTION_MOVE
動作程式碼和更新後的程式碼
搖桿軸心的位置遊戲可以使用
MotionEvent
用於判斷搖桿是否移動
沒有在乎的事
請注意,搖桿動作事件可能會批次批次處理多個動作樣本
在單一物件中定義多個大小MotionEvent
物件包含
每個搖桿的目前位置以及多個歷來資料
指定每個軸的位置回報含有動作程式碼 ACTION_MOVE
的動作事件 (例如搖桿動作) 時,Android 會批次處理
以提升效率特定軸的歷來值包括
大於目前軸值的不重複值組合,且大於目前值的
所回報的價值。詳情請參閱
詳情請參閱「MotionEvent
」參考資料。
您可以使用歷來資訊更準確地轉譯遊戲
物件動作。目的地:
擷取目前和歷來的值,呼叫
getAxisValue()
或 getHistoricalAxisValue()
。您也可以在「帳單」分頁中
方法是呼叫
getHistorySize()
。
下列程式碼片段說明如何覆寫
onGenericMotionEvent()
回呼來處理搖桿輸入。您應先
處理軸的歷史值,然後處理其目前的位置。
Kotlin
class GameView(...) : View(...) { override fun onGenericMotionEvent(event: MotionEvent): Boolean { // Check that the event came from a game controller return if (event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK && event.action == MotionEvent.ACTION_MOVE) { // Process the movements starting from the // earliest historical position in the batch (0 until event.historySize).forEach { i -> // Process the event at historical position i processJoystickInput(event, i) } // Process the current movement sample in the batch (position -1) processJoystickInput(event, -1) true } else { super.onGenericMotionEvent(event) } } }
Java
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); } }
使用搖桿輸入功能前,您必須先判斷搖桿是否為 ,然後據此計算它的軸移動。通常 具有 flat 區域,也就是 (0,0) 座標附近的值範圍 系統會將這個軸視為中心點。如果根據 Android 位於平面範圍內,您應該將控制器設為 休息 (也就是兩軸之間無動作)。
以下程式碼片段顯示計算運動時移動的 Helper 方法
。您已透過 processJoystickInput()
方法叫用這個輔助程式
詳細說明。
Kotlin
private fun getCenteredAxis( event: MotionEvent, device: InputDevice, axis: Int, historyPos: Int ): Float { val range: InputDevice.MotionRange? = device.getMotionRange(axis, event.source) // 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. range?.apply { val value: Float = if (historyPos < 0) { event.getAxisValue(axis) } else { 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 0f }
Java
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; }
全部整合之後,接下來要說明如何處理搖桿動作 您的遊戲:
Kotlin
private fun processJoystickInput(event: MotionEvent, historyPos: Int) { val inputDevice = event.device // 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. var x: Float = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_X, historyPos) if (x == 0f) { x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_X, historyPos) } if (x == 0f) { x = getCenteredAxis(event, inputDevice, 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. var y: Float = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_Y, historyPos) if (y == 0f) { y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_Y, historyPos) } if (y == 0f) { y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_RZ, historyPos) } // Update the ship object based on the new x and y values }
Java
private void processJoystickInput(MotionEvent event, int historyPos) { InputDevice inputDevice = 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, inputDevice, MotionEvent.AXIS_X, historyPos); if (x == 0) { x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_X, historyPos); } if (x == 0) { x = getCenteredAxis(event, inputDevice, 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, inputDevice, MotionEvent.AXIS_Y, historyPos); if (y == 0) { y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_Y, historyPos); } if (y == 0) { y = getCenteredAxis(event, inputDevice, 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 級別 18),這個控制器會AXIS_LTRIGGER
也回報了 這兩個AXIS_BRAKE
軸的值相同。AXIS_RTRIGGER
和AXIS_GAS
。Android 會回報所有類比觸發事件 正規化值從 0.0 (發行) 到 1.0 (完全按下)。非 所有控制器都有觸發條件,因此請考慮讓玩家 搭配其他按鈕玩遊戲