【前言】

  把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行):

  1. static int createWindow(_GLFWwindow* window,
  2. const _GLFWwndconfig* wndconfig,
  3. const _GLFWctxconfig* ctxconfig,
  4. const _GLFWfbconfig* fbconfig,
  5. HWND parent) // 父窗体句柄
  6. {
  7. int xpos, ypos, fullWidth, fullHeight;
  8. WCHAR* wideTitle;
  9.  
  10. window->win32.dwStyle = WS_CHILDWINDOW | WS_VISIBLE; // 子窗体样式
  11. window->win32.dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
  12.  
  13. xpos = 0;
  14. ypos = 0;
  15.  
  16. fullWidth = wndconfig->width;
  17. fullHeight = wndconfig->height;
  18.  
  19. wideTitle = _glfwCreateWideStringFromUTF8(wndconfig->title);
  20. if (!wideTitle)
  21. {
  22. _glfwInputError(GLFW_PLATFORM_ERROR,
  23. "Win32: Failed to convert window title to UTF-16");
  24. return GL_FALSE;
  25. }
  26.  
  27. window->win32.handle = CreateWindowExW(window->win32.dwExStyle,
  28. _GLFW_WNDCLASSNAME,
  29. wideTitle,
  30. window->win32.dwStyle,
  31. xpos, ypos,
  32. fullWidth, fullHeight,
  33. parent, // 传入父窗体句柄
  34. NULL, // No window menu
  35. GetModuleHandleW(NULL),
  36. window); // Pass object to WM_CREATE
  37. //
  38. // ...
  39. }

    然后从内向外依次修改调用它的地方:

    win32_window.c,769行  

  1. int _glfwPlatformCreateWindow(_GLFWwindow* window,
  2. const _GLFWwndconfig* wndconfig,
  3. const _GLFWctxconfig* ctxconfig,
  4. const _GLFWfbconfig* fbconfig,
  5. HWND parent)
  6. {
  7. // ...
  8. //
  9. if (!createWindow(window, wndconfig, ctxconfig, fbconfig, parent))
  10. return GL_FALSE;
  11.  
  12. // ...
  13. //
  14. if (!createWindow(window, wndconfig, ctxconfig, fbconfig, parent))
  15. return GL_FALSE;
  16. //
  17. // ...
  18. }

    internal.h,524行

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

    window.c,116行

  1. GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height,
  2. const char* title,
  3. GLFWmonitor* monitor,
  4. GLFWwindow* share,
  5. int parent)
  6. {
  7. // ...
  8. //
  9. if (!_glfwPlatformCreateWindow(window, &wndconfig, &ctxconfig, &fbconfig, (HWND)parent))
  10. //
  11. // ...
  12. }

    glfw3.h,1645行:

  1. 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类)的头文件中加入一个方法和成员:

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

    别忘了在cpp中加入

  1. HWND GLViewImpl::m_sParent = NULL;

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

  1. bool GLViewImpl::initWithRect(const std::string& viewName, Rect rect, float frameZoomFactor)
  2. {
  3. // ...
  4. //
  5. _mainWindow = glfwCreateWindow(rect.size.width * _frameZoomFactor,
  6. rect.size.height * _frameZoomFactor,
  7. _viewName.c_str(),
  8. _monitor,
  9. nullptr,
  10. (int)m_sParent); // 传入父窗口句柄
  11. //
  12. // ...
  13. }

    修改Application类的run方法,去掉里面的消息循环:

  1. int Application::run()
  2. {
  3. PVRFrameEnableControlWindow(false);
  4.  
  5. initGLContextAttrs();
  6.  
  7. // Initialize instance and cocos2d.
  8. if (!applicationDidFinishLaunching())
  9. {
  10. return 1;
  11. }
  12.  
  13. // Retain glview to avoid glview being released in the while loop
  14. Director::getInstance()->getOpenGLView()->retain();
  15.  
  16. return 0;
  17. }

  

  6、   编辑MFC窗体

    接下来在MFC窗体中添加一个Picture Control控件,控件ID设为IDC_RENDERWND,然后选中控件(非常蛋疼的是只能在控件边框处点击才能选中)点右键——“添加变量”:

    

  7、添加渲染类

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

    

    

    然后修改类:

  1. #pragma once
  2.  
  3. // CRenderWnd
  4.  
  5. class CRenderWnd : public CWnd
  6. {
  7. DECLARE_DYNAMIC(CRenderWnd)
  8.  
  9. public:
  10. CRenderWnd();
  11. virtual ~CRenderWnd();
  12.  
  13. protected:
  14. DECLARE_MESSAGE_MAP()
  15. public:
  16. afx_msg void OnTimer(UINT_PTR nIDEvent);
  17. afx_msg void OnDestroy();
  18.  
  19. public:
  20. void Initialize();
  21.  
  22. private:
  23. BOOL m_bInited;
  24. };

    实现:

  1. // RenderWnd.cpp : 实现文件
  2. //
  3.  
  4. #include "stdafx.h"
  5. #include "Cocos2dxMFC.h"
  6. #include "RenderWnd.h"
  7.  
  8. #include "cocos2d.h"
  9. #include "AppDelegate.h"
  10.  
  11. // CRenderWnd
  12.  
  13. IMPLEMENT_DYNAMIC(CRenderWnd, CWnd)
  14.  
  15. CRenderWnd::CRenderWnd()
  16. : m_bInited(FALSE)
  17. {
  18.  
  19. }
  20.  
  21. CRenderWnd::~CRenderWnd()
  22. {
  23. }
  24.  
  25. BEGIN_MESSAGE_MAP(CRenderWnd, CWnd)
  26. ON_WM_TIMER()
  27. ON_WM_DESTROY()
  28. END_MESSAGE_MAP()
  29.  
  30. // CRenderWnd 消息处理程序
  31.  
  32. AppDelegate app;
  33. void CRenderWnd::Initialize()
  34. {
  35. cocos2d::GLViewImpl::SetParent(this->GetSafeHwnd());
  36. cocos2d::Application::getInstance()->run();
  37.  
  38. this->m_bInited = TRUE;
  39. SetTimer(1, 1, NULL);
  40. }
  41.  
  42. void CRenderWnd::OnTimer(UINT_PTR nIDEvent)
  43. {
  44. if (this->m_bInited)
  45. {
  46. auto director = cocos2d::Director::getInstance();
  47. director->mainLoop();
  48. director->getOpenGLView()->pollEvents();
  49.  
  50. CWnd::OnTimer(nIDEvent);
  51. }
  52. }
  53.  
  54. void CRenderWnd::OnDestroy()
  55. {
  56. CWnd::OnDestroy();
  57.  
  58. if (this->m_bInited)
  59. {
  60. auto director = cocos2d::Director::getInstance();
  61. director->getOpenGLView()->release();
  62. director->end();
  63. director->mainLoop();
  64.  
  65. this->m_bInited = FALSE;
  66. }
  67. }

    然后将刚才绑定的控件m_RenderWnd的类型由CStatic改为CRenderWnd,并在主窗体的OnInitDialog方法中加入一行:

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

  8、运行起来

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

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

    

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

  1. #undef OS_WINDOWS

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

    

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

    

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

    

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

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

  1. static void _checkPath()
  2. {
  3. if (0 == s_resourcePath.length())
  4. {
  5. WCHAR *pUtf16ExePath = nullptr;
  6. _get_wpgmptr(&pUtf16ExePath);
  7.  
  8. // We need only directory part without exe
  9. WCHAR *pUtf16DirEnd = wcsrchr(pUtf16ExePath, L'\\');
  10.  
  11. char utf8ExeDir[CC_MAX_PATH] = { 0 };
  12. int nNum = WideCharToMultiByte(CP_UTF8, 0, pUtf16ExePath, pUtf16DirEnd-pUtf16ExePath+1, utf8ExeDir, sizeof(utf8ExeDir), nullptr, nullptr);
  13.  
  14. s_resourcePath = convertPathFormatToUnixStyle(utf8ExeDir);
  15. }
  16. }

    _get_wpgmptr是个嘛玩意?查一下可以知道,这个函数用于取得进程exe所在的目录。

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

  1. static void _checkPath()
  2. {
  3. if (! s_pszResourcePath[0])
  4. {
  5. WCHAR wszPath[MAX_PATH] = {0};
  6. int nNum = WideCharToMultiByte(CP_ACP, 0, wszPath,
  7. GetCurrentDirectoryW(sizeof(wszPath), wszPath),
  8. s_pszResourcePath, MAX_PATH, NULL, NULL);
  9. s_pszResourcePath[nNum] = '\\';
  10. }
  11. }  

    很明显,2.2.6中使用GetCurrentDirectoryW获取当前目录的,使用这个函数就能获取正确的工作目录了。为什么用cocos new出来的3.6项目没这个问题?因为new出来的项目的预链接事件中最后有这么一句:

  

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

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

    

    我还想说:

    

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

  1. static void _checkPath()
  2. {
  3. if (0 == s_resourcePath.length())
  4. {
  5. char pathBuffer[MAX_PATH] = { 0 };
  6. WCHAR wszPath[MAX_PATH] = { 0 };
  7. int nNum = WideCharToMultiByte(CP_ACP, 0, wszPath,
  8. GetCurrentDirectory(sizeof(wszPath), wszPath),
  9. pathBuffer, MAX_PATH, NULL, NULL);
  10. pathBuffer[nNum] = '\\';
  11.  
  12. s_resourcePath = pathBuffer;
  13. }
  14. }

  

  ⑨、最后的小修改

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

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

  实现:

  1. BOOL CCocos2dxMFCDlg::PreTranslateMessage(MSG* pMsg)
  2. {
  3. if (pMsg->message == WM_KEYDOWN)
  4. {
  5. if (pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_RETURN)
  6. {
  7. return TRUE;
  8. }
  9. }
  10. return CDialogEx::PreTranslateMessage(pMsg);
  11. }

 


【运行起来】

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

  

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


【后记】

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

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

强行在MFC窗体中渲染Cocos2d-x 3.6的更多相关文章

  1. 【续】强行在C# Winform中渲染Cocos2d-x 3.6

    [前言] 上一篇讲了怎么把Cocos2d-x 3.6渲染进MFC窗体,这里来讲一下怎么在C# Winform中做到同样的功能.如果你不熟悉MFC的使用但对C# Winform比较在行,请往下看. 这一 ...

  2. 《MFC dialog中加入OpenGL窗体》

    <MFC dialog中加入OpenGL窗体> 最近学习了如何在MFC对话框程序中加入OpenGL窗体的方法,在这里将自己的实现过程归纳一下. 步骤零: 加入PictureControl控 ...

  3. MFC窗体程序中添加调试控制台

    在编写复杂程序的过程中,我们经常需要将一些信息输出到文件或者屏幕上.较控制台应用程序,MFC窗体程序要显得麻烦一些! 下面有2种方法来实现为MFC窗体程序添加调试控制台,方便程序员调试程序和了解当前程 ...

  4. MFC 如何在一个窗体中嵌套在另一个窗体中

    其中的一个方法是讲子窗体设置为非模式对话框,具体操作为 :设置子窗体的border属性为none,style为 child. 在父窗体中需要用create来实现,具体例子如下. 在父窗体的OnInit ...

  5. 在MFC框架中使用OpenGL的简单实例

    引言 我们知道,在MFC框架中,用于绘图的接口是GDI.但GDI只能绘制简单的2D图形,要想制作精美的3D图形,一个可行的办法是使用OpenGL或者Direct3D等第三方库. 由于最近在给导师的一个 ...

  6. xBIM 实战04 在WinForm窗体中实现IFC模型的加载与浏览

    系列目录    [已更新最新开发文章,点击查看详细]  WPF底层使用 DirectX 进行图形渲染.DirectX  能理解可由显卡直接渲染的高层元素,如纹理和渐变,所以 DirectX 效率更高. ...

  7. wpf的UserControl用户控件怎么添加到Window窗体中

    转载自 http://www.cnblogs.com/shuang121/archive/2013/01/09/2853591.html 我们来新建一个用户控件UserControl1.xaml &l ...

  8. C#窗体中读取修改xml文件

    由于之前没有操作过xml文件,尤其是在窗体中操作xml,脑子一直转不动,而且很抵制去做这个功能,终于还是突破了自己通过查询资料完成了这个功能,在此记录一下自己的成果. 功能说明:程序中存在的xml文件 ...

  9. C#将exe运行程序嵌入到自己的winform窗体中

    以下例子是将Word打开,然后将它嵌入到winform窗体中,效果如下图:C将exe运行程序嵌入到自己的winform窗体中 - kingmax_res - iSport注意:该方法只适用于com的e ...

随机推荐

  1. 配置glance使用NFS后端

    首先先使用“glance image-delete”命令删除所有镜像,释放磁盘空间. 停止glance服务:service openstack-glance-api stopservice opens ...

  2. 可用类型的几何对象esriGeometryType Constants

    The available kinds of geometry objects. Constant Value Description esriGeometryNull 0 A geometry of ...

  3. NVIDA 提到的 深度框架库

    BidMachBlocksCaffeChainerCNTKcuda-convnetcuda-convnet2Deeplearning4jkaldiKerasLasagneMarvinMatConvNe ...

  4. PAT (Advanced Level) 1022. Digital Library (30)

    简单模拟题. 写的时候注意一些小优化,小心TLE. #include<iostream> #include<cstring> #include<cmath> #in ...

  5. JQuery中的mouseover和mouseenter的区别

    mouseover和mouseout是一对:mouseenter和mouseleave是一对. 相同点:都是鼠标经过就触发事件 不同点: 给外盒子一个经过触发事件,但是mouseover会在鼠标经过外 ...

  6. php CI 实战教程:如何去掉index.php目录

    Windows下自由创建.htaccess文件的N种方法 .htaccess是apache的访问控制文件,apache中httpd.conf的选项配合此文件,完美实现了目录.站点的访问控制,当然最多的 ...

  7. Blog开始

    好久没更新Blog了,去看了下之前的csdn的blog感觉特别的乱,为此决心重开blog,记录工作及学习中的一些事 2013-10-28 ymc ...

  8. 手机访问pc网站,自动跳转到手机网站

    <script type='text/javascript'> var browser = { versions: function () { var u = navigator.user ...

  9. libconfig第一篇———使用指南

    官网:http://www.hyperrealm.com/libconfig/ 1 libconfig是什么? Libconfig是一个结构化的配置文件库,它可以定义一些配置文件,例如test.cfg ...

  10. JS中获取和操作iframe

    一.需求与遇到的问题 在网站的后台管理中使用了iframe框架布局,包括顶部菜单.左侧导航和主页面.需求是:点击主页面上的一个按钮,在顶部菜单栏的右侧显示“退出”链接,点击可退出系统. 我的思路是:在 ...