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


【前言】

  把Cocos2dx渲染到另一個應用程序框架中的方法,在2.x時代有很多大神已經實現了,而3.x的做法網上幾乎找不着。這兩天抽空強行折騰了一下,不敢獨享,貼出來供大家參考。

 


【已知存在的問題】

  程序退出時會發生非常嚴重的內存泄漏,博主檢查了很久,但技術不夠暫時無法解決。如果有大神能搞定,求告知一下做法,謝謝!

  在程序從開始運行到關閉期間,有且僅有一個cocos2dx窗體存在時可以選擇性無視內存泄漏。如果非常在意這一點,建議使用cocos2d-x 2.2.6這個版本,放在MFC中的內存泄漏很小。

  *使用VLD檢查泄漏會報錯

 


【為什么要這么做】

  在進行游戲開發途中,多多少少會用到一些輔助工具,比如CocosStudio。但是在更多的時候,CocosStudio並不能以不變應萬變(比如在博文《我用Cocos2d-x制作〈Love Live!學院偶像祭〉的Live場景》中提到的譜面編輯器的功能,CocoStudio無法做到)。在這種情況下,開發人員就需要一款針對當前項目而設計的工具。

  如果輔助工具需要提供豐富的界面和控件,純用Cocos2d-x來制作就會十分雞肋。比如這個打開文件的控件:

  

  當然,一定要做的話用cocos2dx也是可以做的,但是相當麻煩。如果有興趣可以自己嘗試寫一下,提高自己的姿勢水平。

  所以這個時候應當把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層卡死,我們需要把消息循環交給框架的主線程來操作。

  流程圖如下:

  


【需要的工具】

  1、    安裝了MFC組件的Visual Studio 2013

  2、    Cocos2d-x 3.6

  3、    GLFW (下載地址:點我

  4、    CMake(下載地址:點我

 


【操作步驟】

  1、    創建項目

    創建一個MFC項目(我使用的對話框型)。注意在向導中“MFC的使用”這一項要選擇“在共享DLL中使用MFC”:

    

 

  2、    拷貝必要文件

    把cocos2dx的源碼和模板項目中的Classes和Resources文件夾拷貝到項目目錄下(項目模板位於引擎目錄\templates\cpp-template-default下),一定要使用這個結構:

    

 

  3、    修改項目屬性

    打開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”,麻煩:

    

 

  4、 修改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下覆蓋原文件。

 

  5、    修改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;
}

  

  6、   編輯MFC窗體

    接下來在MFC窗體中添加一個Picture Control控件,控件ID設為IDC_RENDERWND,然后選中控件(非常蛋疼的是只能在控件邊框處點擊才能選中)點右鍵——“添加變量”:

    

 

  7、添加渲染類

    在解決方案資源管理器中的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
} 

 

  8、運行起來

    理論上要做的操作已經做完了,現在只需要編譯就能運行起來。然而觸控會這么好心地做好事不留坑嘛?

    當然不會了~傳說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*的轉換。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM