声明 :本文部分内容使用AI辅助生成,经人工编辑、审核和补充个人经验。
更新说明 :技术栈版本信息基于 Cocos2d-x 4.x / Android API 34。
问题背景 Cocos2d-x 要跑在 Win32、Android、TV 盒子等多种平台上。不同平台的输入设备差异很大:Win32 用键盘,Android 用触屏和按键,TV 盒子用遥控器。这篇文章分享我在项目中实现的一套统一按键映射方案,让一套代码适配所有输入设备。
平台按键差异概览 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ┌─────────────────────────────────────────────────────────────────────┐ │ 不同平台的按键输入对比 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ Win32 键盘 Android 设备 TV 遥控器 │ │ ───────────── ───────────── ───────────── │ │ │ │ ↑ VK_UP(38) ↑ KEYCODE_DPAD_UP ↑ KEYCODE_DPAD_UP│ │ ↓ VK_DOWN(40) ↓ KEYCODE_DPAD_DOWN ↓ KEYCODE_DPAD_DOWN│ │ ← VK_LEFT(37) ← KEYCODE_DPAD_LEFT ← KEYCODE_DPAD_LEFT│ │ → VK_RIGHT(39) → KEYCODE_DPAD_RIGHT → KEYCODE_DPAD_RIGHT│ │ ↵ VK_RETURN(13) ↵ KEYCODE_ENTER ↵ KEYCODE_DPAD_CENTER│ │ Esc VK_ESCAPE(27) ◁ KEYCODE_BACK ◁ KEYCODE_BACK │ │ F1 VK_F1(112) ☰ KEYCODE_MENU ☰ KEYCODE_MENU │ │ 1 VK_1(49) 1 KEYCODE_1 1 KEYCODE_1 │ │ 2 VK_2(50) 2 KEYCODE_2 2 KEYCODE_2 │ │ │ │ 问题:不同平台的键值完全不同! │ │ 解决方案:建立统一的按键映射层 │ │ │ └─────────────────────────────────────────────────────────────────────┘
Cocos2d-x 按键系统架构 原始按键系统的问题 Cocos2d-x 2.x 版本的原始按键分发器(CCKeypadDispatcher)只支持两个按键:
1 2 3 4 5 6 7 8 9 10 11 switch (nMsgType) { case kTypeBackClicked: pDelegate->keyBackClicked (); break ; case kTypeMenuClicked: pDelegate->keyMenuClicked (); break ; default : break ; }
这显然无法满足复杂游戏的需求。我们需要扩展按键系统,支持所有按键类型。
扩展后的按键架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ┌─────────────────────────────────────────────────────────────┐ │ 扩展按键系统架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ CCKeypadDispatcher │ │ │ │ (按键分发器) │ │ │ └────────────────────────┬────────────────────────────┘ │ │ │ │ │ ┌────────────┼────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Win32键盘 │ │ Android按键 │ │ 遥控器按键 │ │ │ │ (VK_CODE) │ │ (KeyCode) │ │ (KeyEvent) │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └────────────────┴────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ 统一按键编码 │ │ │ │ (GameKeyCode) │ │ │ └──────────┬──────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ onKeyDown/Up │ │ │ │ (业务处理) │ │ │ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
核心修改方案 步骤 1:扩展按键委托协议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class CCKeypadDelegate {public : virtual void keyBackClicked () {} virtual void keyMenuClicked () {} virtual void onKeyDown (int keyCode) {} virtual void onKeyUp (int keyCode) {} virtual void onKeyLongPress (int keyCode) {} };
步骤 2:修改按键分发器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 bool CCKeypadDispatcher::dispatchKeypadMSG (ccKeypadMSGType nMsgType) { switch (nMsgType) { case kTypeBackClicked: pDelegate->keyBackClicked (); break ; case kTypeMenuClicked: pDelegate->keyMenuClicked (); break ; default : break ; } pDelegate->onKeyDown (nMsgType); return true ; }
步骤 3:Win32 平台事件接入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 LRESULT CCEGLView::WindowProc (UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_KEYDOWN: CCDirector::sharedDirector ()->getKeypadDispatcher () ->dispatchKeypadMSG (wParam); break ; case WM_KEYUP: break ; } return DefWindowProc (m_hWnd, message, wParam, lParam); }
步骤 4:Android 平台事件接入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 extern "C" { JNIEXPORT jboolean JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeKeyDown ( JNIEnv* env, jobject thiz, jint keyCode) { CCDirector* pDirector = CCDirector::sharedDirector (); if (pDirector->getKeypadDispatcher ()->dispatchKeypadMSG (keyCode)) { return JNI_TRUE; } return JNI_FALSE; } }
步骤 5:Android Java 层修改 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Override public boolean onKeyDown (final int pKeyCode, final KeyEvent pKeyEvent) { this .queueEvent(new Runnable () { @Override public void run () { Cocos2dxGLSurfaceView.this .mCocos2dxRenderer.handleKeyDown(pKeyCode); } }); if (pKeyCode == KeyEvent.KEYCODE_BACK) { return true ; } return true ; } @Override public boolean onKeyUp (final int pKeyCode, final KeyEvent pKeyEvent) { this .queueEvent(new Runnable () { @Override public void run () { Cocos2dxGLSurfaceView.this .mCocos2dxRenderer.handleKeyUp(pKeyCode); } }); return super .onKeyUp(pKeyCode, pKeyEvent); }
统一按键编码系统 定义游戏按键枚举 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #ifndef __GAME_KEY_CODE_H__ #define __GAME_KEY_CODE_H__ enum GameKeyCode { GAME_KEY_UP = 0 , GAME_KEY_DOWN, GAME_KEY_LEFT, GAME_KEY_RIGHT, GAME_KEY_OK, GAME_KEY_BACK, GAME_KEY_MENU, GAME_KEY_HOME, GAME_KEY_0, GAME_KEY_1, GAME_KEY_2, GAME_KEY_3, GAME_KEY_4, GAME_KEY_5, GAME_KEY_6, GAME_KEY_7, GAME_KEY_8, GAME_KEY_9, GAME_KEY_VOLUME_UP, GAME_KEY_VOLUME_DOWN, GAME_KEY_MUTE, GAME_KEY_UNKNOWN = -1 }; #endif
平台按键映射器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #ifndef __KEY_CODE_MAPPER_H__ #define __KEY_CODE_MAPPER_H__ #include "GameKeyCode.h" #include <map> class KeyCodeMapper {public : enum PlatformType { PLATFORM_WIN32, PLATFORM_ANDROID, PLATFORM_IOS, PLATFORM_TV_BOX }; static KeyCodeMapper* getInstance () ; void init (PlatformType platform) ; GameKeyCode mapToGameKey (int nativeKeyCode) ; int mapToNativeKey (GameKeyCode gameKey) ; const char * getKeyName (GameKeyCode gameKey) ; private : KeyCodeMapper (); PlatformType _platform; std::map<int , GameKeyCode> _nativeToGame; std::map<GameKeyCode, int > _gameToNative; void initWin32Mapping () ; void initAndroidMapping () ; void initTVBoxMapping () ; }; #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 #include "KeyCodeMapper.h" #if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) #include <windows.h> #endif #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) #define AKEYCODE_DPAD_UP 19 #define AKEYCODE_DPAD_DOWN 20 #define AKEYCODE_DPAD_LEFT 21 #define AKEYCODE_DPAD_RIGHT 22 #define AKEYCODE_DPAD_CENTER 23 #define AKEYCODE_BACK 4 #define AKEYCODE_MENU 82 #define AKEYCODE_HOME 3 #define AKEYCODE_0 7 #define AKEYCODE_1 8 #define AKEYCODE_VOLUME_UP 24 #define AKEYCODE_VOLUME_DOWN 25 #endif static KeyCodeMapper* s_instance = nullptr ;KeyCodeMapper* KeyCodeMapper::getInstance () { if (!s_instance) { s_instance = new KeyCodeMapper (); } return s_instance; } void KeyCodeMapper::init (PlatformType platform) { _platform = platform; _nativeToGame.clear (); _gameToNative.clear (); switch (platform) { case PLATFORM_WIN32: initWin32Mapping (); break ; case PLATFORM_ANDROID: initAndroidMapping (); break ; case PLATFORM_TV_BOX: initTVBoxMapping (); break ; default : initAndroidMapping (); break ; } } void KeyCodeMapper::initWin32Mapping () { _nativeToGame[VK_UP] = GAME_KEY_UP; _nativeToGame[VK_DOWN] = GAME_KEY_DOWN; _nativeToGame[VK_LEFT] = GAME_KEY_LEFT; _nativeToGame[VK_RIGHT] = GAME_KEY_RIGHT; _nativeToGame[VK_RETURN] = GAME_KEY_OK; _nativeToGame[VK_ESCAPE] = GAME_KEY_BACK; _nativeToGame[VK_SPACE] = GAME_KEY_OK; _nativeToGame['0' ] = GAME_KEY_0; _nativeToGame['1' ] = GAME_KEY_1; _nativeToGame['2' ] = GAME_KEY_2; _nativeToGame['3' ] = GAME_KEY_3; _nativeToGame['4' ] = GAME_KEY_4; _nativeToGame['5' ] = GAME_KEY_5; _nativeToGame['6' ] = GAME_KEY_6; _nativeToGame['7' ] = GAME_KEY_7; _nativeToGame['8' ] = GAME_KEY_8; _nativeToGame['9' ] = GAME_KEY_9; for (auto & pair : _nativeToGame) { _gameToNative[pair.second] = pair.first; } } void KeyCodeMapper::initAndroidMapping () { _nativeToGame[AKEYCODE_DPAD_UP] = GAME_KEY_UP; _nativeToGame[AKEYCODE_DPAD_DOWN] = GAME_KEY_DOWN; _nativeToGame[AKEYCODE_DPAD_LEFT] = GAME_KEY_LEFT; _nativeToGame[AKEYCODE_DPAD_RIGHT] = GAME_KEY_RIGHT; _nativeToGame[AKEYCODE_DPAD_CENTER] = GAME_KEY_OK; _nativeToGame[AKEYCODE_BACK] = GAME_KEY_BACK; _nativeToGame[AKEYCODE_MENU] = GAME_KEY_MENU; _nativeToGame[AKEYCODE_HOME] = GAME_KEY_HOME; _nativeToGame[AKEYCODE_0] = GAME_KEY_0; _nativeToGame[AKEYCODE_1] = GAME_KEY_1; _nativeToGame[AKEYCODE_2] = GAME_KEY_2; _nativeToGame[AKEYCODE_VOLUME_UP] = GAME_KEY_VOLUME_UP; _nativeToGame[AKEYCODE_VOLUME_DOWN] = GAME_KEY_VOLUME_DOWN; for (auto & pair : _nativeToGame) { _gameToNative[pair.second] = pair.first; } } void KeyCodeMapper::initTVBoxMapping () { initAndroidMapping (); } GameKeyCode KeyCodeMapper::mapToGameKey (int nativeKeyCode) { auto it = _nativeToGame.find (nativeKeyCode); if (it != _nativeToGame.end ()) { return it->second; } return GAME_KEY_UNKNOWN; } int KeyCodeMapper::mapToNativeKey (GameKeyCode gameKey) { auto it = _gameToNative.find (gameKey); if (it != _gameToNative.end ()) { return it->second; } return -1 ; } const char * KeyCodeMapper::getKeyName (GameKeyCode gameKey) { switch (gameKey) { case GAME_KEY_UP: return "UP" ; case GAME_KEY_DOWN: return "DOWN" ; case GAME_KEY_LEFT: return "LEFT" ; case GAME_KEY_RIGHT: return "RIGHT" ; case GAME_KEY_OK: return "OK" ; case GAME_KEY_BACK: return "BACK" ; case GAME_KEY_MENU: return "MENU" ; case GAME_KEY_HOME: return "HOME" ; default : return "UNKNOWN" ; } }
业务层使用示例 按键监听层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #ifndef __BASE_LAYER_H__ #define __BASE_LAYER_H__ #include "cocos2d.h" #include "GameKeyCode.h" USING_NS_CC; class BaseLayer : public CCLayer {public : virtual bool init () ; virtual void onKeyDown (int nativeKeyCode) ; virtual void onKeyUp (int nativeKeyCode) ; virtual void onGameKeyDown (GameKeyCode keyCode) ; virtual void onGameKeyUp (GameKeyCode keyCode) ; CREATE_FUNC (BaseLayer); }; #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include "BaseLayer.h" #include "KeyCodeMapper.h" bool BaseLayer::init () { if (!CCLayer::init ()) { return false ; } this ->setKeypadEnabled (true ); return true ; } void BaseLayer::onKeyDown (int nativeKeyCode) { GameKeyCode gameKey = KeyCodeMapper::getInstance ()->mapToGameKey (nativeKeyCode); if (gameKey != GAME_KEY_UNKNOWN) { CCLOG ("Key Down: %s (native: %d)" , KeyCodeMapper::getInstance ()->getKeyName (gameKey), nativeKeyCode); onGameKeyDown (gameKey); } } void BaseLayer::onKeyUp (int nativeKeyCode) { GameKeyCode gameKey = KeyCodeMapper::getInstance ()->mapToGameKey (nativeKeyCode); if (gameKey != GAME_KEY_UNKNOWN) { onGameKeyUp (gameKey); } } void BaseLayer::onGameKeyDown (GameKeyCode keyCode) { switch (keyCode) { case GAME_KEY_BACK: CCDirector::sharedDirector ()->popScene (); break ; default : break ; } } void BaseLayer::onGameKeyUp (GameKeyCode keyCode) { }
具体业务层实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #ifndef __SHOP_LAYER_H__ #define __SHOP_LAYER_H__ #include "BaseLayer.h" class ShopLayer : public BaseLayer {public : virtual bool init () ; virtual void onGameKeyDown (GameKeyCode keyCode) ; void selectNextItem () ; void selectPrevItem () ; void confirmSelection () ; void backToMainMenu () ; CREATE_FUNC (ShopLayer); }; #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include "ShopLayer.h" bool ShopLayer::init () { if (!BaseLayer::init ()) { return false ; } initShopUI (); return true ; } void ShopLayer::onGameKeyDown (GameKeyCode keyCode) { switch (keyCode) { case GAME_KEY_UP: selectPrevItem (); break ; case GAME_KEY_DOWN: selectNextItem (); break ; case GAME_KEY_LEFT: switchCategory(-1 ); break ; case GAME_KEY_RIGHT: switchCategory(1 ); break ; case GAME_KEY_OK: case GAME_KEY_ENTER: confirmSelection (); break ; case GAME_KEY_BACK: backToMainMenu (); break ; case GAME_KEY_1: case GAME_KEY_2: case GAME_KEY_3: quickBuy (keyCode - GAME_KEY_1); break ; default : BaseLayer::onGameKeyDown (keyCode); break ; } } void ShopLayer::selectNextItem () { _selectedIndex++; if (_selectedIndex >= _itemCount) { _selectedIndex = 0 ; } updateSelection (); } void ShopLayer::selectPrevItem () { _selectedIndex--; if (_selectedIndex < 0 ) { _selectedIndex = _itemCount - 1 ; } updateSelection (); } void ShopLayer::confirmSelection () { AudioManager::playEffect ("confirm.mp3" ); buyItem (_selectedIndex); } void ShopLayer::backToMainMenu () { AudioManager::playEffect ("back.mp3" ); CCDirector::sharedDirector ()->replaceScene ( CCTransitionFade::create (0.5f , MainMenuScene::scene ()) ); }
各平台键值参考表 Win32 虚拟键码
按键
键值
按键
键值
VK_UP
38
VK_DOWN
40
VK_LEFT
37
VK_RIGHT
39
VK_RETURN
13
VK_ESCAPE
27
VK_SPACE
32
VK_TAB
9
VK_F1
112
VK_F2
113
‘0’-‘9’
48-57
‘A’-‘Z’
65-90
Android 按键码
按键
键值
按键
键值
KEYCODE_DPAD_UP
19
KEYCODE_DPAD_DOWN
20
KEYCODE_DPAD_LEFT
21
KEYCODE_DPAD_RIGHT
22
KEYCODE_DPAD_CENTER
23
KEYCODE_ENTER
66
KEYCODE_BACK
4
KEYCODE_MENU
82
KEYCODE_HOME
3
KEYCODE_VOLUME_UP
24
KEYCODE_0
7
KEYCODE_1
8
遥控器按键码(基于 Android)
按键
键值
说明
KEYCODE_DPAD_UP
19
方向上
KEYCODE_DPAD_DOWN
20
方向下
KEYCODE_DPAD_LEFT
21
方向左
KEYCODE_DPAD_RIGHT
22
方向右
KEYCODE_DPAD_CENTER
23
确定/OK
KEYCODE_BACK
4
返回
KEYCODE_MENU
82
菜单
KEYCODE_HOME
3
主页
KEYCODE_VOLUME_UP
24
音量+
KEYCODE_VOLUME_DOWN
25
音量-
小结 Cocos2d-x 多平台按键映射的核心思路:
统一抽象 :定义平台无关的游戏按键枚举(GameKeyCode)
映射层 :建立平台按键码到游戏按键码的双向映射
分发器改造 :扩展 CCKeypadDispatcher 支持所有按键类型
平台适配 :在 Win32/Android 等平台层接入所有按键事件
业务隔离 :业务层只处理统一的游戏按键码,不用关心平台差异
这套方案我在 TV 游戏项目中用过,一套代码同时支持 Win32 调试、Android 手机和遥控器操作。