大家好,我们经常会玩赛车游戏,当我们按下一个按键时,如果只能匀速运动是令人烦燥的。在游戏中用到加速度,利用加速度对于物体进行更真实的控制是必不可少的技能。今天我们来学习一下Cocos2d-x中如何利用按键来处理速度的累加。
我们知道在WIN32编程中,一个键按下时所对应的窗口会收到WM_KEYDOWN消息,如果按下的键输入的是字符,窗口接着会收到WM_CHAR消息,而当按键抬起时对应的窗口会收到WM_KEYUP消息。在HelloWorld深入分析一节中,我们知道在CCEGLView类中有窗口消息的处理函数WindowProc,所以要进行按键的处理,就需要在接受按键消息时调用相应的函数。我们来看一下它在接受到WM_KEYDOWN,WM_CHAR和WM_KEYUP时做了什么。
- LRESULT CCEGLView::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
- {
- …
- case WM_KEYDOWN:
- if (wParam == VK_F1 || wParam == VK_F2)
- {
- CCDirector* pDirector = CCDirector::sharedDirector();
- //当SHIFT键被按下时,根据是否按下的是F1来决定软键盘收到的消息是响应回退还是主菜单。
- if (GetKeyState(VK_LSHIFT) < 0 || GetKeyState(VK_RSHIFT) < 0 || GetKeyState(VK_SHIFT) < 0)
- pDirector->getKeypadDispatcher()->dispatchKeypadMSG(wParam == VK_F1 ? kTypeBackClicked : kTypeMenuClicked);
- }
- //如果按键消息响应函数有效则调用函数指针m_lpfnAccelerometerKeyHook所指的函数。
- if ( m_lpfnAccelerometerKeyHook!=NULL )
- {
- (*m_lpfnAccelerometerKeyHook)( message,wParam,lParam );
- }
- break;
- case WM_KEYUP:
- //如果按键消息响应函数有效则调用函数指针m_lpfnAccelerometerKeyHook所指的函数。
- if ( m_lpfnAccelerometerKeyHook!=NULL )
- {
- (*m_lpfnAccelerometerKeyHook)( message,wParam,lParam );
- }
- break;
- case WM_CHAR:
- {
- //如果输入的字符是控制符则进行相关处理。
- if (wParam < 0x20)
- {
- if (VK_BACK == wParam)
- {
- CCIMEDispatcher::sharedDispatcher()->dispatchDeleteBackward();
- }
- else if (VK_RETURN == wParam)
- {
- CCIMEDispatcher::sharedDispatcher()->dispatchInsertText("\n", 1);
- }
- else if (VK_TAB == wParam)
- {
- // tab input
- }
- else if (VK_ESCAPE == wParam)
- {
- // ESC input
- CCDirector::sharedDirector()->end();
- }
- }
- else if (wParam < 128)
- {
- //如果是ASCII码,则响应文字输入。
- CCIMEDispatcher::sharedDispatcher()->dispatchInsertText((const char *)&wParam, 1);
- }
- else
- { //大于128的使用宽字符,所以这里将其转化为多字节字符串来输入文本。
- char szUtf8[8] = {0};
- int nLen = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR)&wParam, 1, szUtf8, sizeof(szUtf8), NULL, NULL);
- CCIMEDispatcher::sharedDispatcher()->dispatchInsertText(szUtf8, nLen);
- }
- //如果按键消息响应函数有效则调用函数指针m_lpfnAccelerometerKeyHook所指的函数。
- if ( m_lpfnAccelerometerKeyHook!=NULL )
- {
- (*m_lpfnAccelerometerKeyHook)( message,wParam,lParam );
- }
- }
- break;
- …
- }
可以看到,CCEGLView类中有一个成员函数指针m_lpfnAccelerometerKeyHook,它将具体的实际的按键处理交给了外部函数。
那么,是谁?在哪里去设置函数指针指向的函数呢?我们保留疑问,继续学习。
我们打开CCLayer.h,我们来看一下CCLayer的声明:
class CC_DLL CCLayer :public CCNode, public CCTouchDelegate,public CCAccelerometerDelegate,publicCCKeypadDelegate
在这一行里,我们可以清楚的知道CCLayer的本质是什么,它是具有输入功能的节点。
CCAccelerometerDelegate是加速键消息处理接口类,可以接收到加速键产生的速度信息并进行处理,我们打开源码
CCAccelerometerDelegate.h来看一下:
- //这个结构用于记录当前时刻的X,Y,Z三个方向的速度信息。
- typedef struct
- {
- double x;
- double y;
- double z;
- double timestamp;
- }
- CCAcceleration;
- //类CCAccelerometerDelegate
- class CC_DLL CCAccelerometerDelegate
- {
- public:
- //接受CCAcceleration消息处理的虚函数。
- virtual void didAccelerate(CCAcceleration* pAccelerationValue) {CC_UNUSED_PARAM(pAccelerationValue);}
- };
再回到CCLayer.h:
- class CC_DLL CCLayer : public CCNode, public CCTouchDelegate, public CCAccelerometerDelegate, public CCKeypadDelegate
- {
- …
- //重载CCAccelerometerDelegate中相应函数,具体实现相应功能。
- virtual void didAccelerate(CCAcceleration* pAccelerationValue)
- {CC_UNUSED_PARAM(pAccelerationValue);}
- …
- //取得是否开启响应按键处理。
- bool isAccelerometerEnabled();
- //设置是否开启响应按键处理。
- void setAccelerometerEnabled(bool value);
- …
- protected:
- bool m_bIsAccelerometerEnabled;
- …
- }
打开CPP:
- //取得是否开启响应按键处理。
- bool CCLayer::isAccelerometerEnabled()
- {
- return m_bIsAccelerometerEnabled;
- }
- //设置是否开启响应按键处理。
- void CCLayer::setAccelerometerEnabled(bool enabled)
- {
- if (enabled != m_bIsAccelerometerEnabled)
- {
- m_bIsAccelerometerEnabled = enabled;
- //如果正在运行中。
- if (m_bIsRunning)
- {
- //取得当前设备
- CCDirector* pDirector = CCDirector::sharedDirector();
- if (enabled)
- {
- //取得设备的加速键管理器,设置其内部的按键消息响应处理接口类对象指针指向当前层。
- pDirector->getAccelerometer()->setDelegate(this);
- }
- else
- { //取得设备的加速键管理器,设置其内部的按键消息响应处理接口类对象指针为空。
- pDirector->getAccelerometer()->setDelegate(NULL);
- }
- }
- }
- }
设备也不甘寂寞啊。里面有一个getAccelerometer函数为取得加速键管理器。
打开CCDirector.h:
CC_PROPERTY(CCAccelerometer*,m_pAccelerometer, Accelerometer);
CCAccelerometer类的定义在CCAccelerometer.h中:
- class CC_DLL CCAccelerometer
- {
- public:
- //构造与析构
- CCAccelerometer();
- ~CCAccelerometer();
- //设置加速键消息响应处理接口类实例指针。
- void setDelegate(CCAccelerometerDelegate* pDelegate);
- //更新当前时刻的速度处理。
- void update( double x,double y,double z,double timestamp );
- private:
- //当前时刻的速度信息。
- CCAcceleration m_obAccelerationValue;
- //加速键消息响应处理接口类实例指针。
- CCAccelerometerDelegate* m_pAccelDelegate;
- };
其对应CPP为:
- namespace
- {
- //定义一些命名空间中的全局变量。
- //X,Y,Z方向的速度
- double g_accelX=0.0;
- double g_accelY=0.0;
- double g_accelZ=0.0;
- //X,Y,Z方向的加速度
- const double g_accelerationStep=0.2f;
- const double g_minAcceleration=-1.0f;
- const double g_maxAcceleration=1.0f;
- //命名空间内的模版函数,用于取得val的有效值(即限定了最小和最大范围)
- template <class T>
- T CLAMP( const T val,const T minVal,const T maxVal )
- {
- CC_ASSERT( minVal<=maxVal );
- T result=val;
- if ( result<minVal )
- result=minVal;
- else if ( result>maxVal )
- result=maxVal;
- CC_ASSERT( minVal<=result && result<=maxVal );
- return result;
- }
- //命名空间内的函数,用于响应按下键后的处理。
- bool handleKeyDown( WPARAM wParam )
- {//定义变量值设置是否有速度更新。
- bool sendUpdate=false
- //通过接键来处理X,Y,Z方向的速度变化。
- switch( wParam )
- {
- case VK_LEFT:
- sendUpdate=true;
- //X方向速度减小 g_accelX=CLAMP( g_accelX-g_accelerationStep,g_minAcceleration,g_maxAcceleration );
- break;
- case VK_RIGHT:
- sendUpdate=true;
- //X方向速度增大
- g_accelX=CLAMP( g_accelX+g_accelerationStep,g_minAcceleration,g_maxAcceleration );
- break;
- case VK_UP:
- sendUpdate=true;
- //Y方向速度增大
- g_accelY=CLAMP( g_accelY+g_accelerationStep,g_minAcceleration,g_maxAcceleration );
- break;
- case VK_DOWN:
- sendUpdate=true;
- //Y方向速度减小
- g_accelY=CLAMP( g_accelY-g_accelerationStep,g_minAcceleration,g_maxAcceleration );
- break;
- case VK_OEM_COMMA:
- sendUpdate=true;
- //Z方向速度增大
- g_accelZ=CLAMP( g_accelZ+g_accelerationStep,g_minAcceleration,g_maxAcceleration );
- break;
- case VK_OEM_PERIOD:
- sendUpdate=true;
- //Z方向速度减小
- g_accelZ=CLAMP( g_accelZ-g_accelerationStep,g_minAcceleration,g_maxAcceleration );
- break;
- }
- //返回是否有速度更新。
- return sendUpdate;
- }
- //命名空间内的函数,用于响应键松开时的处理。
- bool handleKeyUp( WPARAM wParam )
- {//定义变量值设置是否有速度更新。
- bool sendUpdate=false;
- switch( wParam )
- {
- case VK_LEFT:
- case VK_RIGHT:
- sendUpdate=true;
- //将速度重置为零
- g_accelX=0.0;
- break;
- case VK_UP:
- case VK_DOWN:
- sendUpdate=true;
- //将速度重置为零
- g_accelY=0.0;
- break;
- case VK_OEM_COMMA:
- case VK_OEM_PERIOD:
- sendUpdate=true;
- //将速度重置为零
- g_accelZ=0.0;
- break;
- }
- //返回是否有速度更新。
- return sendUpdate;
- }
- //命名空间内的函数,用于加速键响应处理函数。
- void myAccelerometerKeyHook( UINT message,WPARAM wParam,LPARAM lParam )
- {
- //取得加速键管理器指针
- cocos2d::CCAccelerometer *pAccelerometer = cocos2d::CCDirector::sharedDirector()->getAccelerometer();
- bool sendUpdate=false;
- switch( message )
- {
- case WM_KEYDOWN:
- //调用按下加速键的处理。
- sendUpdate=handleKeyDown( wParam );
- break;
- case WM_KEYUP:
- //调用松开加速键的处理。
- sendUpdate=handleKeyUp( wParam );
- break;
- case WM_CHAR:
- break;
- default:
- // Not expected to get here!!
- CC_ASSERT( false );
- break;
- }
- //如果有速度更新。
- if ( sendUpdate )
- {
- //计算当前时间。
- const time_t theTime=time(NULL);
- const double timestamp=(double)theTime / 100.0;
- //调用加速键管理器的更新函数更新当前速度 pAccelerometer->update( g_accelX,g_accelY,g_accelZ,timestamp );
- }
- }
- //重置速度
- void resetAccelerometer()
- {
- g_accelX=0.0;
- g_accelY=0.0;
- g_accelZ=0.0;
- }
- }
- NS_CC_BEGIN
- //构造函数。
- CCAccelerometer::CCAccelerometer() :
- m_pAccelDelegate(NULL)
- {
- memset(&m_obAccelerationValue, 0, sizeof(m_obAccelerationValue));
- }
- //析构
- CCAccelerometer::~CCAccelerometer()
- {
- }
- //设置加速键所用的加速键消息处理接口类实例。
- void CCAccelerometer::setDelegate(CCAccelerometerDelegate* pDelegate)
- {
- //保存加速键消息处理接口类实例
- m_pAccelDelegate = pDelegate;
- if (pDelegate)
- {
- //这里就解答了之前的疑问,也就是说,如果你对CCLayer设置开启响应按键处理,则会设置CCEGLView类实例使用的按键消息处理回调函数为命名空间内的函数myAccelerometerKeyHook。
- CCEGLView::sharedOpenGLView()->setAccelerometerKeyHook( &myAccelerometerKeyHook );
- }
- else
- {
- //如果参数为空,这里就注销设置CCEGLView类实例使用的按键消息处理回调函数。同时重置速度值。
- CCEGLView::sharedOpenGLView()->setAccelerometerKeyHook( NULL );
- resetAccelerometer();
- }
- }
- //加速键的更新函数,用于更新当前速度
- void CCAccelerometer::update( double x,double y,double z,double timestamp )
- {
- //保存相应的值到结构变量中。
- if (m_pAccelDelegate)
- {
- m_obAccelerationValue.x = x;
- m_obAccelerationValue.y = y;
- m_obAccelerationValue.z = z;
- m_obAccelerationValue.timestamp = timestamp;
- //调用虚函数进行处理,实际处理方式就由m_pAccelDelegate的派生类实例(如CCLayer及其派生类实例)来具体实现了。
- m_pAccelDelegate->didAccelerate(&m_obAccelerationValue);
- }
- }
有点迷糊吧,现在我来解释一下这个圈子是怎么一回事。
见图:

原理说明:
有一个函数myAccelerometerKeyHook,它会取得当前设备中的加速键管理器(CCAccelerometer)实例对象,并将接受到的按键消息做为参数调用其update函数,其update函数将按键消息生成的速度信息结构转发给相应的速度信息处理接口类(CCAccelerometerDelegate)实例指针,然后由此指针所指向的实例对象调用虚函数didAccelerate来进行实现相应的加速效果。
当我们对一个速度信息处理接口类(CCAccelerometerDelegate)的派生类,如CCLayer及其派生类设置开启响应加速键消息处理时,系统会将其设置为设备的加速键管理器(CCAccelerometer)实例对象的速度信息处理接口类(CCAccelerometerDelegate)实例指针,并将函数myAccelerometerKeyHook设置为CCEGLView类实例使用的按键消息处理回调函数。
原理解释清楚了,现在我们来看一下TestCpp中的AccelerometerTest。
打开头文件:
- //演示层AccelerometerTest。
- class AccelerometerTest: public CCLayer
- {
- protected:
- //用于演示加速变化的精灵。
- CCSprite* m_pBall;
- //上一帧的时间。
- double m_fLastTime;
- public:
- //构造与析构
- AccelerometerTest(void);
- ~AccelerometerTest(void);
- //关键之处了,具体实现按键后的速度变化。参数为按键后收到的速度信息。
- virtual void didAccelerate(CCAcceleration* pAccelerationValue);
- //取得标题。
- virtual std::string title();
- //当前层加载时的处理。
- virtual void onEnter();
- };
- //用于演示的场景。
- class AccelerometerTestScene : public TestScene
- {
- public:
- //加载场景时的处理。
- virtual void runThisTest();
- };
打开CPP:
- //定义宏,限定_pos的有效范围值。
- #define FIX_POS(_pos, _min, _max) \
- if (_pos < _min) \
- _pos = _min; \
- else if (_pos > _max) \
- _pos = _max; \
- //构造
- AccelerometerTest::AccelerometerTest(void)
- : m_fLastTime(0.0)
- {
- }
- //析构
- AccelerometerTest::~AccelerometerTest(void)
- {
- //对占用的小球引用计数减一。因为在onEnter中进行了加一操作。
- m_pBall->release();
- }
- //取得标题。
- std::string AccelerometerTest::title()
- {
- return "AccelerometerTest";
- }
- //加载当前层时的处理。
- void AccelerometerTest::onEnter()
- {
- //调用基类的相应函数。
- CCLayer::onEnter();
- //设置开启响应加速键
- setAccelerometerEnabled(true);
- //取得窗口大小
- CCSize s = CCDirector::sharedDirector()->getWinSize();
- //创建一个文字标签。
- CCLabelTTF* label = CCLabelTTF::create(title().c_str(), "Arial", 32);
- //将文字标签加入到当前层中。
- addChild(label, 1);
- //设置文字标签的位置。
- label->setPosition( CCPointMake(s.width/2, s.height-50) );
- //由一个小球的图片创建一个精灵。
- m_pBall = CCSprite::create("Images/ball.png");
- //设置小球的位置居于屏幕中央。
- m_pBall->setPosition(ccp(s.width / 2, s.height / 2));
- //将小球放到当前层中。
- addChild(m_pBall);
- //占用小球,让小球的引用计数器加一。这里加一,在析构时就必须减一才能使引用计数器保证不乱。不过个人认为这里引用计数加一没什么必要。因为addChild已经对小球精灵做了引用计数加一操作了,代表了占用,再加一略显繁琐。
- m_pBall->retain();
- }
- //具体实现按键后的速度变化。参数为按键后收到的速度信息。
- void AccelerometerTest::didAccelerate(CCAcceleration* pAccelerationValue)
- {
- //取得设备及窗口大小。
- CCDirector* pDir = CCDirector::sharedDirector();
- CCSize winSize = pDir->getWinSize();
- //判断小球精灵是否有效。
- if ( m_pBall == NULL ) {
- return;
- }
- //取得小球的图像区域大小。
- CCSize ballSize = m_pBall->getContentSize();
- //取得小球的当前位置。
- CCPoint ptNow = m_pBall->getPosition();
- //将当前位置转换成界面坐标系的位置。
- CCPoint ptTemp = pDir->convertToUI(ptNow);
- //由收到的速度乘以一个系数后来影响位置。
- ptTemp.x += pAccelerationValue->x * 9.81f;
- ptTemp.y -= pAccelerationValue->y * 9.81f;
- //再转换为OPENGL坐标系的位置。貌似有点麻烦,其实直接在上面X,Y的加减上做上正确的方向即可。
- CCPoint ptNext = pDir->convertToGL(ptTemp);
- //限定位置的X,Y的有效范围,等于小球边缘始终在窗口内。
- FIX_POS(ptNext.x, (ballSize.width / 2.0), (winSize.width - ballSize.width / 2.0));
- FIX_POS(ptNext.y, (ballSize.height / 2.0), (winSize.height - ballSize.height / 2.0));
- //将位置传给小球。
- m_pBall->setPosition(ptNext);
- }
- //运行场景
- void AccelerometerTestScene::runThisTest()
- {
- //创建一个新的AccelerometerTest的实例并加入当前场景之中。
- CCLayer* pLayer = new AccelerometerTest();
- addChild(pLayer);
- //new时实例的计数器会加一,所以这里release后会减1保持对应。另注意:addChild(pLayer)后也会因占用实例而对其计数器加一。
- pLayer->release();
- //运行当前场景。
- CCDirector::sharedDirector()->replaceScene(this);
- }
运行结果如图:

当我们按下键:上,下,右,左,<,>时,小球分别产生X,Y,Z的正负方向的加速度。WINDOWS在通过WM_KEYDOWN消息来即时处理按键响应时有延迟间隔的,所以按键响应不流畅,改成GetKeyState后就流畅多了,比如在CCEGLView的swapBuffer中加入相应的处理使每一帧都能进行按键的实时判断。
- void CCEGLView::swapBuffers()
- {
- if (m_hDC != NULL)
- {
- ::SwapBuffers(m_hDC);
- }
- //如果按下左键
- if(GetKeyState(VK_LEFT) < 0)
- {
- if ( m_lpfnAccelerometerKeyHook!=NULL )
- {
- (*m_lpfnAccelerometerKeyHook)( WM_KEYDOWN,VK_LEFT,0 );
- }
- }
- ….按上面写的如法炮制即可。
- }