win32下cocos对接duilib

概述

由于需要将cocos的程序在win32下运行,但cocos使用的是系统本身窗口,从游戏角度来说,这个是比较不能接受的。那么肯定需要重绘窗口,简单来说有两种方案:

  • 使用mfc的窗口,重绘来实现
  • 使用例如duilib接管ui来实现

从后续开发简易度来说,使用duilib会比较好些,这里也使用了这个方案。

本文参考了以下方案

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

Duilib接入步骤

  1. 准备工作
  • 下载cocos和对应win32开发工具vs2013,建议使用vs2013,因为下面几个工具确定可以用它来编译
  • 下载glfw,cocos是使用它接管跨平台的窗口创建和管理的,方便对接opengl。需要注意的是,找对cocos对应glfw版本。
  • 下载并安装cmake,建议下载最新版本。
  • 下载并编译duilib
  1. 修改GLFW源码
  • 进入glfw源码目录,打开cmakegui,source code处选择下下来的glfw解压的文件夹,build the binaries选择生成解决方案的文件夹,然后生成对应VS版本的解决方案
  • 进入build the binaries选择生成解决方案的文件夹,打开vs解决方案
  • 打开win32_windows.c,修改createWindow函数,注释处均为修改的地方

    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
    // 增加三个参数 HWND parent, int px, int py,表示父窗口是谁,以及对应位置
    static int createWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, HWND parent, int px, int py)
    {
    int xpos, ypos, fullWidth, fullHeight;
    WCHAR* wideTitle;
    DWORD style = getWindowStyle(window);
    DWORD exStyle = getWindowExStyle(window);

    // 如果有父窗口,修改窗口属性
    if (parent)
    {
    style = WS_CHILDWINDOW | WS_VISIBLE;
    exStyle = WS_EX_APPWINDOW;
    }

    if (window->monitor)
    {
    GLFWvidmode mode;

    // NOTE: This window placement is temporary and approximate, as the
    // correct position and size cannot be known until the monitor
    // video mode has been set
    _glfwPlatformGetMonitorPos(window->monitor, &xpos, &ypos);
    _glfwPlatformGetVideoMode(window->monitor, &mode);
    fullWidth = mode.width;
    fullHeight = mode.height;
    }
    else
    {
    xpos = CW_USEDEFAULT;
    ypos = CW_USEDEFAULT;

    if (parent==NULL && wndconfig->maximized)
    style |= WS_MAXIMIZE;

    getFullWindowSize(style, exStyle,
    wndconfig->width, wndconfig->height,
    &fullWidth, &fullHeight);
    }

    // 如果有父窗口,修改窗口位置
    if (parent)
    {
    xpos = px;
    ypos = py;
    }

    wideTitle = _glfwCreateWideStringFromUTF8Win32(wndconfig->title);
    if (!wideTitle)
    {
    _glfwInputError(GLFW_PLATFORM_ERROR,
    "Win32: Failed to convert window title to UTF-16");
    return GLFW_FALSE;
    }

    window->win32.handle = CreateWindowExW(exStyle,
    _GLFW_WNDCLASSNAME,
    wideTitle,
    style,
    xpos, ypos,
    fullWidth, fullHeight,
    parent, // No parent window
    NULL, // No window menu
    GetModuleHandleW(NULL),
    NULL);

    free(wideTitle);

    //...
    //
    }
  • 打开win32_windows.c,修改_glfwPlatformCreateWindow函数,对接createWindow函数的参数,并修改对应的头文件internal.h

    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, int px, int py)
    {
    //...
    //
    if (!createWindow(window, wndconfig, parent, px, py))
    return GLFW_FALSE;

    //...
    //
    if (!createWindow(window, wndconfig, parent, px, py))
    return GLFW_FALSE;
    //...
    //
    }
  • glfw3.h,修改接口

    1
    2
    3
    4
    5
    // 为了兼容,保留老接口
    GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share);
    // 增加新接口
    GLFWAPI GLFWwindow* glfwCreateWindowEx(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share,
    intptr_t parent, int px, int py);
  • windows.h,修改接口实现

    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
    GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share)
    {
    return glfwCreateWindowEx(width, height, title, monitor, share, 0, 0, 0);
    }

    GLFWAPI GLFWwindow* glfwCreateWindowEx(int width, int height,
    const char* title,
    GLFWmonitor* monitor,
    GLFWwindow* share,
    intptr_t parent, int px, int py)
    {
    //...
    //

    // Open the actual window and create its context
    if (!_glfwPlatformCreateWindow(window, &wndconfig, &ctxconfig, &fbconfig, (HWND)parent, px, py))
    {
    glfwMakeContextCurrent((GLFWwindow*) previous);
    glfwDestroyWindow((GLFWwindow*) window);
    return NULL;
    }

    //...
    //
    }
  • 重新编译,然后glfw3.h和glfw3.lib复制到Cocos2dx\external\glfw3\include\win32和E:\daclient\Cocos2dx\external\glfw3\prebuilt\win32目录下

  1. 修改对应的cocos源码
  • cocos是使用glfwCreateWindow创建窗口的,找到对应位置,在CCGLViewImpl-desktop.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 增加了三个参数,对应调用此函数的地方也要做响应修改
    bool GLViewImpl::initWithRect(const std::string& viewName, Rect rect, float frameZoomFactor, bool resizable,
    HWND parent, int px, int py)
    {
    //
    //...

    // 去掉标题栏
    #if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
    glfwWindowHint(GLFW_DECORATED, GL_FALSE); // 这个样式实际上去掉了系统提供的默认标题栏。所有窗口区域都为客户区
    #endif

    int neededWidth = rect.size.width * _frameZoomFactor;
    int neededHeight = rect.size.height * _frameZoomFactor;

    // glfwCreateWindowEx替换glfwCreateWindow
    _mainWindow = glfwCreateWindowEx(neededWidth, neededHeight, _viewName.c_str(), _monitor, nullptr,
    (intptr_t)parent, px, py);

    //
    // ...
    }
  • cocos的Application::getInstance()->run();会阻塞线程,但之后线程会被duilib主窗口接管,所以渲染主循环会放到时钟中去,那么需要将run函数拆分。找到CCApplication-win32.cpp

    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
    // 新增三个函数
    int Application::loopinit()
    {
    PVRFrameEnableControlWindow(false);

    initGLContextAttrs();

    // Initialize instance and cocos2d.
    if (!applicationDidFinishLaunching())
    {
    return 1;
    }

    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();

    // Retain glview to avoid glview being released in the while loop
    glview->retain();
    _loopinit = true;

    return 0;
    }

    int Application::looponce()
    {
    if (!_loopinit) return 1;

    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();

    director->mainLoop();
    glview->pollEvents();

    return 0;
    }

    int Application::loopfini()
    {
    if (!_loopinit) return 1;

    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();

    if (glview->isOpenGLReady())
    {
    director->end();
    director->mainLoop();
    director = nullptr;
    }

    glview->release();
    _loopinit = false;

    return 0;
    }
  1. 引入duilib,根据duilib的例子,创建duilib窗口,建议把duilib相关的内容放到单独的cpp文件,不与cocos的文件有牵扯,容易出类库包含不兼容问题。
    1
    2
    3
    4
    // duilib窗口对应函数
    HWND DuiWinCreate(HINSTANCE hInstance);
    void DuiWinShow();
    void DuiWinRun();

duilib窗口的属性配置

duilib库有一些bug,如Dialog的domodel返回值错误;PostQuitMessage(0L)有时候不能退出

  1. 修改主工程
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
// 修改main.cpp

#include "main.h"

#include "DuiWin.h"
#include "AppDelegate.h"
USING_NS_CC;

#define USE_WIN32_CONSOLE
#define CC_LOOPTIMER 101

// 声明时钟回调函数
void CALLBACK WinSunTimerProc(HWND hWnd, UINT nMsg, UINT nTimerid, DWORD dwTime);

//创建一个完整的窗口需要经过四个步骤:设计一个窗口类;注册窗口类;创建窗口;显示及更新窗口
int WINAPI _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

HICON hIcon = ::LoadIcon(hInstance, MAKEINTRESOURCE(MAINICON));

#ifdef USE_WIN32_CONSOLE
AllocConsole();
freopen("CONIN$", "r", stdin);
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
#endif
HWND hwnd = DuiWinCreate(hInstance);

AppDelegate app;
app.m_parent = hwnd;
Application::getInstance()->loopinit();
SetTimer(hwnd, CC_LOOPTIMER, 1, WinSunTimerProc);

DuiWinShow();
DuiWinRun();

KillTimer(hwnd, CC_LOOPTIMER);
Application::getInstance()->loopfini();
/*AppDelegate app;
app.m_parent = NULL;
Application::getInstance()->run();*/

#ifdef USE_WIN32_CONSOLE
FreeConsole();
#endif

return 0;
}

void CALLBACK WinSunTimerProc(HWND hWnd, UINT nMsg, UINT nTimerid, DWORD dwTime)
{
if (nTimerid == CC_LOOPTIMER)
{
Application::getInstance()->looponce();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 对应创建窗口的地方
bool AppDelegate::applicationDidFinishLaunching()
{
//
// ...

#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
glview = GLViewImpl::createWithRect("168game", Rect(0, 0, 1280, 720), 1.0f, false, HWND(m_parent),0, 24);
#else

//
// ...
}
  1. 至此已经对接完成,本方案不会影响ios和android包的实现。

  2. 其他

  • 显示应用在系统状态的图片,加载一个mfc资源,使用HICON hIcon = ::LoadIcon(hInstance, MAKEINTRESOURCE(MAINICON));即可
  • 使用自定义鼠标cur,似乎不能实现,duilib使用的是系统鼠标,并做一些自动切换