古法编程: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

9赞

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

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

1赞

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

给以前用2dx开发的游戏,一条上线微信小游戏参考的路吧。

支持.最起码多了一个解决方案 很不错

2dx的老项目还真不少的,至少25年的时候,还有公司找我做2dx的。我现在这个公司虽然ccc引擎居多,但是2dx的项目同样不少(20-30个),比起ccc重做,这个方案其实并不是什么新鲜事,明显更加便捷,虽然实际效果未知,但是快速上微信平台就是价值。

通过2dx赚到钱的公司/游戏不一定比creator少,毕竟当年2dx是真的很多人用的 :13:

用2dx的是不是都40多了

也挺好,技术本身探索不应该过分考虑现有的意义,因为每个人的眼光是有限的,未来如果用到的话,什么意义就都有了。老黄做CUDA的时候又有几个人能理解呢?只要有一个可能性,也许这个技术就有可能帮助一个项目。权衡好投入就行

没那么大,我31,也用过2dx

1 技术路线不一样 给不同偏好的多一种选择
2 2dx开发效率未必比cc的差 很多游戏其实不需要用到太多编辑器的功能 主要是UI的拼装 逻辑部分用代码就可以
3 2dx在web环境是打包成wasm 理论上比cc目前这种js实现性能要好(cc目前被诟病 一直想提升性能也是web这块)
4 历史项目的快速移植

怎么说呢,去年入职的一家公司,还在坚持2dx,项目都是十年包浆老项目了,然后他们自己改了一个,用emscripten上微信小游戏。效果倒还是可以的。

楼主的想法是好,反而是楼上那种,才入行几年,写过一些项目的小登,就别站着说话不腰疼了。

做项目可不是啥都用最新的技术就是对的。要考虑到历史积累,历史负担,重构的风险和代价,综合分析。执行开发和主程序以及技术总监可不是一个只看技术成长的发展路线哦