├── .eslintrc.js ├── .gitignore ├── .idea ├── inspectionProfiles │ └── Project_Default.xml ├── jsLibraryMappings.xml ├── jsLinters │ └── eslint.xml └── vcs.xml ├── README.md ├── doc ├── c25d1182.png └── hehe.gif ├── game ├── README.md └── index.html ├── icon └── icon.png ├── package.json ├── src ├── core.js ├── editor.js ├── main.js ├── profile.js ├── server.js └── window.js └── static └── 404.html /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | module.exports = { 3 | // 将 ESLint 限制到一个特定的项目,在配置文件里设置 "root": true。ESLint 一旦发现配置文件中有 "root": true,它就会停止在父级目录中寻找。 4 | root: true, 5 | // 检测ES6代码 6 | parser: 'babel-eslint', 7 | parserOptions: { 8 | sourceType: 'module' 9 | }, 10 | // 11 | env: { 12 | browser: true, 13 | }, 14 | // 消除no-undef影响 15 | globals: { 16 | _: true 17 | }, 18 | 'rules': { 19 | // 使用单引号 20 | 'quotes': [1, 'single'], 21 | 'prefer-promise-reject-errors': 0, 22 | 'space-unary-ops': 0, 23 | 'no-unused-expressions': 0, 24 | 'no-useless-return': 0, 25 | 'standard/no-callback-literal': 0, 26 | 'import/first': 0, 27 | 'import/export': 0, 28 | 'no-mixed-operators': 0, 29 | 'no-use-before-define': 0, 30 | // 允许使用分号 31 | 'semi': [0, 'never'], 32 | // 允许使用== 33 | 'eqeqeq': 0, 34 | // 缩进使用不做限制 35 | 'indent': 0, 36 | // 允许使用tab 37 | 'no-tabs': 0, 38 | // 函数圆括号之前的空格 39 | // 'space-before-function-paren': [2, "always"], 40 | // 不要求块内空格填充格式 41 | 'padded-blocks': 0, 42 | // 不限制变量一起声明 43 | 'one-var': 0, 44 | // debugger使用 45 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 46 | // 开发模式允许使用console 47 | 'no-console': 0, 48 | // 条件语句中复制操作符需要用圆括号括起来 49 | 'no-cond-assign': [2, 'except-parens'], 50 | // 允许使用条件表达式使用常量 51 | 'no-constant-condition': 0, 52 | // 单行可忽略大括号,多行不可忽略 53 | 'curly': [2, 'multi-line'], 54 | // 不允许使用var变量 55 | 'no-var': 2, 56 | // 不允许出现多个空格 57 | 'no-multi-spaces': ["error", {ignoreEOLComments: true}], 58 | 'camelcase': 0, 59 | // 对象字面量的键值空格风格 60 | 'key-spacing': 2, 61 | // if语句包含一个return语句, else就多余 62 | 'no-else-return': 2, 63 | // 建议将经常出现的数字提取为变量 64 | 'no-magic-numbers': [0, {ignoreArrayIndexes: true}], 65 | // 不允许重复声明变量 66 | 'no-redeclare': [2, {builtinGlobals: true}], 67 | // 立即执行函数风格 68 | 'wrap-iife': [2, 'inside'], 69 | // 不允许圆括号中出现空格 70 | 'space-in-parens': [2, 'never'], 71 | // 确保运算符周围有空格 72 | 'space-infix-ops': 2, 73 | // 强制点号与属性同一行 74 | 'dot-location': [2, 'property'], 75 | // 强制单行代码使用空格 76 | 'block-spacing': [2, 'always'], 77 | // 约束for-in使用hasOwnProperty判断 78 | 'guard-for-in': 0, 79 | // 采用one true brace style大括号风格 80 | 'brace-style': [2, '1tbs', {'allowSingleLine': true}], 81 | // 统一逗号周围空格风格 82 | 'comma-spacing': [2, {'before': false, 'after': true}], 83 | // 禁止出现多个空行 84 | 'no-multiple-empty-lines': [2, {'max': 1, 'maxEOF': 2}], 85 | // 允许箭头函数不使用圆括号 86 | 'arrow-parens': 0, 87 | // 规范generator函数的使用 88 | 'generator-star-spacing': [2, {'before': false, 'after': true}], 89 | // 要求在块级 90 | // 'lines-around-comment': [2, { 91 | // 'beforeBlockComment': true, 92 | // 'afterBlockComment': false, 93 | // 'beforeLineComment': true, 94 | // 'afterLineComment': false 95 | // }] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .idea 3 | package-lock.json 4 | splash.png 5 | style-desktop.css 6 | style-mobile.css 7 | game/res 8 | game/src 9 | game/subpackages 10 | node_modules 11 | .DS_Store 12 | cocos2d-js-min.js 13 | main.js 14 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CocosCreatorForDeskApp 2 | 如果你的游戏时使用cocos creator开发制作的,并且希望能够发布为桌面应用,那么该解决方案一定能帮助到您! 3 | 4 | 该解决方案,基于Electron,轻松将cocos creator发布到任何桌面,使用该解决方案的好处是,再也不用去踩发布原生桌面应用的坑了! 5 | 6 | 为此,您需要注意,相比原生桌面应用,该解决方案最终游戏包体会增加50~70M不等,如果您特别在意游戏安装包体积,那么,请慎重考虑! 7 | 8 | 9 | 10 | 11 | ## 使用说明 12 | - 安装依赖`npm install` 13 | - 运行: `npm run app` 14 | 15 | ## 打包说明 16 | 17 | ### 打包前的准备工作 18 | 19 | - 使用CocosCreator构建出一份web版本的游戏 20 | - 将构建的web游戏拷贝到game目录,game目录文件示例 21 | ![](doc/c25d1182.png) 22 | 23 | 本项目采用的是`electron-builder`打包 24 | 25 | - 全局安装`electron-builder`依赖: `npm install -g electron-builder` 26 | - 打包: 27 | - win: `npm run pack-win` 28 | - mac: `npm run pack-mac` 29 | 30 | 31 | ## 后续开发功能 32 | - 请查看相关issues 33 | - 欢迎追加issues 34 | 35 | 欢迎加入CocosCreator插件交流群(224756137) 36 | 37 | 一起学习,一起进步! 38 | 39 | ![](doc/hehe.gif) 40 | -------------------------------------------------------------------------------- /doc/c25d1182.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidys/CocosCreatorForDeskApp/955c0c2f2f3366e6fa6db2abba01d70fe07b17a0/doc/c25d1182.png -------------------------------------------------------------------------------- /doc/hehe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidys/CocosCreatorForDeskApp/955c0c2f2f3366e6fa6db2abba01d70fe07b17a0/doc/hehe.gif -------------------------------------------------------------------------------- /game/README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 请把你的游戏放在这个game目录 3 | -------------------------------------------------------------------------------- /game/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cocos Creator | examplecases 7 | 8 | 9 | 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 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidys/CocosCreatorForDeskApp/955c0c2f2f3366e6fa6db2abba01d70fe07b17a0/icon/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cocoscreatorfordeskapp", 3 | "version": "1.0.0", 4 | "description": "使用cocos creator开发游戏,打包桌面应用解决方案", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "app": "electron .", 8 | "pack-win": "electron-builder --win --x64", 9 | "pack-mac": "electron-builder --mac" 10 | }, 11 | "build": { 12 | "appId": "com.my.game", 13 | "productName": "Game", 14 | "mac": { 15 | "target": [ 16 | "dmg" 17 | ] 18 | }, 19 | "win": { 20 | "asar": true, 21 | "asarUnpack": "static", 22 | "icon": "icon/icon.png", 23 | "target": [ 24 | "zip" 25 | ] 26 | } 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/tidys/CocosCreatorForDeskApp.git" 31 | }, 32 | "author": "", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/tidys/CocosCreatorForDeskApp/issues" 36 | }, 37 | "homepage": "https://github.com/tidys/CocosCreatorForDeskApp#readme", 38 | "dependencies": { 39 | "@types/fs-extra": "^8.0.0", 40 | "@types/node": "^12.6.3", 41 | "chokidar": "^3.0.2", 42 | "express": "^4.17.1", 43 | "fs-extra": "^8.1.0", 44 | "json-format": "^1.0.1" 45 | }, 46 | "devDependencies": { 47 | "babel-eslint": "^10.0.2", 48 | "electron": "^5.0.6", 49 | "eslint-plugin-vue": "^5.2.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidys/CocosCreatorForDeskApp/955c0c2f2f3366e6fa6db2abba01d70fe07b17a0/src/core.js -------------------------------------------------------------------------------- /src/editor.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs'); 2 | const Path = require('path'); 3 | const Electron = require('electron'); 4 | 5 | module.exports = { 6 | _getUserDateDir() { 7 | let dir = null; 8 | if (Electron.remote && Electron.remote.app) { 9 | dir = Electron.remote.app.getPath('userData'); 10 | } else if (Electron && Electron.app) { 11 | dir = Electron.app.getPath('userData'); 12 | } 13 | return dir; 14 | }, 15 | 16 | getAppConfigDefaultPath(file) { 17 | let dir = this._getUserDateDir(); 18 | let savePath = Path.join(dir, file || 'default.json'); 19 | 20 | if (!Fs.existsSync(savePath)) { 21 | Fs.writeFileSync(savePath, '{}'); 22 | } 23 | // console.log( savePath ); 24 | return savePath; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | global.Editor = require('./editor.js'); 2 | const Window = require('./window.js'); 3 | const Server = require('./server.js'); 4 | const {app} = require('electron'); 5 | app.on('ready', function () { 6 | Server.initServer(function (url) { 7 | Window.createMainWindow(url); 8 | }); 9 | }); 10 | app.on('window-all-closed', function () { 11 | if (process.platform !== 'darwin') { 12 | app.quit(); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /src/profile.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs'); 2 | const Path = require('path'); 3 | const JsonFormat = require('json-format'); 4 | 5 | 6 | class Profile { 7 | constructor(file) { 8 | this.data = null; 9 | this.file = file; 10 | this.vaild = this.load(file); 11 | } 12 | 13 | load(file) { 14 | if (!Fs.existsSync(file)) { 15 | return false; 16 | } 17 | 18 | let data = Fs.readFileSync(file, 'utf-8'); 19 | try { 20 | this.data = JSON.parse(data); 21 | } catch (e) { 22 | this.data = null; 23 | return false; 24 | } 25 | return true; 26 | } 27 | 28 | saveItem(key, value) { 29 | if (!this.vaild) { 30 | console.log('save failed'); 31 | return; 32 | } 33 | this.data[key] = value; 34 | Fs.writeFileSync(this.file, JSON.stringify(this.data)); 35 | } 36 | 37 | // 保存object类型的数据 38 | saveWithObjectData(data) { 39 | if (!this.vaild) { 40 | console.log('save failed'); 41 | return; 42 | } 43 | for (let key in data) { 44 | this.data[key] = data[key]; 45 | } 46 | 47 | // Fs.writeFileSync( this.file, JSON.stringify( this.data ) ); 48 | Fs.writeFileSync(this.file, JsonFormat(this.data)); 49 | } 50 | 51 | // 保存array类型的数据 52 | saveWithArrayData(data) { 53 | if (!this.vaild) { 54 | console.log('save failed'); 55 | return; 56 | } 57 | this.data = data; 58 | 59 | // Fs.writeFileSync( this.file, JSON.stringify( this.data ) ); 60 | Fs.writeFileSync(this.file, JsonFormat(this.data)); 61 | } 62 | } 63 | 64 | module.exports = Profile; 65 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const Express = require('express'); 2 | const Path = require('path'); 3 | const Fs = require('fs'); 4 | module.exports = { 5 | _isInit: false, 6 | server: null, 7 | url: null, 8 | _launchCB: null, 9 | initServer(launchCB) { 10 | if (this._isInit === false) { 11 | this._isInit = true; 12 | this._launchCB = launchCB; 13 | this.probe(4101, this._onFindPort.bind(this)); 14 | } 15 | 16 | }, 17 | _startServer(port) { 18 | let App = Express(); 19 | App.use(Express.static(Path.join(__dirname, '../game'))); 20 | 21 | // App.get('/', function (req, res) { 22 | // let file = Path.join(__dirname, '../game/index.html'); 23 | // res.sendFile(file); 24 | // }); 25 | 26 | let server = App.listen(port, 'localhost', function () { 27 | let host = server.address().address; 28 | let port = server.address().port; 29 | this.url = `http://${host}:${port}`; 30 | console.log(`预览服务访问地址 ${this.url}`); 31 | this._launchCB && this._launchCB(this.url); 32 | }.bind(this)) 33 | this.server = server; 34 | }, 35 | 36 | _onFindPort(bl, port) { 37 | if (bl) { 38 | console.log('端口被占用:' + port); 39 | this.probe(port + 1, this._onFindPort.bind(this)); 40 | } else { 41 | this._startServer(port); 42 | } 43 | }, 44 | probe(port, callback) { 45 | let net = require('net'); 46 | 47 | // tcp使用端口0表示系统分配端口 48 | let server = net.createServer().listen(port); 49 | server.on('listening', function () { 50 | server.once('close', function () { 51 | callback(false, port); 52 | }); 53 | server.close(); 54 | }); 55 | server.on('error', function (err) { 56 | callback(true, port); 57 | }); 58 | }, 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/window.js: -------------------------------------------------------------------------------- 1 | const Electron = require('electron'); 2 | const BrowserWindow = Electron.BrowserWindow; 3 | const Profile = require('./profile.js'); 4 | const Chokidar = require('chokidar'); 5 | const Path = require('path'); 6 | const Fs = require('fs'); 7 | 8 | module.exports = { 9 | createMainWindow: function (url) { 10 | let win = this._createWindow({ 11 | title: '游戏窗口', 12 | url: url, 13 | devtools: true, 14 | profile: 'main-window.json', 15 | autoReload: true, 16 | watchDir: Path.join(__dirname), 17 | closeCallBack: function () { 18 | 19 | } 20 | }) 21 | }, 22 | _createWindow(options) { 23 | if (!options.profile) { 24 | return; 25 | } 26 | let profilePath = Editor.getAppConfigDefaultPath(options.profile); 27 | let profile = new Profile(profilePath); 28 | let width = 850; 29 | let height = 680; 30 | let screenSize = Electron.screen.getPrimaryDisplay().size; 31 | let x = screenSize.width / 2 - width / 2; 32 | let y = screenSize.height / 2 - height / 2; 33 | if (profile.data) { 34 | width = profile.data.width || width; 35 | height = profile.data.height || height; 36 | x = profile.data.x || x; 37 | y = profile.data.y || y; 38 | } 39 | let win = new BrowserWindow({ 40 | width: width, 41 | height: height, 42 | x: x, 43 | y: y, 44 | title: options.title || '', 45 | parent: options.parent || null, 46 | modal: !!options.modal, 47 | resizable: true, 48 | webPreferences: { 49 | webSecurity: false, 50 | nodeIntegration: true, // 允许在渲染进程使用node特性 51 | }, 52 | backgroundColor: '#ffffff' 53 | }); 54 | if (options.html) { 55 | let indexFile = options.html; 56 | if (!Fs.existsSync(indexFile)) { 57 | indexFile = Path.join(__dirname, '../static/404.html'); 58 | } 59 | win.loadFile(indexFile); 60 | } else if (options.url) { 61 | win.loadURL(options.url); 62 | } else { 63 | return; 64 | } 65 | win.show(); 66 | if (options.devtools) { 67 | win.openDevTools(); 68 | } 69 | win.on('resize', function (win) { 70 | let size = win.sender.getSize(); 71 | profile.saveWithObjectData({ 72 | width: size[0], 73 | height: size[1] 74 | }); 75 | }); 76 | win.on('close', (event) => { 77 | let window = event.sender; 78 | let pos = win.getPosition(); 79 | profile.saveWithObjectData({ 80 | x: pos[0], 81 | y: pos[1] 82 | }); 83 | 84 | options.closeCallBack && options.closeCallBack(); 85 | }); 86 | if (options.autoReload) { 87 | this.watchReload(win, options.watchDir); 88 | } 89 | }, 90 | watchReload(window, dir) { 91 | let watcher = Chokidar.watch(dir); 92 | let timer = null; 93 | watcher.on('all', function (event, file, state) { 94 | if (event === 'change') { 95 | console.log(`${event}: ${file}`); 96 | clearTimeout(timer); 97 | timer = setTimeout(function () { 98 | if (window && !window.isDestroyed()) { 99 | window.reload(); 100 | } 101 | }, 1000); 102 | } 103 | }); 104 | }, 105 | } 106 | -------------------------------------------------------------------------------- /static/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 404! 9 | 10 | 11 | --------------------------------------------------------------------------------