古法编程:cocos2dx开发微信小游戏,聊聊踩过的坑

一、环境准备

1. Cocos2d-x

2. Emscripten SDK (emsdk)

用于将 C++ 编译为 WASM + JS

使用版本:3.1.10


git clone https://github.com/emscripten-core/emsdk.git

cd emsdk

# 安装并激活指定版本

./emsdk install 3.1.10

./emsdk activate 3.1.10

3. H5 游戏包转微信小游戏包工具

工具地址:https://developers.weixin.qq.com/minigame/dev/guide/game-engine/common-adaptation.html

页面向下滚动即可找到对应转换工具。

4. Cocos2d-x转微信小游戏 常见问题

https://developers.weixin.qq.com/minigame/dev/guide/game-engine/common-adaptation/Design/Cocos2dxExport.html


二、我的踩坑记录

2024 年底开始尝试将 Cocos2d-x 游戏转为微信小游戏包,因业余时间有限,历时一年多完成。以下为实际遇到的问题与修改方案。

1. 可写目录权限问题


// 修改前

bool FileUtilsEmscripten::init()

{

    _defaultResRootPath = "/";

    return FileUtils::init();

}

// 修改后

bool FileUtilsEmscripten::init()

{

    _defaultResRootPath = "/";

    auto pDir = getWritablePath();

    if(0 != access(pDir.c_str(), 0)) {

        mkdir(pDir.c_str(), 0777);    

    }

   

    return FileUtils::init();

}

2. 去掉引擎中的 CCASSERT

iOS 平台触发 CCASSERT 会导致崩溃,直接注释相关断言。


void Node::addChild(Node *child, int localZOrder, int tag)

{    

    //CCASSERT( child != nullptr, "Argument must be non-nil");

    //CCASSERT( child->_parent == nullptr, "child already added. It can't be added again");

    addChildHelper(child, localZOrder, tag, "", true);

}

void Node::addChild(Node* child, int localZOrder, const std::string &name)

{

   // CCASSERT(child != nullptr, "Argument must be non-nil");

   // CCASSERT(child->_parent == nullptr, "child already added. It can't be added again");

    addChildHelper(child, localZOrder, INVALID_TAG, name, false);

}

3. Spine 版本兼容问题

项目使用较老 Spine 版本,需将引擎内 Spine 替换为 2.x;

若项目本身使用 3.x 则无需替换。

4. 界面变形问题

由默认固定宽高导致,改为由 JS 传入实际屏幕宽高。


// 修改前

GLViewImpl* GLViewImpl::create(const std::string& viewName, bool resizable)

{

    auto ret = new (std::nothrow) GLViewImpl;

    if(ret && ret->initWithRect(viewName, Rect(0, 0, 960, 640), 1.0f, resizable)) {

        ret->autorelease();

        return ret;

    }

    CC_SAFE_DELETE(ret);

    return nullptr;

}

// 修改后,s_width 和 s_height 由 JS 传入

/*

extern "C" {

    EMSCRIPTEN_KEEPALIVE

    void cocos_set_window_size( int width, int height)

    {

        s_width = width;

        s_height = height;

    }

}

*/

GLViewImpl* GLViewImpl::create(const std::string& viewName, bool resizable)

{

    auto ret = new (std::nothrow) GLViewImpl;

    if(ret && ret->initWithRect(viewName, Rect(0, 0, s_width, s_height), 1.0f, resizable)) {

        ret->autorelease();

        return ret;

    }

    CC_SAFE_DELETE(ret);

    return nullptr;

}

5. 声音问题

改用 JS 播放音频,C++ 仅将声音路径传递给 JS 层,由 JS 实现播放逻辑。

6. 多触摸问题

JS 层接收触摸事件,透传给 C++ 处理。

JS 层触摸转发


var Module = WXGameKit.gameInstance.Module || (GameGlobal.Module = GameGlobal.Module || {});

const touchHandler = {

  onTouchStart: function(event) {

    event.touches.forEach(function(touch){

      let percentX = touch.clientX / window.innerWidth;

      let percentY = 1.0 - touch.clientY / window.innerHeight;

   

      Module._cocos_onTouchStart(touch.identifier, percentX, percentY);

    });

  },

 

  onTouchMove: function(event) {

    event.touches.forEach(function(touch){

      let percentX = touch.clientX / window.innerWidth;

      let percentY = 1.0 - touch.clientY / window.innerHeight;

 

      Module._cocos_onTouchMove(touch.identifier, percentX, percentY);

    });

  },

 

  onTouchEnd: function(event) {

    event.changedTouches.forEach(function(touch){

      let percentX = touch.clientX / window.innerWidth;

      let percentY = 1.0 - touch.clientY / window.innerHeight;

      Module._cocos_onTouchEnd(touch.identifier, percentX, percentY);

    });

  },

 

  onTouchCancel: function(event) {

    Module._cocos_onTouchCancel(0, 0, 0);

  },

};

wx.onTouchStart(touchHandler.onTouchStart.bind(touchHandler));

wx.onTouchMove(touchHandler.onTouchMove.bind(touchHandler));

wx.onTouchEnd(touchHandler.onTouchEnd.bind(touchHandler));

wx.onTouchCancel(touchHandler.onTouchCancel.bind(touchHandler));

C++ 层导出函数


extern "C" {

    EMSCRIPTEN_KEEPALIVE

    void cocos_onTouchStart(int touchId, float x, float y)

    {

       

    }

    EMSCRIPTEN_KEEPALIVE

    void cocos_onTouchMove(int touchId, float x, float y)

    {

    }

    EMSCRIPTEN_KEEPALIVE

    void cocos_onTouchEnd(int touchId, float x, float y)

    {

    }

   

    EMSCRIPTEN_KEEPALIVE

    void cocos_onTouchCancel(int touchId, float x, float y)

    {

    }

}

7. Tilemap 路径问题


// 修改前

void TMXMapInfo::internalInit(const std::string& tmxFileName, const std::string& resourcePath)

{

    if (!tmxFileName.empty())

    {

        _TMXFileName = FileUtils::getInstance()->fullPathForFilename(tmxFileName);

    }

   

    if (!resourcePath.empty())

    {

        _resources = resourcePath;

    }

   

    _objectGroups.reserve(4);

    // tmp vars

    _currentString = "";

    _storingCharacters = false;

    _layerAttribs = TMXLayerAttribNone;

    _parentElement = TMXPropertyNone;

    _currentFirstGID = -1;

}

// 修改后

void TMXMapInfo::internalInit(const std::string& tmxFileName, const std::string& resourcePath)

{

    if (!tmxFileName.empty())

    {

        if (tmxFileName.find_last_of("/") != string::npos)

        {

            string dir = tmxFileName.substr(0, tmxFileName.find_last_of("/") + 1);

            _relativeTMXFileDir = dir;

        }

        _TMXFileName = FileUtils::getInstance()->fullPathForFilename(tmxFileName);

    }

   

    if (!resourcePath.empty())

    {

        _resources = resourcePath;

    }

   

    _objectGroups.reserve(4);

    // tmp vars

    _currentString = "";

    _storingCharacters = false;

    _layerAttribs = TMXLayerAttribNone;

    _parentElement = TMXPropertyNone;

    _currentFirstGID = -1;

}


// 修改前

else if (elementName == "image")

{

    TMXTilesetInfo* tileset = tmxMapInfo->getTilesets().back();

    // build full path

    std::string imagename = attributeDict["source"].asString();

    tileset->_originSourceImage = imagename;

    if (!_externalTilesetFullPath.empty())

    {

        string dir = _externalTilesetFullPath.substr(0, _externalTilesetFullPath.find_last_of('/') + 1);

        tileset->_sourceImage = dir + imagename;

    }

    else if (_TMXFileName.find_last_of('/') != string::npos)

    {

        string dir = _TMXFileName.substr(0, _TMXFileName.find_last_of('/') + 1);

        tileset->_sourceImage = dir + imagename;

    }

    else

    {

        tileset->_sourceImage = _resources + (!_resources.empty() ? "/" : "") + imagename;

    }

}

// 修改后

else if (elementName == "image")

{

    TMXTilesetInfo* tileset = tmxMapInfo->getTilesets().back();

    // build full path

    std::string imagename = attributeDict["source"].asString();

    tileset->_originSourceImage = imagename;

    if (!_externalTilesetFullPath.empty())

    {

        string dir = _externalTilesetFullPath.substr(0, _externalTilesetFullPath.find_last_of('/') + 1);

        tileset->_sourceImage = dir + imagename;

    }

    else if (_TMXFileName.find_last_of('/') != string::npos)

    {

        string dir = _relativeTMXFileDir;

        tileset->_sourceImage = dir + imagename;

    }

    else

    {

        tileset->_sourceImage = _resources + (!_resources.empty() ? "/" : "") + imagename;

    }

}

8. 可写路径持久化问题

默认可写路径重启后丢失,修改自定义可写路径。


// 修改前

string FileUtilsEmscripten::getWritablePath() const

{

    return "/cocos2dxWritablePath/";

}

// 修改后

string FileUtilsEmscripten::getWritablePath() const

{

    return "/CustomWritablePath/";

}

游戏效果

感兴趣的同学,可以看看2dx开发的微信小游戏效果
image

1赞

我不反对这种方式,但 首先没体现出什么优势,也没体现出什么技术价值,都是给自己找坑然后再填坑,除了情怀 还有什么能?
纯讨论事情 就是论事

就事论事就是价值很大,比论坛里大部分技术贴都有价值,Cocos2dx有大量的游戏而且品质还很不错,想要转小游戏要嘛creator重写但是工作量很大,最快的就是楼主的方案,其实unity也是一样的方案cshape转webassembly不过unity还做了大量优化的工作也就是现在的团结引擎

多大量?有统计吗?都是以前遗留的东西,新的产品 还有几个用2DX的?和2X3X比如 就是个零头的零头了吧,至于移植,是启用新的工具还是 非要走2DX,你有统计吗?我认为很少有人会继续用2DX 就算有也是小众人数,不作为好的依据来说明的