強行在MFC窗體中渲染Cocos2d-x 3.6

【前言】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*的轉換。

相關文章
相關標籤/搜索