风蚀之月

Cocos2d-x的windows改造

22 Dec 2012

最近在整cocos2d-x引擎,感觉这个引擎在跨平台特性上还是很不错的。

而且各个方面都封装的还不错,不用自己再去重复造轮子。可是在windows上使用时还是有些小小的不足,于是自己边查资料开始动手改造之。目前遇到的需要改造的有两个地方,一个是引擎本身没有提供全屏功能,作为一个在windows上的游戏如果不能全屏的话,多少还是有些别扭的,无论是从习惯上还是实际操作中都会遇到问题。另一个地方是CCMenu的事件处理,由于是从移动设备上移植过来的,所以内部并没有传统的onmousemove和onmouseout事件,这样实现出来的菜单显示效果是做在ontouchmove上的,在窗口中使用起来特效出现的时机就会变得很独特:只有在鼠标左键按下时才会触发类似于鼠标移入移出的效果。

为了避免不必要的麻烦,先说明一下开发环境,win7+vs2010+cocos2d-2.1beta3-x-2.1.0。其实关于全屏的问题,在网上能够找到一些相关的资料,只是上面提供的方法是针对旧版本的,在新版本中不再有效了。原来的方法是这样的,主要改动的是platform下的CCEGLView。

		/////////////// FULLSCREEN HACK - BEGIN 

		DWORD		dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;					// Window Extended Style
		DWORD		dwStyle = WS_CAPTION | WS_POPUPWINDOW | WS_MINIMIZEBOX;			// Window Style

		if (m_bFullScreen)
		{
			DEVMODE dmScreenSettings;								// Device Mode
			memset(&dmScreenSettings,0,sizeof(dmScreenSettings));	// Makes Sure Memory's Cleared
			dmScreenSettings.dmSize=sizeof(dmScreenSettings);		// Size Of The Devmode Structure
			dmScreenSettings.dmPelsWidth	= w;					// Selected Screen Width
			dmScreenSettings.dmPelsHeight	= h;					// Selected Screen Height
			dmScreenSettings.dmBitsPerPel	= 32;					// Selected Bits Per Pixel
			dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;

			// Try To Set Selected Mode And Get Results.  NOTE: CDS_FULLSCREEN Gets Rid Of Start Bar.
			if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
			{
				// If The Mode Fails, Offer Two Options.  Quit Or Use Windowed Mode.
				if (MessageBox(NULL, L"The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?",L"cocos2d-x",MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
				{
					setFullScreen(false);		// back to windowed mode
				}
				else
				{
					// Pop Up A Message Box Letting User Know The Program Is Closing.
					MessageBox(NULL,L"Program Will Now Close.",L"ERROR",MB_OK|MB_ICONSTOP);
					return FALSE;									// Return FALSE
				}
			}
			else	// yeah! we are in fullscreen
			{
				dwExStyle = WS_EX_APPWINDOW;
				dwStyle=WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN;										

				//ShowCursor(FALSE);

				RECT rect;
				rect.left=(long)0;			// Set Left Value To 0
				rect.right=(long)w;			// Set Right Value To Requested Width
				rect.top=(long)0;			// Set Top Value To 0
				rect.bottom=(long)h;		// Set Bottom Value To Requested Height

				AdjustWindowRectEx(&rect, dwStyle, FALSE, dwExStyle);		// Adjust Window To True Requested Size

			}
		}

		/////////////// FULLSCREEN HACK - END

将以上的代码进行替换之后会发现程序并没有进入全屏模式,好在下到了修改过的源码,从这段源码再往下和目前的版本对比的话会发现。函数的调用顺序已经不同了,原来的调用方式是在这一段之后进行窗口创建的。而现在的版本则是先初始化了一个1000x1000的窗口再对其进行resize的。这样的话即便是添加了全屏标志也没有用,因为窗口已经完成了初始化。

所以只能重新进行修改,其实很简单,在resize的这里再创建一个新的窗口就可以了。为了避免对程序结构的改动,再加上本身对win32 api和opengl之间的机制不是很熟悉,于是google到nehe的opengl教程照着教程边学边对源码进行修改。同时这里交代下没有实现的功能,那就是全屏的运行时切换。nehe的教程中提供了全屏切换的代码,但是代入之后总是无法执行成功。试着改动一些地方也没有办法。跟踪代码的话发现在app初始化完成之后,窗口有关的指针似乎沉入了引擎的深处。虽然试着改了一下,无奈在cocos2d上的功底实在不够,只能留在以后对引擎有更好的把握时再做了。

全屏里的另一个问题是这句话引起的:”The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?”。想我大N卡怎么可能会不支持全屏呢?于是仔细的研究了下这个问题。之后看了msdn也有对ChangeDisplaySettings()函数的详细说明。其实这个问题主要出在提供的分辨率并不是显卡和显示设备支持的。通常被支持的分辨率都是我们常见的那几种,像是800x600、1366x768这些设置给显卡就不会出错。回过头来看的话,我们平时玩的游戏其实在分辨率设置的时候也是只提供这些分辨率的,而不会让用户自己去输入分辨率。

另一个处理显示时遇到的问题是锁定鼠标。虽然到最后并没有用到,但是还是在这里记录一下,在win32 api中,只要调用

 CRect rect;
         this->GetWindowRect(&rect);

然后使用ClipCursor函数把鼠标控制在这个范围以内,这个函数的功能就是控制鼠标的范围。

ClipCursor(&rect);

而要释放鼠标控制就调用:

ClipCursor(NULL);

CCMenu的处理,我一开始天真的以为只要改动CCEGlView里的事件封装机制就可以了。

    case WM_MOUSEMOVE:
#if(_MSC_VER >= 1600)
        // Don't process message generated by Windows Touch
        if (m_bSupportTouch && (s_pfGetMessageExtraInfoFunction() & MOUSEEVENTF_FROMTOUCH) == MOUSEEVENTF_FROMTOUCH) break;
#endif
        if (
			true
			//MK_LBUTTON == wParam && m_bCaptured //改动了这里,去掉了判定,直接传输事件
			)
        {
            POINT point = {(short)LOWORD(lParam), (short)HIWORD(lParam)};
            CCPoint pt(point.x, point.y);
            int id = 0;
            pt.x /= m_fFrameZoomFactor;
            pt.y /= m_fFrameZoomFactor;
            handleTouchesMove(1, &id, &pt.x, &pt.y);
        }
        break;

可是改动之后移动鼠标,Menu依然不动。无奈之下只能跟踪事件的封装和发布过程,才终于找到了解决的方法。于是一路顺着事件机制改过去。之前的改动一直都只限于win32目录,所以可以比较随意的改。再往上走需要修改的是CCEGLViewProtocol.cpp 和CCTouchDispatcher.cpp,为了避免对其他平台的影响,就必须借助CC_TARGET_PLATFORM宏的作用了。

//CCEGLViewProtocol.cpp --> void CCEGLViewProtocol::handleTouchesMove(int num, int ids[], float xs[], float ys[]) 函数替换对应内容
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 )
			int nUnusedIndex = 0;

            CCTouch* pTouch = new CCTouch();
			pTouch->setTouchInfo(nUnusedIndex, (x - m_obViewPortRect.origin.x) / m_fScaleX, 
                                     (y - m_obViewPortRect.origin.y) / m_fScaleY);

            //CCLOG("x = %f y = %f", pTouch->getLocationInView().x, pTouch->getLocationInView().y);

            set.addObject(pTouch);

#else
        CCInteger* pIndex = (CCInteger*)s_TouchesIntergerDict.objectForKey(id);
        if (pIndex == NULL) {
            CCLOG("if the index doesn't exist, it is an error");
            continue;
        }

        CCLOGINFO("Moving touches with id: %d, x=%f, y=%f", id, x, y);
        CCTouch* pTouch = s_pTouches[pIndex->getValue()];
        if (pTouch)
        {
			pTouch->setTouchInfo(pIndex->getValue(), (x - m_obViewPortRect.origin.x) / m_fScaleX, 
								(y - m_obViewPortRect.origin.y) / m_fScaleY);

            set.addObject(pTouch);
        }
        else
        {
            // It is error, should return.
            CCLOG("Moving touches with id: %d error", id);
            return;
        }
#endif

可以看到,在这个CCEglView的protocol中,设计了机制保证只有touch begin之后才会有move事件。可以极好的提高在移动设备上的效率,但是却损失了windows上的常用事件。所以我们只要将事件重新进行发送就可以了。

        //CCTouchDispatcher.cpp  -->   void CCTouchDispatcher::touches(CCSet *pTouches, CCEvent *pEvent, unsigned int uIndex)
               if (uIndex == CCTOUCHBEGAN)
                {
                    bClaimed = pHandler->getDelegate()->ccTouchBegan(pTouch, pEvent);

                    if (bClaimed)
                    {
                        pHandler->getClaimedTouches()->addObject(pTouch);
                    }
                } else
                if (pHandler->getClaimedTouches()->containsObject(pTouch))
                {
                    // moved ended canceled
                    bClaimed = true;

                    switch (sHelper.m_type)
                    {
                    case CCTOUCHMOVED:
                        pHandler->getDelegate()->ccTouchMoved(pTouch, pEvent);
                        break;
                    case CCTOUCHENDED:
                        pHandler->getDelegate()->ccTouchEnded(pTouch, pEvent);
                        pHandler->getClaimedTouches()->removeObject(pTouch);
                        break;
                    case CCTOUCHCANCELLED:
                        pHandler->getDelegate()->ccTouchCancelled(pTouch, pEvent);
                        pHandler->getClaimedTouches()->removeObject(pTouch);
                        break;
                    }
                }
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 )
				else{
					if(CCTOUCHMOVED == sHelper.m_type){
						pHandler->getDelegate()->ccTouchMoved(pTouch, pEvent);
					}
				}

#endif

可以看到上面实现了touch事件中的claim机制,为了不破坏这些机制,只在最后加上自己的事件转发。

做了上面的改动之后,本以为能够实现mouse move的事件效果了。可是却依然没有,这时候检查CCMenu的实现,发现仍然有m_eState的保护标志在对事件处理进行控制。为了保证引擎在其他平台上的工作,在工程中便直接继承CCMenu实现自己的MouseMenu了。这样可以方便添加自己的效果,比如播放声音之类的。但是最后却发现犯了一个错误,那就是在menu的touch move中有断言。这样的话所有使用CCMenu的代码都会出错,不过自己还是偷了个懒,没有再去动CCMenu。其实改动起来很简单,像下面的那样处理就差不多了。主要电脑运行速度比较慢,每次改动库内部文件一点,就会触发很多东西的重新生成,实在是太消耗时间了。

//这不是对CCMenu的修改,而是MouseMenu的一部分,不过其实我们只需要在win32时去掉断言就可以了。
void MouseMenu::ccTouchMoved(CCTouch* touch, CCEvent* event)
{
    CC_UNUSED_PARAM(event);

#if (CC_TARGET_PLATFORM != CC_PLATFORM_WIN32 )
    CCAssert(m_eState == kCCMenuStateTrackingTouch, "[Menu ccTouchMoved] -- invalid state");
#endif

    CCMenuItem *currentItem = this->itemForTouch(touch);
	if(NULL != currentItem){
		if (currentItem != m_pSelectedItem ) 
		{
			SoundManager::sharedSoundManager()->PlayHitSFX();
			if (m_pSelectedItem)
			{
				m_pSelectedItem->unselected();
			}
			m_pSelectedItem = currentItem;
			if (m_pSelectedItem)
			{
				m_pSelectedItem->selected();
			}
		}
	}else{
			if (m_pSelectedItem)
			{
				SoundManager::sharedSoundManager()->PlayLaserSFX();
				m_pSelectedItem->unselected();
				m_pSelectedItem = NULL;
			}
	}
}

到了这里终于可以看到熟悉的菜单效果了,而且还可以播放效果音。

所有修改过的代码打包在这里:http://pan.baidu.com/share/link?shareid=153192&uk=1913549955&third=0