【前言】html
把Cocos2dx渲染到另外一個應用程序框架中的方法,在2.x時代有不少大神已經實現了,而3.x的作法網上幾乎找不着。這兩天抽空強行折騰了一下,不敢獨享,貼出來供你們參考。git
【已知存在的問題】github
程序退出時會發生很是嚴重的內存泄漏,博主檢查了好久,但技術不夠暫時沒法解決。若是有大神能搞定,求告知一下作法,謝謝!app
在程序從開始運行到關閉期間,有且僅有一個cocos2dx窗體存在時能夠選擇性無視內存泄漏。若是很是在乎這一點,建議使用cocos2d-x 2.2.6這個版本,放在MFC中的內存泄漏很小。框架
*使用VLD檢查泄漏會報錯編輯器
【爲何要這麼作】ide
在進行遊戲開發途中,多多少少會用到一些輔助工具,好比CocosStudio。可是在更多的時候,CocosStudio並不能以不變應萬變(好比在博文《我用Cocos2d-x製做〈Love Live!學院偶像祭〉的Live場景》中提到的譜面編輯器的功能,CocoStudio沒法作到)。在這種狀況下,開發人員就須要一款針對當前項目而設計的工具。函數
若是輔助工具須要提供豐富的界面和控件,純用Cocos2d-x來製做就會十分雞肋。好比這個打開文件的控件:工具
固然,必定要作的話用cocos2dx也是能夠作的,可是至關麻煩。若是有興趣能夠本身嘗試寫一下,提升本身的姿式水平。oop
因此這個時候應當把cocos2dx層放在一個提供了各類控件的應用程序框架裏面,cocos2dx僅用於作顯示,其他的數據操做交由框架完成。
目前博主比較熟悉的框架是MFC和C# Winform。說實話C# Winform作窗體比MFC方便快捷太多。可是若是使用C# Winform就得去作C#調用C++,同時對於某些特定參數(好比string到const char*的轉換)必須作特殊處理,比較麻煩,不然DLL堆棧會出錯。而MFC不存在這個問題。
【核心思想】
Cocos2dx在Windows上運行起來是一個窗口,那麼在其內部必定調用了CreateWindowEx這個API。那麼只要咱們找到這個API,把參數設爲子窗口,並把父窗口的句柄傳進去,就能夠達到要求。建立出來的窗體就是父窗體中的子窗體了。
還要注意一點是cocos2dx原生程序有一個本身的消息循環,若是直接調用Application::run會致使MFC層卡死,咱們須要把消息循環交給框架的主線程來操做。
流程圖以下:
【須要的工具】
一、 安裝了MFC組件的Visual Studio 2013
二、 Cocos2d-x 3.6
三、 GLFW (下載地址:點我)
四、 CMake(下載地址:點我)
【操做步驟】
一、 建立項目
建立一個MFC項目(我使用的對話框型)。注意在嚮導中「MFC的使用」這一項要選擇「在共享DLL中使用MFC」:
二、 拷貝必要文件
把cocos2dx的源碼和模板項目中的Classes和Resources文件夾拷貝到項目目錄下(項目模板位於引擎目錄\templates\cpp-template-default下),必定要使用這個結構:
三、 修改項目屬性
打開MFC項目解決方案,在屬性管理器(視圖——屬性管理器)中爲項目添加cocos2dx的兩個屬性表。屬性表位於解決方案目錄\cocos2d\cocos\2d:
而後將libcocos2d,libbox2d,libspine加入解決方案中,並把libcocos2d設爲MFC項目的依賴項:
再在MFC項目的附加包含目錄中加入:
$(EngineRoot)cocos\audio\include
$(EngineRoot)external
$(EngineRoot)external\chipmunk\include\chipmunk
$(EngineRoot)extensions
..\Classes
..
%(AdditionalIncludeDirectories)
$(_COCOS_HEADER_WIN32_BEGIN)
$(_COCOS_HEADER_WIN32_END)
預處理器定義中加入:
COCOS2D_DEBUG=1
附加庫目錄中加入:
$(_COCOS_LIB_PATH_WIN32_BEGIN)
$(_COCOS_LIB_PATH_WIN32_END)
附加依賴項加入:
$(_COCOS_LIB_WIN32_BEGIN)
$(_COCOS_LIB_WIN32_END)
libcocos2d.lib
再修改項目屬性——工做目錄,以及生成目錄:
再將Classes下的全部文件加入MFC項目:
最後設置不使用預編譯頭,否則每加入一個類都得加上#include 「stdafx.h」,麻煩:
四、 修改GLFW
Cocos2dx 2.x中建立窗口在CCEGLView類中完成,直接修改它就行。到3.x後使用glfw管理窗口,CreateWindowEx被封裝進去了。而cocos2dx並無附帶glfw的源碼,只有頭文件和lib文件。因此咱們須要下載glfw的源碼進行修改。
用CMakeGUI打開GLFW,source code處選擇下下來的glfw解壓的文件夾,build the binaries選擇生成解決方案的文件夾,而後生成對應VS版本的解決方案(glfw解壓的文件夾不要刪除):
而後打開生成的sln,查找CreateWindowEx,修改它所在的函數(win32_window.c,633行):
static int createWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, HWND parent) // 父窗體句柄 { int xpos, ypos, fullWidth, fullHeight; WCHAR* wideTitle; window->win32.dwStyle = WS_CHILDWINDOW | WS_VISIBLE; // 子窗體樣式 window->win32.dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; xpos = 0; ypos = 0; fullWidth = wndconfig->width; fullHeight = wndconfig->height; wideTitle = _glfwCreateWideStringFromUTF8(wndconfig->title); if (!wideTitle) { _glfwInputError(GLFW_PLATFORM_ERROR, "Win32: Failed to convert window title to UTF-16"); return GL_FALSE; } window->win32.handle = CreateWindowExW(window->win32.dwExStyle, _GLFW_WNDCLASSNAME, wideTitle, window->win32.dwStyle, xpos, ypos, fullWidth, fullHeight, parent, // 傳入父窗體句柄 NULL, // No window menu GetModuleHandleW(NULL), window); // Pass object to WM_CREATE // // ... }
而後從內向外依次修改調用它的地方:
win32_window.c,769行
int _glfwPlatformCreateWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, HWND parent) { // ... // if (!createWindow(window, wndconfig, ctxconfig, fbconfig, parent)) return GL_FALSE; // ... // if (!createWindow(window, wndconfig, ctxconfig, fbconfig, parent)) return GL_FALSE; // // ... }
internal.h,524行
int _glfwPlatformCreateWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, HWND parent);
window.c,116行
GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share, int parent) { // ... // if (!_glfwPlatformCreateWindow(window, &wndconfig, &ctxconfig, &fbconfig, (HWND)parent)) // // ... }
glfw3.h,1645行:
GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share, int parent);
改好後使用MinSizeRel選項進行編譯,編譯好後在GLFW解決方案目錄\src\MinSizeRel下找到glfw3.lib文件,連同glfw3.h(在glfw解壓目錄\include\GLFW)一塊兒,分別放入MFC項目解決方案目錄\cocos2d\external\glfw3\prebuilt\win32 和 MFC項目解決方案目錄\cocos2d\external\glfw3\include\win32下覆蓋原文件。
五、 修改Cocos層
在GLViewImpl類(3.2中是GLView類)的頭文件中加入一個方法和成員:
public: static void SetParent(HWND parent){ m_sParent = parent; } private: static HWND m_sParent;
別忘了在cpp中加入
HWND GLViewImpl::m_sParent = NULL;
而後修改GLViewImpl::initWithRect方法,修改調用glfwCreateWindow的地方:
bool GLViewImpl::initWithRect(const std::string& viewName, Rect rect, float frameZoomFactor) { // ... // _mainWindow = glfwCreateWindow(rect.size.width * _frameZoomFactor, rect.size.height * _frameZoomFactor, _viewName.c_str(), _monitor, nullptr, (int)m_sParent); // 傳入父窗口句柄 // // ... }
修改Application類的run方法,去掉裏面的消息循環:
int Application::run() { PVRFrameEnableControlWindow(false); initGLContextAttrs(); // Initialize instance and cocos2d. if (!applicationDidFinishLaunching()) { return 1; } // Retain glview to avoid glview being released in the while loop Director::getInstance()->getOpenGLView()->retain(); return 0; }
六、 編輯MFC窗體
接下來在MFC窗體中添加一個Picture Control控件,控件ID設爲IDC_RENDERWND,而後選中控件(很是蛋疼的是隻能在控件邊框處點擊才能選中)點右鍵——「添加變量」:
七、添加渲染類
在解決方案資源管理器中的MFC項目上點右鍵——「添加」——「類…」,添加一個MFC類:
而後修改類:
#pragma once // CRenderWnd class CRenderWnd : public CWnd { DECLARE_DYNAMIC(CRenderWnd) public: CRenderWnd(); virtual ~CRenderWnd(); protected: DECLARE_MESSAGE_MAP() public: afx_msg void OnTimer(UINT_PTR nIDEvent); afx_msg void OnDestroy(); public: void Initialize(); private: BOOL m_bInited; };
實現:
// RenderWnd.cpp : 實現文件 // #include "stdafx.h" #include "Cocos2dxMFC.h" #include "RenderWnd.h" #include "cocos2d.h" #include "AppDelegate.h" // CRenderWnd IMPLEMENT_DYNAMIC(CRenderWnd, CWnd) CRenderWnd::CRenderWnd() : m_bInited(FALSE) { } CRenderWnd::~CRenderWnd() { } BEGIN_MESSAGE_MAP(CRenderWnd, CWnd) ON_WM_TIMER() ON_WM_DESTROY() END_MESSAGE_MAP() // CRenderWnd 消息處理程序 AppDelegate app; void CRenderWnd::Initialize() { cocos2d::GLViewImpl::SetParent(this->GetSafeHwnd()); cocos2d::Application::getInstance()->run(); this->m_bInited = TRUE; SetTimer(1, 1, NULL); } void CRenderWnd::OnTimer(UINT_PTR nIDEvent) { if (this->m_bInited) { auto director = cocos2d::Director::getInstance(); director->mainLoop(); director->getOpenGLView()->pollEvents(); CWnd::OnTimer(nIDEvent); } } void CRenderWnd::OnDestroy() { CWnd::OnDestroy(); if (this->m_bInited) { auto director = cocos2d::Director::getInstance(); director->getOpenGLView()->release(); director->end(); director->mainLoop(); this->m_bInited = FALSE; } }
而後將剛纔綁定的控件m_RenderWnd的類型由CStatic改成CRenderWnd,並在主窗體的OnInitDialog方法中加入一行:
BOOL CCocos2dxMFCDlg::OnInitDialog() { // ... // // TODO: 在此添加額外的初始化代碼 this->m_RenderWnd.Initialize(); return TRUE; // 除非將焦點設置到控件,不然返回 TRUE }
八、運行起來
理論上要作的操做已經作完了,如今只須要編譯就能運行起來。然而觸控會這麼好心地作好事不留坑嘛?
固然不會了~傳說cocos系列的坑連起來能夠繞地球多少圈來着,這裏噗通一下就入坑了,不信你F5一下:
這什麼鬼?!實際上是ApplicationProtocol中Platform枚舉中的一個值和MFC的某個宏同名了。解決方法是在stdafx.h中加入這樣一句:
#undef OS_WINDOWS
而後繼續編譯。固然是坑不單行,又報錯:
不過這個簡單,根據報錯內容,在項目的預處理器定義中加入_CRT_SECURE_NO_WARNINGS。
按理說最後是否是應該出現一個BOSS級深坑來着?BOSS來了:此時編譯能夠經過了,可是一運行必然報錯。看看輸出窗口:
嗷,原來是找不到文件。可是咱們以前已經設置了工做目錄,Resources下面也有文件啊(這個坑在2.2.6中並無)。
從Label::createWithTTF一路追蹤下去,最後發現cocos2dx搜索文件的目錄是在這裏設置的(CCFileUtils-win32.cpp 59行):
static void _checkPath() { if (0 == s_resourcePath.length()) { WCHAR *pUtf16ExePath = nullptr; _get_wpgmptr(&pUtf16ExePath); // We need only directory part without exe WCHAR *pUtf16DirEnd = wcsrchr(pUtf16ExePath, L'\\'); char utf8ExeDir[CC_MAX_PATH] = { 0 }; int nNum = WideCharToMultiByte(CP_UTF8, 0, pUtf16ExePath, pUtf16DirEnd-pUtf16ExePath+1, utf8ExeDir, sizeof(utf8ExeDir), nullptr, nullptr); s_resourcePath = convertPathFormatToUnixStyle(utf8ExeDir); } }
_get_wpgmptr是個嘛玩意?查一下能夠知道,這個函數用於取得進程exe所在的目錄。
咱們再看看cocos2dx 2.2.6中對應的部分(CCFileUtilsWin32.cpp 34行):
static void _checkPath() { if (! s_pszResourcePath[0]) { WCHAR wszPath[MAX_PATH] = {0}; int nNum = WideCharToMultiByte(CP_ACP, 0, wszPath, GetCurrentDirectoryW(sizeof(wszPath), wszPath), s_pszResourcePath, MAX_PATH, NULL, NULL); s_pszResourcePath[nNum] = '\\'; } }
很明顯,2.2.6中使用GetCurrentDirectoryW獲取當前目錄的,使用這個函數就能獲取正確的工做目錄了。爲何用cocos new出來的3.6項目沒這個問題?由於new出來的項目的預連接事件中最後有這麼一句:
這個命令會把Resources下的全部文件拷貝到輸出目錄(也就是進程exe所在的目錄)下,天然不會出現找不到文件的問題了。
不知道這麼作的意義和目的是什麼?可是此時我想說:
我還想說:
修改的方法很簡單,參考2.2.6把_checkPath中_get_wpgmptr函數改成GetCurrentDirectoryW:
static void _checkPath() { if (0 == s_resourcePath.length()) { char pathBuffer[MAX_PATH] = { 0 }; WCHAR wszPath[MAX_PATH] = { 0 }; int nNum = WideCharToMultiByte(CP_ACP, 0, wszPath, GetCurrentDirectory(sizeof(wszPath), wszPath), pathBuffer, MAX_PATH, NULL, NULL); pathBuffer[nNum] = '\\'; s_resourcePath = pathBuffer; } }
⑨、最後的小修改
若是你用的MFC窗體是一個Dialog類型的,運行後會發現按回車或Esc後窗體直接關閉了。因此還須要屏蔽掉回車和Esc鍵的響應。在MFC對話框類中添加一個方法重寫PreTranslateMessage:
private: virtual BOOL PreTranslateMessage(MSG* pMsg);
實現:
BOOL CCocos2dxMFCDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_KEYDOWN) { if (pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_RETURN) { return TRUE; } } return CDialogEx::PreTranslateMessage(pMsg); }
【運行起來】
若是編譯沒有出錯的話,運行起來會看到這個樣子:
只要將接口留出來,就能夠很方便地經過MFC層的控件來控制cocos層了。至於要作成一個什麼樣的工具,全靠你們發揮咯~
【後記】
採用這套思路理論上能夠把cocos渲染到任何一個支持調用C++層代碼的框架中。
須要渲染在C# Winform中的童鞋請看這篇博客,裏面有講處理方法及string到const char*的轉換。