• (转载)强行在MFC窗体中渲染Cocos2d-x 3.6


    强行在MFC窗体中渲染Cocos2d-x 3.6

    GuyaWeiren2015-06-29 15:14:063696 次阅读

    【前言】

    把Cocos2d-x渲染到另一个应用程序框架中的方法,在2.x时代有很多大神已经实现了,而3.x的做法网上几乎找不着。这两天抽空强行折腾了一下,不敢独享,贴出来供大家参考。

    【已知存在的问题】

    程序退出时会发生非常严重的内存泄漏,博主检查了很久,但技术不够暂时无法解决。如果有大神能搞定,求告知一下做法,谢谢!

    在程序从开始运行到关闭期间,有且仅有一个Cocos2d-x窗体存在时可以选择性无视内存泄漏。如果非常在意这一点,建议使用Cocos2d-x 2.2.6这个版本,放在MFC中的内存泄漏很小。

    *使用VLD检查泄漏会报错

    【为什么要这么做】

    在进行游戏开发途中,多多少少会用到一些辅助工具,比如Cocos Studio。但是在更多的时候,Cocos Studio并不能以不变应万变(比如在博文《我用Cocos2d-x制作〈Love Live!学院偶像祭〉的Live场景》中提到的谱面编辑器的功能,Cocos Studio无法做到)。在这种情况下,开发人员就需要一款针对当前项目而设计的工具。

    如果辅助工具需要提供丰富的界面和控件,纯用Cocos2d-x来制作就会十分鸡肋。比如这个打开文件的控件:

    blob.png

    当然,一定要做的话用Cocos2d-x也是可以做的,但是相当麻烦。如果有兴趣可以自己尝试写一下,提高自己的姿势水平。

    所以这个时候应当把Cocos2d-x层放在一个提供了各种控件的应用程序框架里面,Cocos2d-x仅用于做显示,其余的数据操作交由框架完成。

    目前博主比较熟悉的框架是MFC和C# Winform。说实话C# Winform做窗体比MFC方便快捷太多。但是如果使用C# Winform就得去做C#调用C++,同时对于某些特定参数(比如string到const char*的转换)必须做特殊处理,比较麻烦,否则DLL堆栈会出错。而MFC不存在这个问题。

    【核心思想】

    Cocos2d-x在Windows上运行起来是一个窗口,那么在其内部一定调用了CreateWindowEx这个API。那么只要我们找到这个API,把参数设为子窗口,并把父窗口的句柄传进去,就可以达到要求。创建出来的窗体就是父窗体中的子窗体了。

    还要注意一点是Cocos2d-x原生程序有一个自己的消息循环,如果直接调用Application::run会导致MFC层卡死,我们需要把消息循环交给框架的主线程来操作。

    流程图如下:

    blob.png

    【需要的工具】

    1、    安装了MFC组件的Visual Studio 2013

    2、    Cocos2d-x 3.6

    3、    GLFW (下载地址:点我

    4、    CMake(下载地址:点我

    【操作步骤】

    1、创建项目

    创建一个MFC项目(我使用的对话框型)。注意在向导中“MFC的使用”这一项要选择“在共享DLL中使用MFC”:

    blob.png

    2、拷贝必要文件

    把Cocos2d-xx的源码和模板项目中的Classes和Resources文件夹拷贝到项目目录下(项目模板位于引擎目录 emplatescpp-template-default下),一定要使用这个结构:

    blob.png

    3、修改项目属性

    打开MFC项目解决方案,在属性管理器(视图——属性管理器)中为项目添加Cocos2d-x的两个属性表。属性表位于解决方案目录cocos2dcocos2d:

    blob.png

    然后将libcocos2d,libbox2d,libspine加入解决方案中,并把libcocos2d设为MFC项目的依赖项:

    blob.png

    再在MFC项目的附加包含目录中加入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $(EngineRoot)cocosaudioinclude
    $(EngineRoot)external
    $(EngineRoot)externalchipmunkincludechipmunk
    $(EngineRoot)extensions
    ..Classes
    ..
    %(AdditionalIncludeDirectories)
    $(_COCOS_HEADER_WIN32_BEGIN)
    $(_COCOS_HEADER_WIN32_END)

    预处理器定义中加入:

    1
    COCOS2D_DEBUG=1

    附加库目录中加入:

    1
    2
    $(_COCOS_LIB_PATH_WIN32_BEGIN)
    $(_COCOS_LIB_PATH_WIN32_END)

    附加依赖项加入:

    1
    2
    3
    $(_COCOS_LIB_WIN32_BEGIN)
    $(_COCOS_LIB_WIN32_END)
    libcocos2d.lib

    再修改项目属性——工作目录,以及生成目录:

    blob.png

    再将Classes下的所有文件加入MFC项目:

    blob.png

    最后设置不使用预编译头,不然每加入一个类都得加上#include “stdafx.h”,麻烦:

    blob.png

    4、修改GLFW

    Cocos2d-x 2.x中创建窗口在CCEGLView类中完成,直接修改它就行。到3.x后使用glfw管理窗口,CreateWindowEx被封装进去了。而Cocos2d-x并没有附带glfw的源码,只有头文件和lib文件。所以我们需要下载glfw的源码进行修改。

    用CMakeGUI打开GLFW,source code处选择下下来的glfw解压的文件夹,build the binaries选择生成解决方案的文件夹,然后生成对应VS版本的解决方案(glfw解压的文件夹不要删除):

    blob.png

    然后打开生成的sln,查找CreateWindowEx,修改它所在的函数(win32_window.c,633行):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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行

    1
    2
    3
    4
    5
    int _glfwPlatformCreateWindow(_GLFWwindow* window,
                                  const _GLFWwndconfig* wndconfig,
                                  const _GLFWctxconfig* ctxconfig,
                                  const _GLFWfbconfig* fbconfig,
                                  HWND parent);

    window.c,116行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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行:

    1
    GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share, int parent); 

    改好后使用MinSizeRel选项进行编译,编译好后在GLFW解决方案目录srcMinSizeRel下找到glfw3.lib文件,连同glfw3.h(在glfw解压目录includeGLFW)一起,分别放入MFC项目解决方案目录cocos2dexternalglfw3prebuiltwin32 和 MFC项目解决方案目录cocos2dexternalglfw3includewin32下覆盖原文件。

    5、修改Cocos层

    在GLViewImpl类(3.2中是GLView类)的头文件中加入一个方法和成员:

    1
    2
    3
    4
    5
    public:
        static void SetParent(HWND parent){ m_sParent = parent; }
      
    private:
        static HWND m_sParent;

    别忘了在cpp中加入

    1
    HWND GLViewImpl::m_sParent = NULL;

    然后修改GLViewImpl::initWithRect方法,修改调用glfwCreateWindow的地方:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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方法,去掉里面的消息循环:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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,然后选中控件(非常蛋疼的是只能在控件边框处点击才能选中)点右键——“添加变量”:

    blob.png

    7、添加渲染类

    在解决方案资源管理器中的MFC项目上点右键——“添加”——“类…”,添加一个MFC类:

    blob.png

    blob.png

    然后修改类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #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;
    };

    实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    // 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方法中加入一行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    BOOL CCocos2dxMFCDlg::OnInitDialog()
    {
            // ...
            //
        // TODO:  在此添加额外的初始化代码
      
            this->m_RenderWnd.Initialize(); 
      
        return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
    } 

    8、运行起来

    理论上要做的操作已经做完了,现在只需要编译就能运行起来。然而触控会这么好心地做好事不留坑嘛?

    当然不会了~传说cocos系列的坑连起来可以绕地球多少圈来着,这里噗通一下就入坑了,不信你F5一下:

    blob.png

    这什么鬼?!其实是ApplicationProtocol中Platform枚举中的一个值和MFC的某个宏同名了。解决方法是在stdafx.h中加入这样一句:

    1
    #undef OS_WINDOWS

    然后继续编译。当然是坑不单行,又报错:

    blob.png

    不过这个简单,根据报错内容,在项目的预处理器定义中加入_CRT_SECURE_NO_WARNINGS。

    blob.png

    按理说最后是不是应该出现一个BOSS级深坑来着?BOSS来了:此时编译可以通过了,但是一运行必然报错。看看输出窗口:

    blob.png

    嗷,原来是找不到文件。但是我们之前已经设置了工作目录,Resources下面也有文件啊(这个坑在2.2.6中并没有)。

    从Label::createWithTTF一路追踪下去,最后发现Cocos2d-x搜索文件的目录是在这里设置的(CCFileUtils-win32.cpp 59行):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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所在的目录。

    我们再看看Cocos2d-x 2.2.6中对应的部分(CCFileUtilsWin32.cpp 34行):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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出来的项目的预链接事件中最后有这么一句:

    blob.png

    这个命令会把Resources下的所有文件拷贝到输出目录(也就是进程exe所在的目录)下,自然不会出现找不到文件的问题了。

    不知道这么做的意义和目的是什么?但是此时我想说:

    blob.png

    我还想说:

    blob.png

    修改的方法很简单,参考2.2.6把_checkPath中_get_wpgmptr函数改为GetCurrentDirectoryW:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
        }
    }

    9、最后的小修改

    如果你用的MFC窗体是一个Dialog类型的,运行后会发现按回车或Esc后窗体直接关闭了。所以还需要屏蔽掉回车和Esc键的响应。在MFC对话框类中添加一个方法重写PreTranslateMessage:

    1
    2
    private:
        virtual BOOL PreTranslateMessage(MSG* pMsg);

    实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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);
    }

    【运行起来】

    如果编译没有出错的话,运行起来会看到这个样子:

    1435562031638844.png

    只要将接口留出来,就可以很方便地通过MFC层的控件来控制cocos层了。至于要做成一个什么样的工具,全靠大家发挥咯~

    【后记】

    采用这套思路理论上可以把cocos渲染到任何一个支持调用C++层代码的框架中。

    需要渲染在C# Winform中的童鞋请看这篇博客,里面有讲处理方法及string到const char*的转换。

    来源网址:http://www.cnblogs.com/GuyaWeiren/p/4600937.html

  • 相关阅读:
    存储过程
    数据库中的锁
    数据库事务
    三大范式
    IOC(一)
    rabbitmq部署
    配置SQLServer2012,允许远程连接
    6种常见的Git错误以及解决的办法
    灵活使用Win+R快捷键提高工作效率
    sql 创建视图常用的几种sql函数
  • 原文地址:https://www.cnblogs.com/wodehao0808/p/4737321.html
Copyright © 2020-2023  润新知