Cocos Creator大厅+子游戏模式

一、前言

根据上一篇(Cocos Creator热更新),可以看出以下几点:

  • build-default目录下的main.js,为cocos creator项目的入口;
  • 热更新一文中,放置在服务器上的,仅有资源,脚本,配置等,没有入口程序,因此本文中,我们需要创造一个入口程序。
还是解释一下什么叫大厅+子游戏模式:

  1. 将大厅单独作为一个完整的项目,不同的子游戏,则为不同的项目
  2. 然后要实现不同项目之间的互调,即大厅调子游戏,或者子游戏调大厅
  3. 资源共享,共用的资源放在大厅项目中,并且子游戏中可以调用

这样做的好处:

  1. 减小上架包的体积
  2. 提高热更新的效率(打开指定子游戏,才会更新子游戏)
  3. 降低项目的耦合性(如果不共享资源,子游戏完全可以随时抽取出来作为一个单独的包使用)

二、修改子游戏

1. 添加version_generato.js
2. 构建项目
3. 在原生src下,添加 main.js 入口文件
  3.1 每次构建完项目,拷贝main.js到原生目录的src中

  main.js的内容如下:

(function () {
    'use strict';

    if (window.jsb) {
        /// 1.初始化资源Lib路径Root.
        var subgameSearchPath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/')+'ALLGame/subgame/';

        /// 2.subgame资源未映射,则初始化资源映射表,否则略过映射.
        if(!cc.HallAndSubGameGlobal.subgameGlobal){
            cc.HallAndSubGameGlobal.subgameGlobal = {};

            /// 加载settings.js
            require(subgameSearchPath + 'src/settings.js');
            var settings = window._CCSettings;
            window._CCSettings = undefined;

            if ( !settings.debug ) {
                var uuids = settings.uuids;

                var rawAssets = settings.rawAssets;
                var assetTypes = settings.assetTypes;
                var realRawAssets = settings.rawAssets = {};
                for (var mount in rawAssets) {
                    var entries = rawAssets[mount];
                    var realEntries = realRawAssets[mount] = {};
                    for (var id in entries) {
                        var entry = entries[id];
                        var type = entry[1];
                        // retrieve minified raw asset
                        if (typeof type === 'number') {
                            entry[1] = assetTypes[type];
                        }
                        // retrieve uuid
                        realEntries[uuids[id] || id] = entry;
                    }
                }

                var scenes = settings.scenes;
                for (var i = 0; i < scenes.length; ++i) {
                    var scene = scenes[i];

                    if (typeof scene.uuid === 'number') {
                        scene.uuid = uuids[scene.uuid];
                    }
                }

                var packedAssets = settings.packedAssets;
                for (var packId in packedAssets) {
                    var packedIds = packedAssets[packId];
                    for (var j = 0; j < packedIds.length; ++j) {
                        if (typeof packedIds[j] === 'number') {
                            packedIds[j] = uuids[packedIds[j]];
                        }
                    }
                }
            }

            /// 加载project.js
            var projectDir = 'src/project.js';
            if ( settings.debug ) {
                projectDir = 'src/project.dev.js';
            }
            require(subgameSearchPath + projectDir);

            /// 如果当前搜索路径没有subgame,则添加进去搜索路径。
            var currentSearchPaths = jsb.fileUtils.getSearchPaths();
            if(currentSearchPaths && currentSearchPaths.indexOf(subgameSearchPath) === -1){
                jsb.fileUtils.addSearchPath(subgameSearchPath, true);
                console.log('subgame main.js 之前未添加,添加下subgameSearchPath' + currentSearchPaths);
            }

            cc.AssetLibrary.init({
                libraryPath: 'res/import',
                rawAssetsBase: 'res/raw-',
                rawAssets: settings.rawAssets,
                packedAssets: settings.packedAssets,
                md5AssetsMap: settings.md5AssetsMap
            });

            cc.HallAndSubGameGlobal.subgameGlobal.launchScene = settings.launchScene;

            /// 将subgame的场景添加到cc.game中,使得cc.director.loadScene可以从cc.game._sceneInfos查找到相关场景
            for(var i = 0; i < settings.scenes.length; ++i){
                cc.game._sceneInfos.push(settings.scenes[i]);
            }
        }

        /// 3.加载初始场景
        var launchScene = cc.HallAndSubGameGlobal.subgameGlobal.launchScene;
        cc.director.loadScene(launchScene, null,
            function () {
                console.log('subgame main.js 成功加载初始场景' + launchScene);
            }
        );
    }
})();

ps: 不用管src外部的main.js文件

  3.2 或者 添加build-templates目录,自动在每次构建项目后生成main.js文件

这里的main.js内容和上面的内容一致

4. 执行version_generator.js文件

  生成version.manifest 和 project.mainfest。这个在上一篇中已经讲过,就不细说了。

三、拷贝res,src,version.manifest 和 project.mainfest到服务器目录下

  很明显,现在我们只是把子游戏生成了资源包,但是没有做任何热更新的操作。
接下来,就需要在大厅项目中,添加下载,更新的逻辑了。

四、在大厅项目中,添加相应逻辑

  负责下载,检测更新,更新子游戏的工具库文件内容如下:

const SubgameManager = {
    _storagePath: [],

    _getfiles: function(name, type, downloadCallback, finishCallback) {
        this._storagePath[name] = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'ALLGame/' + name);
        this._downloadCallback = downloadCallback;
        this._finishCallback = finishCallback;
        this._fileName = name;

        /// 替换该地址
        var UIRLFILE = "http://192.168.200.117:8000/" + name + "/remote-assets";
        var filees = this._storagePath[name] + '/project.manifest';

        var customManifestStr = JSON.stringify({
            'packageUrl': UIRLFILE,
            'remoteManifestUrl': UIRLFILE + '/project.manifest',
            'remoteVersionUrl': UIRLFILE + '/version.manifest',
            'version': '0.0.1',
            'assets': {},
            'searchPaths': []
        });

        var versionCompareHandle = function(versionA, versionB) {
            var vA = versionA.split('.');
            var vB = versionB.split('.');
            for (var i = 0; i < vA.length; ++i) {
                var a = parseInt(vA[i]);
                var b = parseInt(vB[i] || 0);
                if (a === b) {
                    continue;
                } else {
                    return a - b;
                }
            }
            if (vB.length > vA.length) {
                return -1;
            } else {
                return 0;
            }
        };

        this._am = new jsb.AssetsManager('', this._storagePath[name], versionCompareHandle);

        if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
            this._am.retain();
        }

        this._am.setVerifyCallback(function(path, asset) {
            var compressed = asset.compressed;
            if (compressed) {
                return true;
            } else {
                return true;
            }
        });


        if (cc.sys.os === cc.sys.OS_ANDROID) {
            this._am.setMaxConcurrentTask(2);
        }

        if (type === 1) {
            this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._updateCb.bind(this));
        } else if (type == 2) {
            this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._checkCb.bind(this));
        } else {
            this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._needUpdate.bind(this));
        }

        cc.eventManager.addListener(this._updateListener, 1);

        if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
            var manifest = new jsb.Manifest(customManifestStr, this._storagePath[name]);
            this._am.loadLocalManifest(manifest, this._storagePath[name]);
        }

        if (type === 1) {
            this._am.update();
            this._failCount = 0;
        } else {
            this._am.checkUpdate();
        }
        this._updating = true;
        cc.log('更新文件:' + filees);
    },

    // type = 1
    _updateCb: function(event) {
        var failed = false;
        let self = this;
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                /*0 本地没有配置文件*/
                cc.log('updateCb本地没有配置文件');
                failed = true;
                break;

            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                /*1下载配置文件错误*/
                cc.log('updateCb下载配置文件错误');
                failed = true;
                break;

            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                /*2 解析文件错误*/
                cc.log('updateCb解析文件错误');
                failed = true;
                break;

            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                /*3发现新的更新*/
                cc.log('updateCb发现新的更新');
                break;

            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                /*4 已经是最新的*/
                cc.log('updateCb已经是最新的');
                failed = true;
                break;

            case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                /*5 最新进展 */
                self._downloadCallback && self._downloadCallback(event.getPercentByFile());
                break;


            case jsb.EventAssetsManager.ASSET_UPDATED:
                /*6需要更新*/
                break;

            case jsb.EventAssetsManager.ERROR_UPDATING:
                /*7更新错误*/
                cc.log('updateCb更新错误');
                break;

            case jsb.EventAssetsManager.UPDATE_FINISHED:
                /*8更新完成*/
                self._finishCallback && self._finishCallback(true);
                break;

            case jsb.EventAssetsManager.UPDATE_FAILED:
                /*9更新失败*/
                self._failCount++;
                if (self._failCount <= 3) {
                    self._am.downloadFailedAssets();
                    cc.log(('updateCb更新失败' + this._failCount + ' 次'));
                } else {
                    cc.log(('updateCb失败次数过多'));
                    self._failCount = 0;
                    failed = true;
                    self._updating = false;
                }
                break;

            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                /*10解压失败*/
                cc.log('updateCb解压失败');
                break;
        }

        if (failed) {
            cc.eventManager.removeListener(self._updateListener);
            self._updateListener = null;
            self._updating = false;
            self._finishCallback && self._finishCallback(false);
        }
    },

    // type = 2
    _checkCb: function(event) {
        var failed = false;
        let self = this;
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                /*0 本地没有配置文件*/
                cc.log('checkCb本地没有配置文件');
                break;

            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                /*1下载配置文件错误*/
                cc.log('checkCb下载配置文件错误');
                failed = true;
                break;

            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                /*2 解析文件错误*/
                cc.log('checkCb解析文件错误');
                failed = true;
                break;

            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                /*3发现新的更新*/
                self._getfiles(self._fileName, 1, self._downloadCallback, self._finishCallback);
                break;

            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                /*4 已经是最新的*/
                cc.log('checkCb已经是最新的');
                self._finishCallback && self._finishCallback(true);
                break;

            case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                /*5 最新进展 */
                break;

            case jsb.EventAssetsManager.ASSET_UPDATED:
                /*6需要更新*/
                break;

            case jsb.EventAssetsManager.ERROR_UPDATING:
                /*7更新错误*/
                cc.log('checkCb更新错误');
                failed = true;
                break;


            case jsb.EventAssetsManager.UPDATE_FINISHED:
                /*8更新完成*/
                cc.log('checkCb更新完成');
                break;

            case jsb.EventAssetsManager.UPDATE_FAILED:
                /*9更新失败*/
                cc.log('checkCb更新失败');
                failed = true;
                break;

            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                /*10解压失败*/
                cc.log('checkCb解压失败');
                break;

        }
        this._updating = false;
        if (failed) {
            self._finishCallback && self._finishCallback(false);
        }
    },

    // type = 3
    _needUpdate: function(event) {
        let self = this;
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log('子游戏已经是最新的,不需要更新');
                self._finishCallback && self._finishCallback(false);
                break;

            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                cc.log('子游戏需要更新');
                self._finishCallback && self._finishCallback(true);
                break;

            // 检查是否更新出错
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
            case jsb.EventAssetsManager.ERROR_UPDATING:
            case jsb.EventAssetsManager.UPDATE_FAILED:
                self._downloadCallback();
                break;
        }
    },

    /**
     * 下载子游戏
     * @param {string} name - 游戏名
     * @param progress - 下载进度回调
     * @param finish - 完成回调
     * @note finish 返回true表示下载成功,false表示下载失败
     */
    downloadSubgame: function(name, progress, finish) {
        this._getfiles(name, 2, progress, finish);
    },

    /**
     * 进入子游戏
     * @param {string} name - 游戏名
     */
    enterSubgame: function(name) {
        if (!this._storagePath[name]) {
            this.downloadSubgame(name);
            return;
        }

        require(this._storagePath[name] + '/src/main.js');
    },

    /**
     * 判断子游戏是否已经下载
     * @param {string} name - 游戏名
     */
    isSubgameDownLoad: function (name) {
        let file = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'ALLGame/' + name + '/project.manifest';
        if (jsb.fileUtils.isFileExist(file)) {
            return true;
        } else {
            return false;
        }
    },

    /**
     * 判断子游戏是否需要更新
     * @param {string} name - 游戏名
     * @param isUpdateCallback - 是否需要更新回调
     * @param failCallback - 错误回调
     * @note isUpdateCallback 返回true表示需要更新,false表示不需要更新
     */
    needUpdateSubgame: function (name, isUpdateCallback, failCallback) {
        this._getfiles(name, 3, failCallback, isUpdateCallback);
    },
};

module.exports = SubgameManager;

  调用的过程如下:
    1. 判断子游戏是否已下载
    2. 已下载,判断是否需要更新
    3.1 下载游戏
    3.2 更新游戏
    4. 进入子游戏

const SubgameManager = require('SubgameManager');

cc.Class({
    extends: cc.Component,

    properties: {
        downloadBtn: {
            default: null,
            type: cc.Node
        },
        downloadLabel: {
            default: null,
            type: cc.Label
        }
    },

    onLoad: function () {
        const name = 'subgame';    
        //判断子游戏有没有下载
        if (SubgameManager.isSubgameDownLoad(name)) {
            //已下载,判断是否需要更新
            SubgameManager.needUpdateSubgame(name, (success) => {
                if (success) {
                    this.downloadLabel.string = "子游戏需要更新";
                } else {
                    this.downloadLabel.string = "子游戏不需要更新";
                }
            }, () => {
                cc.log('出错了');
            });
        } else {
            this.downloadLabel.string = "子游戏未下载";
        }

        this.downloadBtn.on('click', () => {
            //下载子游戏/更新子游戏
            SubgameManager.downloadSubgame(name, (progress) => {
                if (isNaN(progress)) {
                    progress = 0;
                }
                this.downloadLabel.string = "资源下载中   " + parseInt(progress * 100) + "%";
            }, function(success) {
                if (success) {
                    SubgameManager.enterSubgame('subgame');
                } else {
                    cc.log('下载失败');
                }
            });
        }, this);
    },
});

说到这呢,就得提一下,
如果界面设计时,从大厅点击子游戏,中间有loading的界面的话,
loading界面就应该放在大厅的工程中了。

五、测试

  打开服务——>编译大厅目录——>安装运行
注意:
    一定要生成原生apk,在真机(也可以是类似于夜神的模拟器啦)上运行测试。
结果:
    1. 第一次,本地没有子游戏,提示“游戏未下载”,下载后,无需重启,可直接进入子游戏;
    2. 修改version_generator.js中的版本号,将步骤二,再走一遍,能检测到更新,同样无需重启;
    3. 在大厅中,使用cc.sys.localStorage存储的值,在子游戏中可以获取到;

本人的一点小思考:

在研究之前,想着一定要研究一下资源共享的问题;
现在想来,既然要将子游戏独立出一个项目,自然也期望以后子游戏可以作为一个单独的apk来运行,如果共用大厅的资源,以后想抽取出来,又是一项艰巨的任务。但是这样必然会造成一定的重复资源。具体取舍,等到项目后期再协调。

作者:陌上冰火
链接:https://www.jianshu.com/p/fe54ca980384
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Cocos creator 大厅子游戏和热更新

工作中,需要把cocos creator创建的多个游戏适配到Android和ios应用中,经过调研,可以利用大厅子游戏模式实现。大厅本身作为一个游戏工程,可以有加载页面,和热加载子游戏。

热更新:
https://www.jianshu.com/p/9fc813fe9e4c

大厅子游戏:
https://www.jianshu.com/p/fe54ca980384

如何动态加载和更新子游戏:
自从jsb 3.0以来,可以用反射调用Android或者ios的代码:

const SubgameManager = require('SubgameManager');

cc.Class({
    extends: cc.Component,

    properties: {

        downloadBtn: {
            default: null,
            type: cc.Node
        },
        
        label: {
            default: null,
            type: cc.Label
        },
        // defaults, set visually when attaching this script to the Canvas
        text: 'Hello, World!'
    },

    // use this for initialization
    onLoad: function () {

        var name = 'subgame';  

        if (cc.sys.OS_ANDROID == cc.sys.os) {
            name = jsb.reflection.callStaticMethod("org/cocos2dx/javascript/GameNameProvider", "getName", "()Ljava/lang/String;");
            console.log("OS_ANDROID platform provides: " + name);
        }  

        if (cc.sys.OS_IOS == cc.sys.os) {
            name = jsb.reflection.callStaticMethod("GameNameProvider", "getName");
            console.log("OS_IOS platform provides: " + name);
            
        }  

        //判断子游戏有没有下载
        if (SubgameManager.isSubgameDownLoad(name)) {
            //已下载,判断是否需要更新
            SubgameManager.needUpdateSubgame(name, (success) => {
                if (success) {
                    this.label.string = "子游戏需要更新";
                    console.log("子游戏需要更新");
                } else {
                    this.label.string = "子游戏不需要更新";
                    console.log("子游戏不需要更新");
                }
            }, () => {
                console.log('出错了');
            });
        } else {
            console.log("子游戏未下载");
            this.label.string = "子游戏未下载";
        }

        this.downloadBtn.on('click', () => {
            //下载子游戏/更新子游戏
            console.log("downloadBtn clicked");
            SubgameManager.downloadSubgame(name, (progress) => {
                if (isNaN(progress)) {
                    progress = 0;
                }
                this.label.string = "资源下载中   " + parseInt(progress * 100) + "%";
                console.log(this.label.string);
            }, function(success) {
                if (success) {
                    SubgameManager.enterSubgame(name);
                    console.log("进入子游戏");
                } else {
                    console.log('下载失败');
                }
            });
        }, this);
    },

    // called every frame
    update: function (dt) {

    },
});

Android代码:

package org.cocos2dx.javascript;

public class GameNameProvider {

    public static String getName() {
        return "subgame";
    }
}

iOS代码:

#import <Foundation/Foundation.h>

@interface GameNameProvider:NSObject {
}

+ (NSString *)getName;
@end

#import "GameNameProvider.h"
#import <Foundation/Foundation.h>

@implementation GameNameProvider

// request login
+ (NSString *) getName {
    return @"subgame";
}
@end

demo地址:
https://github.com/tigershinny/CocosHotupdate

作者:tigershin
链接:https://www.jianshu.com/p/bf76989a6c1f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Cocos Creator子游戏动态下载实现(大厅+子游戏模式)

热更新
在App开发过程中,当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。热更新就是动态下发代码,它可以使开发者在不发布新版本的情况下,修复 BUG 和发布功能,让开发者得以绕开苹果的审核机制,避免长时间的审核等待以及多次被拒造成的成本。

Cocos Creator热更新
相对于app开发,游戏开发中游戏更新相对较频繁,之前在iOS开发时,JSPatch有一套很成熟的框架来实现iOS的热补丁。
Cocos Creator也提供了一种不成熟的方案,可以实现热更新。具体可以参考官方文档:
资源热更新教程
热更新管理器 AssetsManager

在 Cocos2d-x 中,开发方式是以代码来驱动,游戏中的数据大多也是在代码中存储,除非开发者构建了自己的数据驱动框架。在 Cocos Creator 框架中,所有场景都会被序列化为纯数据,在运行时使用这些纯数据来重新构建场景,界面,动画甚至组件等元素。这种开发模式,为我们热更新提供了便利。
详细的数据驱动可以参考官方文档:2. 数据驱动

子游戏下载
主程序的修改,可以通过热更新来更新,那么为了安装包较小,可不可以把所有的子模块都动态下载呢,举个例子:JJ斗地主、网易成都棋牌都是在进入游戏时才对游戏下载。

第一种思路:
就是在Cocos Creator的一个项目中,分成不同的模块进行开发,然后使用热更新来下载每个子游戏,这样可以直接套用官方的模式。
但是一个很明显的缺点就是,多个小游戏之间耦合性太高,而且非常不利于多人协作开发

第二种思路:
因为整个项目都是数据文件,是否可以下载多个项目,通过main.js入口文件来进行跳转,答案是肯定的。

先看一下项目结构,在构建的时候会生成build文件夹,里面有main.js,是项目的入口文件,frameworks是各个平台的文件,res是资源文件,src是js代码生成的源文件

jsb_polyfill.js 文件是 JS 引擎编译出来的,包含了对 C++ 引擎的一些接口封装和 Entity Component 层的代码。在不同版本的引擎中,它的代码会产生比较大的差异,而 C++ 底层也会随之发生一些改变。
project.js,所有的js代码会打包到project.js文件中,在非调试模式时是project.dev.js
setting.js文件是所有资源文件的配置文件,通过uuid进行索引

如果子项目不包括原生代码的话,project.js setting.js 以及res文件夹就包含了所有代码和资源,如果加上main.js的入口文件,那么把这些文件整个打包下载下来,就可以实现子游戏的热更新,但是需要对部分js文件进行修改

具体实现方案
子游戏修改:
1.修改子游戏的main.js文件,并放入 build-templates/jsb-default/src 文件夹中,该文件为跳转入口文件,但是并不替换子游戏的 main.js,因此子游戏独立运行并没有影响
main.js文件的主要改动是从子游戏的路径中读取配置文件等
2.通过Cocos Creator提供的热更新方法,打包文件后, 把src和res文件夹通过version_generator.js生成manifest文件,把生成的两个文件以及src和res文件夹上传到远端服务器中

主程序修改:
1.修改大厅 jsb_polyfill.js 文件,并放入 build-templates/jsb-default/src 文件夹中,jsb_polyfill.js文件和 Creator 的版本有关,因为升级 Creator 时需要重新修改
jsb_polyfill.js修改的目的是,为了实现大厅和子游戏资源文件的共享,因为 cc.AssetLibrary 的实现是会覆盖之前的资源映射关系,因此需要修改系统文件,这样在子游戏中也可以调用大厅的资源文件了

2.也要修改main.js文件
3.实现热更新的方法,具体可以参考官方实现

Demo:https://github.com/wf96390/CocosHotupdateDemo

Demo部署:

  1. 可以使用subgame.sh进行打包,打包文件生成到hotupdate目录中
  2. 如果部署到本地的话,默认是http://127.0.0.1/subgame/目录,你也可以进行修改(打包路径+代码中的热更新路径修改),把上一步生成的src, res, project.manifest, version.manifest复制到服务器subgame文件夹中

参考:
http://forum.cocos.com/t/topic/54899
http://forum.cocos.com/t/topic/53115
————————————————
版权声明:本文为CSDN博主「-慢慢-」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wf96390/java/article/details/80815430

Swoole Timer 的应用

这是关于 Swoole 学习的第一篇文章:Swoole Timer 的应用。

PHP 的协程高性能网络通信引擎,使用 C/C++ 语言编写,提供了多种通信协议的网络服务器和客户端模块。

Swoole 可应用于互联网、移动通信、企业软件、网络游戏、物联网、车联网、智能家庭等领域。

学习 Swoole 之前,最好先了解下底层知识,比如,线程/进程、IO、TCP/IP协议 等。

推荐大家读一下《Linux 高性能服务器编程》这本书。我有这本书的PDF版,需要的可以关注公众号,回复 “Linux 高性能服务器编程” 即可获取。

这篇文章主要分享 Timer 毫秒精度的定时器。

本地版本:PHP 7.2.6、Swoole 4.3.1。

Timer

主要有三个方法:

swoole_timer_tick 间隔的时钟控制器

swoole_timer_after 指定的时间后执行

swoole_timer_clear 删除定时器

示例代码:

//每隔3000ms触发一次
$timer_id = swoole_timer_tick(3000, function () {
    echo "tick 3000ms - ".date('Y-m-d H:i:s')."\n";
});

//9000ms后删除定时器
swoole_timer_after(9000, function () use ($timer_id) {
    echo "after 9000ms - ".date('Y-m-d H:i:s')."\n";
    swoole_timer_clear($timer_id);
});

运行结果:

tick 3000ms - 2019-04-07 21:36:56
tick 3000ms - 2019-04-07 21:36:59
tick 3000ms - 2019-04-07 21:37:02
after 9000ms - 2019-04-07 21:37:02

应用场景

一、比如,每天凌晨跑业务脚本,脚本中包括了请求其他业务方或第三方的接口,如果接口超时无响应或没有数据返回,需要进行重试。

重试机制为:每5隔分钟再发送一次请求,最多尝试5次,在5次内成功停止该任务,5次仍失败也停止该任务。

示例代码:

$api_url  = 'xxx'; //接口地址
$exec_num = 0;     //执行次数
swoole_timer_tick(5*60*1000, function($timer_id) use ($api_url, &$exec_num) {
    $exec_num ++ ;
    $result = $this->requestUrl($api_url);
    echo date('Y-m-d H:i:s'). " 执行任务中...(".$exec_num.")\n";
    if ($result) {
        //业务代码...
        swoole_timer_clear($timer_id); // 停止定时器
        echo date('Y-m-d H:i:s'). " 第(".$exec_num.")次请求接口任务执行成功\n";
    } else {
        if ($exec_num >= 5) {
            swoole_timer_clear($timer_id); // 停止定时器
            echo date('Y-m-d H:i:s'). " 请求接口失败,已失败5次,停止执行\n";
        } else {
            echo date('Y-m-d H:i:s'). " 请求接口失败,5分钟后再次尝试\n";
        }
    }
});

运行结果:

2019-04-07 21:40:48 执行任务中...(1)
2019-04-07 21:40:48 请求接口失败,5分钟后再次尝试
2019-04-07 21:45:48 执行任务中...(2)
2019-04-07 21:45:48 请求接口失败,5分钟后再次尝试
2019-04-07 21:50:48 执行任务中...(3)
2019-04-07 21:50:48 请求接口失败,5分钟后再次尝试
2019-04-07 21:55:48 执行任务中...(4)
2019-04-07 21:55:48 请求接口失败,5分钟后再次尝试
2019-04-07 22:00:48 执行任务中...(5)
2019-04-07 22:00:48 请求接口失败,已失败5次,停止执行

二、比如,设计一个用WEB界面管理管理定时任务的系统。

Linux Crontab 最小时间粒度为分钟。

PHP Swoole 最小时间粒度为毫秒。

0   1   2   3   4   5
|   |   |   |   |   |
|   |   |   |   |   +------ day of week (0 - 6) (Sunday=0)
|   |   |   |   +------ month (1 - 12)
|   |   |   +-------- day of month (1 - 31)
|   |   +---------- hour (0 - 23)
|   +------------ min (0 - 59)
+-------------- sec (0-59)

WEB界面管理

  • 登录、权限管理
  • 任务管理(增删改查)
  • 脚本机管理(机器IP地址)
  • 任务日志

项目地址

https://github.com/osgochina/Donkey

三、比如,监控服务器状况。

参考文档

本文欢迎转发,转发请注明作者和出处,谢谢!

(最佳方案)cocos creator~客户端WebSocket与 node.js ws模块通信

1)node.js服务端ws模块
var ws = require(“ws”); var server = new ws.Server({ port: 6080 });
server.on(“connection”, function (session) { session.on(“close”, function () {
}); session.on(“error”, function (err) { }); session.on(“message”, function
(data) { console.log(“server rcv data=” + data); session.send(data); }); });

2)客户端websocket.js封装
var websocket = { sock: null, on_open: function () {
this.send_data(JSON.stringify({ stype: “auth”, ctype: “login”, data: { name:
“jianan”, pwd: 123456 } })); }, on_message: function (event) {
console.log(“client rcv data=” + event.data); }, on_close: function () {
this.close(); }, on_error: function () { this.close(); }, close: function () {
if(this.sock){ this.sock.close(); this.sock = null; } }, connect: function
(url) { this.sock = new WebSocket(url); this.sock.binaryType = “arraybuffer”;
this.sock.onopen = this.on_open.bind(this); this.sock.onmessage =
this.on_message.bind(this); this.sock.onclose = this.on_close.bind(this);
this.sock.onerror = this.on_error.bind(this); }, send_data: function (data) {
this.sock.send(data); } } module.exports = websocket;
cocos creator客户端连接node.js服务器(此处通信用json协议)
var websocket = require(“./websocket.js”); cc.Class({ extends: cc.Component,
properties: { }, // LIFE-CYCLE CALLBACKS: // onLoad () {}, start () {
websocket.connect(“ws://127.0.0.1:6080/ws”); }, // update (dt) {}, });

总结:

socket.io则注意事项略多,特别需要客户端服务器版本号一致,容易出问题。 推荐使用websocket,而不要使用socket.io !!!

swoole Timer 毫秒定时器

定时器的原理:

 定时器在实际的开发中非常常用,列如crontab就是一个非常常用的定时器,可以在指定的时间执行规定的任务,PHP本身也提pcntl_alarm,通过模拟时钟信号实现定时器,不过这些定时器都有一个缺点,不能精确到毫秒,还有比如执行一个任务,在某些特定的场合想关闭定时器,传统的php也实现不了这些功能。而swoole 弥补了这些缺点。

swoole 的 swoole_timer模块提供了一个毫秒定时器,swoole_timer是基于timerfd+epoll实现的异步毫秒定时器,可完美的运行在EventLoop中,与swoole_client/swoole_event等模块可以无缝结合。

定时器的使用:

  在swoole 的定时器分为两种:一种是长类型的定时器tick,这种定时器一旦被创建,就会按照一定的时间执行任务,除非清除定时器,而另外一种定时器 after是临时定时器,这个定时器仅仅只运作一次,在指定的时间内响应一次,就会自动清除。

1、tick 定时器

    创建一个 tick  定时器的原型 如下:

int swoole_timer_tick(int $ms, mixed $callback, mixed $param = null);int swoole_server::tick(int $ms, mixed $callback, mixed $param = null);

回调函数的原型如下所示: php function onTimer(int $timer_id, mixed $params = null); 第一个参数是响应的timer_id,对应调用tick函数返回的id;第二个参数是调用tick时传入的第三个参数的值。我们可以用这个param参数传递一些公共参数进去(因为这个参数一旦给定,就不能在后续的逻辑中改变了)。另外,也可以使用PHP的闭包(use关键字)实现传参。具体实例如下:

参数名参数说明$ms定时器的间隔,单位是ms$callback回调函数,定时器响应后执行此函数$param用户自定义参数,会被传递给callback回调

tick函数会返回定时器的id。当我们不再需要某个定时器的时候,可以根据这个id,调用swoole_timer_clear函数删除定时器。

2、after 定时器

 创建一个 after 定时器的原型 如下:

int swoole_timer_after(int $after_time_ms, mixed $callback_function, mixed $user_param);

回调函数的原型如下所示:

function onTimer() // 回调函数的原型,不接受任何参数

after定时器的回调函数不接受任何参数,可以通过闭包方式传递参数,也可以通过类成员变量的方式传递。具体实例如下:

class Test{private $str = "Say Hello";public function onAfter(){echo $this->str; // 输出”Say Hello“}}$test = new Test();swoole_timer_after(1000, array($test, "onAfter"); // 成员变量swoole_timer_after(2000, function() use($test){ // 闭包$test->onAfter(); // 输出”Say Hello“});


after定时器适用于某些延后执行的操作,比如用户创建了一个订单需要在5分钟后检查这个订单是否被支付等。这些任务往往需要在间隔一定时间后执行,并且只需要执行一次,就可以直接使用after来实现了。

Cocos Creator 快速上手:制作第一个游戏—–二次开发

快速上手:制作第一个游戏
您正在阅读的手册文档包括了系统化的介绍 Cocos Creator 的编辑器界面、功能和工作流程,但如果您想快速上手体验使用 Cocos Creator 开发游戏的大体流程和方法,这一章将满足您的好奇心。完成本章教程之后,您应该能获得足够上手制作游戏的信息,不过我们还是推荐您继续阅读本手册来了解各个功能模块的细节和完整的工作流程。

接下来就让我们开始吧,跟随教程我们将会制作一款名叫摘星星的坑爹小游戏。这款游戏的玩家要操作一个反应迟钝却蹦跳不停的小怪物来碰触不断出现的星星,难以驾驭的加速度将给玩家带来很大挑战,和您的小伙伴比比谁能拿到更多星星吧!

可以在这里感受一下这款游戏的完成形态:

http://fbdemos.leanapp.cn/star-catcher/

准备项目和资源
我们已经为您准备好了制作这款游戏需要的全部资源,下载初始项目后,解压到您希望的位置,之后我们就可以开始了:

下载初始项目

您也可以下载最终完成的项目,跟随教程制作过程中有任何不明白的地方都可以打开完成版的项目作为参考:

下载完成项目

打开初始项目
如果您还不了解如何获取和启动 Cocos Creator,请阅读安装和启动一节。

我们首先启动 Cocos Creator,然后选择打开其他项目
在弹出的文件夹选择对话框中,选中我们刚下载并解压完成的 start_project,点击打开按钮
Cocos Creator 编辑器主窗口会打开,我们将看到如下的项目状态

检查游戏资源
我们的初始项目中已经包含了所有需要的游戏资源,因此您不需要再导入任何其他资源。如果希望了解导入资源的方法,可以阅读资源工作流程的相关内容。

下面我们先来了解一下项目中都有哪些资源,请关注名为 资源管理器 的面板,这里显示的是项目中的所有资源树状结构。

可以看到,项目资源的根目录名叫assets,对应我们解压之后初始项目中的 assets 目录,只有这个目录下的资源才会被 Cocos Creator 导入项目并进行管理。

资源管理器可以显示任意层次的目录结构,我们可以看到这样的图标就代表一个文件夹,点击文件夹左边的三角图标可以展开文件夹的内容。将文件夹全部展开后, 资源管理器 中就会呈现如下图的状态。

每个资源都是一个文件,导入项目后根据扩展名的不同而被识别为不同的资源类型,其图标也会有所区别,下面我们来看看项目中的资源各自的类型和作用:

 声音文件,一般为 mp3 文件,我们将在主角跳跃和得分时播放名为jump和score的声音文件。
 位图字体,由 fnt 文件和同名的 png 图片文件共同组成。位图字体(Bitmap Font)是一种游戏开发中常用的字体资源,详情请阅读位图字体资源
各式各样的缩略图标,这些都是图像资源,一般是 png 或 jpg 文件。图片文件导入项目后会经过简单的处理成为texture类型的资源。之后就可以将这些资源拖拽到场景或组件属性中去使用了。
创建游戏场景
在 Cocos Creator 中,游戏场景(Scene)是开发时组织游戏内容的中心,也是呈现给玩家所有游戏内容的载体。游戏场景中一般会包括以下内容:

场景图像和文字(Sprite,Label)
角色
以组件形式附加在场景节点上的游戏逻辑脚本
当玩家运行游戏时,就会载入游戏场景,游戏场景加载后就会自动运行所包含组件的游戏脚本,实现各种各样开发者设置的逻辑功能。所以除了资源以外,游戏场景是一切内容创作的基础,让我们现在就新建一个场景。

在资源管理器中点击选中assets目录,确保我们的场景会被创建在这个目录下
点击资源管理器左上角的加号按钮,在弹出的菜单中选择 Scene

我们创建了一个名叫New Scene的场景文件,右键点击它并选择重命名,将它改名为game。

双击game,就会在 场景编辑器 和层级编辑器中打开这个场景。
了解Canvas
打开场景后, 层级管理器 中会显示当前场景中的所有节点和他们的层级关系。我们刚刚新建的场景中只有一个名叫Canvas的节点,Canvas可以被称为画布节点或渲染根节点,点击选中Canvas,可以在 属性检查器 中看到他的属性。

这里的Design Resolution属性规定了游戏的设计分辨率,Fit Height和Fit Width规定了在不同尺寸的屏幕上运行时,我们将如何缩放Canvas以适配不同的分辨率。

由于提供了多分辨率适配的功能,我们一般会将场景中的所有负责图像显示的节点都放在Canvas下面。这样当Canvas的scale(缩放)属性改变时,所有作为其子节点的图像也会跟着一起缩放以适应不同屏幕的大小。

更详细的信息请阅读Canvas组件参考。目前我们只要知道接下来添加的场景图像都会放在Canvas节点下面就可以了。

设置场景图像
添加背景
首先在 资源管理器 里按照assets/textures/background的路径找到我们的背景图像资源,点击并拖拽这个资源到 层级编辑器 中的Canvas节点上,直到Canvas节点显示橙色高亮,表示将会添加一个以background为贴图资源的子节点。

这时就可以松开鼠标按键,可以看到Canvas下面添加了一个名叫background的节点。当我们使用拖拽资源的方式添加节点时,节点会自动以贴图资源的文件名来命名。

我们在对场景进行编辑修改时,可以通过主菜单文件->保存场景来及时保存我们的修改。也可以使用快捷键Ctrl+S(Windows)或Cmd + S(Mac)来保存。

修改背景尺寸

在 场景编辑器 中,可以看到我们刚刚添加的背景图像,下面我们将修改背景图像的尺寸,来让他覆盖整个屏幕。

首先选中background节点,然后点击主窗口左上角工具栏第四个 矩形变换工具:

使用这个工具我们可以方便的修改图像节点的尺寸,将鼠标移动到 场景编辑器 中 background 的左边,按住并向左拖拽直到 background 的左边超出表示设计分辨率的蓝色线框。然后再用同样的方法将 background 的右边向右拖拽。

之后需要拖拽上下两边,使背景图的大小能够填满设计分辨率的线框。

在使用 矩形变换工具 修改背景图尺寸时,可以在 属性检查器 中看到 Node (节点)中的 Size 属性也在随之变化,完成后我们的背景图尺寸大概是 (1360, 760)。您也可以直接在 Size 属性的输入框中输入数值,和使用 矩形变换工具 可以达到同样效果。这样大小的背景图在市面流行的手机上都可以覆盖整个屏幕,不会出现穿帮情况。

添加地面
我们的主角需要一个可以在上面跳跃的地面,我们马上来添加一个。用和添加背景图相同的方式,拖拽 资源管理器 中 assets/textures/ground 资源到 层级管理器 的 Canvas 上。这次在拖拽时我们还可以选择新添加的节点和 background节点的顺序关系。拖拽资源的状态下移动鼠标指针到 background 节点的下方,直到在 Canvas 上显示橙色高亮框,并同时在 background 下方显示表示插入位置的绿色线条,然后松开鼠标,这样 ground 在场景层级中就被放在了 background 下方,同时也是 Canvas 的一个子节点。

在 层级管理器 中,显示在下方的节点的渲染顺序是在上方节点的后面,也就是说下方的节点是在上方节点之后绘制的,我们可以看到位于最下的ground物体在 场景编辑器 中显示在了最前。另外子节点也会永远显示在父节点之前,我们可以随时调整节点的层级顺序和关系来控制他们的显示顺序。

按照修改背景的方法,我们也可以使用矩形变换工具来为地面节点设置一个合适的大小。在激活矩形变换工具的时候,如果拖拽节点顶点和四边之外的部分,就可以更改节点的位置。下图是我们设置好的地面节点状态:

除了矩形变换工具之外,我们还可以使用移动工具来改变节点的位置。尝试按住移动工具显示在节点上的箭头并拖拽,就可以一次改变节点在单个坐标轴上的位置。

我们在设置背景和地面的位置和尺寸时不需要很精确的数值,可以凭感觉拖拽。如果您偏好比较完整的数字,也可以按照截图直接输入position和size的数值。

添加主角
接下来我们的主角小怪兽要登场了,从 资源管理器 拖拽assets/texture/PurpleMonster到 层级管理器 中 Canvas 的下面,并确保他的排序在 ground 之下,这样我们的主角会显示在最前面。 注意小怪兽节点应该是 Canvas 的子节点,和 ground 节点平行。

为了让主角的光环在场景节点中非常醒目,我们右键点击刚刚添加的PurpleMonster节点,选择重命名之后将其改名为Player。

接下来我们要对主角的属性进行一些设置,首先是改变锚点(Anchor)的位置。默认状态下,任何节点的锚点都会在节点的中心,也就是说该节点中心点所在的位置就是该节点的位置。我们希望控制主角的底部的位置来模拟在地面上跳跃的效果,所以现在我们需要把主角的锚点设置在脚下。在 属性检查器 里找到Anchor属性,把其中的y值设为0,可以看到 场景编辑器 中,表示主角位置的移动工具的箭头出现在了主角脚下。

接下来 场景编辑器 中拖拽Player,把他放在地面上,效果如下图:

这样我们基本的场景美术内容就配置好了。下面一节我们要编写代码让游戏里的内容生动起来。

编写主角脚本
Cocos Creator 开发游戏的一个核心理念就是让内容生产和功能开发可以流畅的并行协作,我们在上个部分着重于处理美术内容,而接下来就是通过编写脚本来开发功能的流程,之后我们还会看到写好的程序脚本可以很容易的被内容生产者使用。

如果您从没写过程序也不用担心,我们会在教程中提供所有需要的代码,只要复制粘贴到正确的位置就可以了,之后这部分工作可以找您的程序员小伙伴来解决。下面让我们开始创建驱动主角行动的脚本吧。

创建脚本
首先在 资源管理器 中右键点击assets文件夹,选择新建->文件夹
右键点击New Folder,选择重命名,将其改名为scripts,之后我们所有的脚本都会存放在这里。
右键点击scripts文件夹,选择新建->JavaScript,创建一个JavaScript脚本
将新建脚本的名字改为Player。双击这个脚本,打开代码编辑器。
注意: Cocos Creator 中脚本名称就是组件的名称,这个命名是大小写敏感的!如果组件名称的大小写不正确,将无法正确通过名称使用组件!

编写组件属性
打开的脚本里已经有了预先设置好的一些代码块,这些代码就是编写一个组件脚本所需的结构。具有这样结构的脚本就是 Cocos Creator 中的组件(Component),他们能够挂载到场景中的节点上,提供控制节点的各种功能。我们先来设置一些属性,然后看看怎样在场景中调整他们。

在 代码编辑器 中找到Player脚本里 properties部分,将其改为以下内容并按Ctrl + S(Windows)或Cmd + S(Mac)保存:

// Player.js
//…
properties: {
// 主角跳跃高度
jumpHeight: 0,
// 主角跳跃持续时间
jumpDuration: 0,
// 最大移动速度
maxMoveSpeed: 0,
// 加速度
accel: 0,
},
//…
这些新增的属性将规定主角的移动方式,在代码中我们不需要关心这些数值是多少,因为我们之后会直接在 属性检查器 中设置这些数值。

现在我们可以把 Player 组件添加到主角节点上。在 层级编辑器 中选中 Player 节点,然后在 属性检查器 中点击 添加组件 按钮,选择 添加用户脚本组件->Player,为主角节点添加 Player 组件。

现在我们可以在 属性检查器 中(需要选中 Player 节点)看到刚添加的 Player 组件了,按照下图将主角跳跃和移动的相关属性设置好:

这些数值除了 jumpDuration 的单位是秒之外,其他的数值都是以像素为单位的,根据我们现在对 Player 组件的设置:我们的主角将能够跳跃 200 像素的高度,起跳到最高点所需的时间是 0.3 秒,最大水平方向移动速度是 400 像素每秒,水平加速度是 350 像素每秒。

这些数值都是建议,一会等游戏运行起来,您完全可以按照自己的喜好随时在 属性检查器 中修改这些数值,不需要改动任何代码,很方便吧!

编写跳跃和移动代码
下面我们添加一个方法,来让主角跳跃起来,在 properties: {…},代码块的下面,添加叫做setJumpAction的方法:

// Player.js
properties: {
//…
},

setJumpAction: function () {
    // 跳跃上升
    var jumpUp = cc.moveBy(this.jumpDuration, cc.p(0, this.jumpHeight)).easing(cc.easeCubicActionOut());
    // 下落
    var jumpDown = cc.moveBy(this.jumpDuration, cc.p(0, -this.jumpHeight)).easing(cc.easeCubicActionIn());
    // 不断重复
    return cc.repeatForever(cc.sequence(jumpUp, jumpDown));
},

这里用到了一些 Cocos2d-js 引擎中的 Action 来实现主角的跳跃动画,详情可以查询Cocos2d-js API.

接下来在onLoad方法里调用刚添加的setJumpAction方法,然后执行runAction来开始动作:

// Player.js
onLoad: function () {
// 初始化跳跃动作
this.jumpAction = this.setJumpAction();
this.node.runAction(this.jumpAction);
},
onLoad方法会在场景加载后立刻执行,所以我们会把初始化相关的操作和逻辑都放在这里面。

保存脚本,然后我们就可以开始第一次运行游戏了!

点击 Cocos Creator 编辑器上方正中的预览游戏按钮,Cocos Creator 会自动打开您的默认浏览器并开始在里面运行游戏,现在应该可以看到我们的主角——紫色小怪兽在场景中间活泼的蹦个不停了。

移动控制
只能在原地傻蹦的主角可没前途,让我们为主角添加键盘输入,用A和D来控制他的跳跃方向。在setJumpAction方法的下面添加setInputControl方法:

// Player.js
setJumpAction: function () {
//…
},

setInputControl: function () {
    var self = this;
    // 添加键盘事件监听
    // 有按键按下时,判断是否是我们指定的方向控制键,并设置向对应方向加速
    cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, function (event){
        switch(event.keyCode) {
            case cc.KEY.a:
                self.accLeft = true;
                break;
            case cc.KEY.d:
                self.accRight = true;
                break;
        }
    });

    // 松开按键时,停止向该方向的加速
    cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, function (event){
        switch(event.keyCode) {
            case cc.KEY.a:
                self.accLeft = false;
                break;
            case cc.KEY.d:
                self.accRight = false;
                break;
        }
    });
},

然后修改onLoad方法,在其中加入向左和向右加速的开关,以及主角当前在水平方向的速度,最后再调用我们刚添加的setInputControl方法,在场景加载后就开始监听键盘输入:

// Player.js
onLoad: function () {
// 初始化跳跃动作
this.jumpAction = this.setJumpAction();
this.node.runAction(this.jumpAction);

    // 加速度方向开关
    this.accLeft = false;
    this.accRight = false;
    // 主角当前水平方向速度
    this.xSpeed = 0;

    // 初始化键盘输入监听
    this.setInputControl();
},

最后修改update方法的内容,添加加速度、速度和主角当前位置的设置:

// Player.js
update: function (dt) {
// 根据当前加速度方向每帧更新速度
if (this.accLeft) {
this.xSpeed -= this.accel * dt;
} else if (this.accRight) {
this.xSpeed += this.accel * dt;
}
// 限制主角的速度不能超过最大值
if ( Math.abs(this.xSpeed) > this.maxMoveSpeed ) {
// if speed reach limit, use max speed with current direction
this.xSpeed = this.maxMoveSpeed * this.xSpeed / Math.abs(this.xSpeed);
}

    // 根据当前速度更新主角的位置
    this.node.x += this.xSpeed * dt;
},

update在场景加载后就会每帧调用一次,我们一般把需要经常计算或及时更新的逻辑内容放在这里。在我们的游戏中,根据键盘输入获得加速度方向后,就需要每帧在update中计算主角的速度和位置。

保存脚本后,下面就可以去泡杯茶,点击预览游戏来看看我们最新的成果。在浏览器打开预览后,用鼠标点击一下游戏画面(这是浏览器的限制,要点击游戏画面才能接受键盘输入),然后就可以按A和D键来控制主角左右移动了!

感觉移动起来有点迟缓?主角跳的不够高?希望跳跃时间长一些?没问题,这些都可以随时调整。只要为Player组件设置不同的属性值,就可以按照您的想法调整游戏。这里有一组设置可供参考:

Jump Height: 150
Jump Duration: 0.3
Max Move Speed: 400
Accel: 1000
这组属性设置会让主角变得灵活无比,至于如何选择,就看您想做一个什么风格的游戏了。

制作星星
主角现在可以跳来跳去了,我们要给玩家一个目标,也就是会不断出现在场景中的星星,玩家需要引导小怪兽碰触星星来收集分数。被主角碰到的星星会消失,然后马上在随机位置重新生成一个。

制作Prefab
对于需要重复生成的节点,我们可以将他保存成 Prefab(预制) 资源,作为我们动态生成节点时使用的模板。关于 Prefab的更多信息,请阅读 预制资源(Prefab)。

首先从 资源管理器 中拖拽 assets/textures/star 图片到场景中,位置随意,我们只是需要借助场景作为我们制作 Prefab 的工作台,制作完成后会我们把这个节点从场景中删除。

我们不需要修改星星的位置或渲染属性,但要让星星能够被主角碰触后消失,我们需要为星星也添加一个专门的组件。按照和添加 Player 脚本相同的方法,添加名叫 Star 的 JavaScript 脚本到 assets/scripts/ 中。

接下来双击这个脚本开始编辑,星星组件只需要一个属性用来规定主角距离星星多近时就可以完成收集,修改 properties,加入以下内容:

// Star.js
properties: {
// 星星和主角之间的距离小于这个数值时,就会完成收集
pickRadius: 0,
// 暂存 Game 对象的引用
game: {
default: null,
serializable: false
}
},
保存脚本后,将这个脚本添加到刚创建的 star 节点上。然后在 属性检查器 中把 Pick Radius 属性值设为 60:

Star Prefab 需要的设置就完成了,现在从 层级管理器 中将 star 节点拖拽到 资源管理器 中的 assets 文件夹下,就生成了名叫 star 的 Prefab 资源。

现在可以从场景中删除 star 节点了,后续可以直接双击这个 star Prefab 资源进行编辑。

接下去我们会在脚本中动态使用星星的 Prefab 资源生成星星。

添加游戏控制脚本
星星的生成是游戏主逻辑的一部分,所以我们要添加一个叫做Game的脚本作为游戏主逻辑脚本,这个脚本之后还会添加计分、游戏失败和重新开始的相关逻辑。

添加Game脚本到assets/scripts文件夹下,双击打开脚本。首先添加生成星星需要的属性:

// Game.js
properties: {
// 这个属性引用了星星预制资源
starPrefab: {
default: null,
type: cc.Prefab
},
// 星星产生后消失时间的随机范围
maxStarDuration: 0,
minStarDuration: 0,
// 地面节点,用于确定星星生成的高度
ground: {
default: null,
type: cc.Node
},
// player 节点,用于获取主角弹跳的高度,和控制主角行动开关
player: {
default: null,
type: cc.Node
}
},
保存脚本后将Game组件添加到 层级编辑器 中的Canvas节点上(选中Canvas节点后,拖拽脚本到 属性检查器 上,或点击 属性检查器 的 添加组件 按钮,并从 用户自定义脚本 中选择 Game,接下来从 资源管理器 中拖拽star Prefab 资源到Game组件的Star Prefab属性中。这是我们第一次为属性设置引用,只有在属性声明时规定type为引用类型时(比如我们这里写的cc.Prefab类型),才能够将资源或节点拖拽到该属性上。

接下来从 层级编辑器 中拖拽ground和Player 节点到组件中相同名字的属性上,完成节点引用。

然后设置Min Star Duration和Max Star Duration属性的值为3和5,之后我们生成星星时,会在这两个之间随机取值,就是星星消失前经过的时间。

在随机位置生成星星
接下来我们继续修改Game脚本,在onLoad方法后面添加生成星星的逻辑:

// Game.js
onLoad: function () {
// 获取地平面的 y 轴坐标
this.groundY = this.ground.y + this.ground.height/2;
// 生成一个新的星星
this.spawnNewStar();
},

spawnNewStar: function() {
    // 使用给定的模板在场景中生成一个新节点
    var newStar = cc.instantiate(this.starPrefab);
    // 将新增的节点添加到 Canvas 节点下面
    this.node.addChild(newStar);
    // 为星星设置一个随机位置
    newStar.setPosition(this.getNewStarPosition());
},

getNewStarPosition: function () {
    var randX = 0;
    // 根据地平面位置和主角跳跃高度,随机得到一个星星的 y 坐标
    var randY = this.groundY + cc.random0To1() * this.player.getComponent('Player').jumpHeight + 50;
    // 根据屏幕宽度,随机得到一个星星 x 坐标
    var maxX = this.node.width/2;
    randX = cc.randomMinus1To1() * maxX;
    // 返回星星坐标
    return cc.p(randX, randY);
}

保存脚本以后点击预览游戏按钮,在浏览器中可以看到,游戏开始后动态生成了一颗星星!用同样的方法,您可以在游戏中动态生成任何预先设置好的以 Prefab 为模板的节点。

添加主角碰触收集星星的行为
现在要添加主角收集星星的行为逻辑了,这里的重点在于,星星要随时可以获得主角节点的位置,才能判断他们之间的距离是否小于可收集距离,如何获得主角节点的引用呢?别忘了我们前面做过的两件事:

Game组件中有个名叫player的属性,保存了主角节点的引用。
每个星星都是在Game脚本中动态生成的。
所以我们只要在Game脚本生成Star节点实例时,将Game组件的实例传入星星并保存起来就好了,之后我们可以随时通过game.player来访问到主角节点。让我们打开Game脚本,在spawnNewStar方法最后面添加这样一句:

// Game.js
spawnNewStar: function() {
// …
// 将 Game 组件的实例传入星星组件
newStar.getComponent(‘Star’).game = this;
},
保存后打开Star脚本,现在我们可以利用Game组件中引用的player节点来判断距离了,在onLoad方法后面添加名为getPlayerDistance和onPicked的方法:

// Star.js
getPlayerDistance: function () {
// 根据 player 节点位置判断距离
var playerPos = this.game.player.getPosition();
// 根据两点位置计算两点之间距离
var dist = cc.pDistance(this.node.position, playerPos);
return dist;
},

onPicked: function() {
    // 当星星被收集时,调用 Game 脚本中的接口,生成一个新的星星
    this.game.spawnNewStar();
    // 然后销毁当前星星节点
    this.node.destroy();
},

然后在update方法中添加每帧判断距离,如果距离小于pickRadius属性规定的收集距离,就执行收集行为:

// Star.js
update: function (dt) {
// 每帧判断和主角之间的距离是否小于收集距离
if (this.getPlayerDistance() < this.pickRadius) {
// 调用收集行为
this.onPicked();
return;
}
},
保存脚本,然后再次预览测试,可以看到控制主角靠近星星时,星星就会消失掉,然后在随机位置生成了新的星星!

添加得分
小怪兽辛辛苦苦的收集星星,没有奖励怎么行,让我们现在就在收集星星时添加得分奖励的逻辑和显示。

添加分数文字(Label)
游戏开始时得分从0开始,每收集一个星星分数就会加1。要显示得分,首先要创建一个 Label 节点。在 层级管理器 中选中Canvas节点,右键点击并选择菜单中的创建新节点->创建渲染节点->Label(文字),一个新的 Label 节点会被创建在Canvas下面,而且顺序在最下面。接下来我们要用如下的步骤配置这个 Label 节点:

将该节点名字改为score
将score节点的位置(position属性)设为(0, 180)。
选中该节点,编辑Label组件的string属性,填入Score: 0的文字。
将Label组件的Font Size属性设为50。
从 资源管理器 中拖拽assets/mikado_outline_shadow位图字体资源(注意图标是)到Label组件的Font属性中,将文字的字体替换成我们项目资源中的位图字体。
完成后效果如下图所示:

在 Game 脚本中添加得分逻辑
我们将会把计分和更新分数显示的逻辑放在Game脚本里,打开Game脚本开始编辑,首先在properties区块的最后添加分数显示 Label 的引用属性:

// Game.js
properties: {
// …
// score label 的引用
scoreDisplay: {
default: null,
type: cc.Label
}
},
接下来在onLoad方法里添加计分用的变量的初始化:

// Game.js
onLoad: function () {
// …
// 初始化计分
this.score = 0;
},
然后在update方法后面添加名叫gainScore的新方法:

// Game.js
gainScore: function () {
this.score += 1;
// 更新 scoreDisplay Label 的文字
this.scoreDisplay.string = ‘Score: ‘ + this.score.toString();
},
保存 Game 脚本后,回到 层级管理器,选中 Canvas 节点,然后把前面添加好的 score 节点拖拽到 属性检查器 里 Game 组件的 Score Display 属性中。

在 Star 脚本中调用 Game 中的得分逻辑
下面打开Star脚本,在onPicked方法中加入gainScore的调用:

// Star.js
onPicked: function() {
// 当星星被收集时,调用 Game 脚本中的接口,生成一个新的星星
this.game.spawnNewStar();
// 调用 Game 脚本的得分方法
this.game.gainScore();
// 然后销毁当前星星节点
this.node.destroy();
},
保存后预览,可以看到现在收集星星时屏幕正上方显示的分数会增加了!

失败判定和重新开始
现在我们的游戏已经初具规模,但得分再多,不可能失败的游戏也不会给人成就感。现在让我们加入星星定时消失的行为,而且让星星消失时就判定为游戏失败。也就是说,玩家需要在每颗星星消失之前完成收集,并不断重复这个过程完成玩法的循环。

为星星加入计时消失的逻辑
打开Game脚本,在onLoad方法的spawnNewStar调用之前加入计时需要的变量声明:

// Game.js
onLoad: function () {
// …
// 初始化计时器
this.timer = 0;
this.starDuration = 0;
// 生成一个新的星星
this.spawnNewStar();
// 初始化计分
this.score = 0;
},
然后在spawnNewStar方法最后加入重置计时器的逻辑,其中this.minStarDuration和this.maxStarDuration是我们一开始声明的Game组件属性,用来规定星星消失时间的随机范围:

// Game.js
spawnNewStar: function() {
// …
// 重置计时器,根据消失时间范围随机取一个值
this.starDuration = this.minStarDuration + cc.random0To1() * (this.maxStarDuration – this.minStarDuration);
this.timer = 0;
},
在update方法中加入计时器更新和判断超过时限的逻辑:

// Game.js
update: function (dt) {
// 每帧更新计时器,超过限度还没有生成新的星星
// 就会调用游戏失败逻辑
if (this.timer > this.starDuration) {
this.gameOver();
return;
}
this.timer += dt;
},
最后加入gameOver方法,游戏失败时重新加载场景。

// Game.js
gameOver: function () {
this.player.stopAllActions(); //停止 player 节点的跳跃动作
cc.director.loadScene(‘game’);
}
对Game脚本的修改就完成了,保存脚本,然后打开Star脚本,我们需要为即将消失的星星加入简单的视觉提示效果,在update方法最后加入以下代码:

// Star.js
update: function() {
// …
// 根据 Game 脚本中的计时器更新星星的透明度
var opacityRatio = 1 – this.game.timer/this.game.starDuration;
var minOpacity = 50;
this.node.opacity = minOpacity + Math.floor(opacityRatio * (255 – minOpacity));
}
保存Star脚本,我们的游戏玩法逻辑就全部完成了!现在点击预览游戏按钮,我们在浏览器看到的就是一个有核心玩法、激励机制、失败机制的合格游戏了。

加入音效
尽管很多人玩手游的时候会无视声音,我们为了教程展示的工作流程尽量完整,还是要补全加入音效的任务。

跳跃音效
首先加入跳跃音效,打开Player脚本,添加引用声音文件资源的jumpAudio属性:

// Player.js
properties: {
// …
// 跳跃音效资源
jumpAudio: {
default: null,
url: cc.AudioClip
},
},
然后改写setJumpAction方法,插入播放音效的回调,并通过添加playJumpSound方法来播放声音:

// Player.js
setJumpAction: function () {
// 跳跃上升
var jumpUp = cc.moveBy(this.jumpDuration, cc.p(0, this.jumpHeight)).easing(cc.easeCubicActionOut());
// 下落
var jumpDown = cc.moveBy(this.jumpDuration, cc.p(0, -this.jumpHeight)).easing(cc.easeCubicActionIn());
// 添加一个回调函数,用于在动作结束时调用我们定义的其他方法
var callback = cc.callFunc(this.playJumpSound, this);
// 不断重复,而且每次完成落地动作后调用回调来播放声音
return cc.repeatForever(cc.sequence(jumpUp, jumpDown, callback));
},

playJumpSound: function () {
    // 调用声音引擎播放声音
    cc.audioEngine.playEffect(this.jumpAudio, false);
},

得分音效
保存Player脚本以后打开Game脚本,来添加得分音效,首先仍然是在properties中添加一个属性来引用声音文件资源:

// Game.js
properties: {
// …
// 得分音效资源
scoreAudio: {
default: null,
url: cc.AudioClip
}
},
然后在gainScore方法里插入播放声音的代码:

// Game.js
gainScore: function () {
this.score += 1;
// 更新 scoreDisplay Label 的文字
this.scoreDisplay.string = ‘Score: ‘ + this.score.toString();
// 播放得分音效
cc.audioEngine.playEffect(this.scoreAudio, false);
},
保存脚本,回到 层级编辑器 ,选中Player节点,然后从 资源管理器 里拖拽assets/audio/jump资源到Player组件的Jump Audio属性上。

然后选中Canvas节点,把assets/audio/score资源拖拽到Game组件的Score Audio属性上。

这样就大功告成了!完成形态的场景层级和各个关键组件的属性如下:

现在我们可以尽情享受刚制作完成的游戏了,您能打到多少分呢?别忘了您可以随时修改Player和Game组件里的移动控制和星星持续时间等游戏参数,来快速调节游戏的难度。修改组件属性之后需要保存场景,修改后的数值才会被记录下来。

到这里都是官网上面所写的。

下面进入二次开发。

官网上提供的这个游戏只有一个场景。那我就想是不是应该有一个开始场景还有一个结束场景,在结束场景中是不是应该有这次游戏的一个分数和一个重生玩一次的按键。

接下来我们就开始把这二个场景加入进来。

1.把官网上的完整代码下载下来。地址:https://github.com/cocos-creator/tutorial-first-game/releases/download/v1.2/complete_project.zip

2.导入到cocos creator。

3.打开导入的项目。现在我们开始进行二次开发。

我们创建二个场景:start、end

双击start场景,给start场景添加background 和 ground二张背景图片,和一个button,

在scripts下面新建start、end。

start文件

cc.Class({
extends: cc.Component,

properties: {

},

// LIFE-CYCLE CALLBACKS:

// onLoad () {},
btnClick:function(){
    console.log("btnClick");
    cc.director.loadScene("game");
},
//start () {},

// update (dt) {},

});

end文件

cc.Class({
extends: cc.Component,

properties: {
    // 暂存 Game 对象的引用
    // score label 的引用
    scores: {
        default: null,
        type: cc.Label
    },

},

// LIFE-CYCLE CALLBACKS:

onLoad () {
   // cc.log(Global.scorezf)
  this.scores.string  = 'gameOver Score:'+ Global.scorezf.toString();
},
btnClick:function(){
    // cc.log(this.game.score.toString())
    cc.director.loadScene('game');
},
//start () {},

// update (dt) {},

});

2.修改控制主角方向的事件。

打开Player文件,修改setInputControl方法

setInputControl: function () {
var self = this;
//add keyboard input listener to jump, turnLeft and turnRight
cc.eventManager.addListener({
// event: cc.EventListener.KEYBOARD,
event: cc.EventListener.TOUCH_ONE_BY_ONE,
// swallowTouches: false, // 设置是否吞没事件,在 onTouchBegan 方法返回 true 时吞掉事件,不再向下传递。
onTouchBegan: function (touch, event) { //实现 onTouchBegan 事件处理回调函数
// var target = event.getCurrentTarget(); // 获取事件所绑定的 target, 通常是cc.Node及其子类
//
// // 获取当前触摸点相对于按钮所在的坐标
// var locationInNode = target.convertToNodeSpace(touch.getLocation());
var canvas = cc.find(‘Canvas’);
var locationInNode = canvas.convertToNodeSpace(touch.getLocation());
cc.log(cc.director.getWinSize().width);
if(cc.director.getWinSize().width/2>locationInNode.x){
self.accLeft = true;
self.accRight = false;
this.xSpeed=0;
}else{
self.accLeft = false;
self.accRight = true;
this.xSpeed=0;
}
return true;
// var s = target.getContentSize();
// var rect = cc.rect(0, 0, s.width, s.height);

            //  if (cc.rectContainsPoint(rect, locationInNode)) {        // 判断触摸点是否在按钮范围内
        //    cc.log("sprite began... x = " + locationInNode.x + ", y = " + locationInNode.y);
            //    target.opacity = 180;
            //      return true;
            // }
            //   return false;
        },
        onTouchMoved: function (touch, event) {
            cc.log('Touch Moved: ' + event);
        },
        onTouchEnded: function (touch, event) {
            cc.log('Touch Ended: ' + event);
            var canvas = cc.find('Canvas');
            var locationInNode = canvas.convertToNodeSpace(touch.getLocation());

          //  cc.log(cc.director.getWinSize());
            if(cc.director.getWinSize().width/2>locationInNode.x){
                self.accLeft = false;
                self.accRight = false;
            }else{
                self.accLeft = false;
                self.accRight = false;
            }
        },
        onTouchCancelled: function (touch, event) {
            cc.log('Touch Cancelled: ' + event);
            // var target = event.getCurrentTarget();    // 获取事件所绑定的 target, 通常是cc.Node及其子类
            //
            // // 获取当前触摸点相对于按钮所在的坐标
            // var locationInNode = target.convertToNodeSpace(touch.getLocation());
            //
            // cc.log(cc.director.getWinSize());
            // if(cc.director.getWinSize().width/2>locationInNode.x){
            //     self.accLeft = false;
            //     self.accRight = false;
            // }else{
            //     self.accLeft = false;
            //     self.accRight = false;
            // }
        }





        // set a flag when key pressed
        // onKeyPressed: function(keyCode, event) {
        //     switch(keyCode) {
        //         case cc.KEY.a:
        //             self.accLeft = true;
        //             self.accRight = false;
        //             break;
        //         case cc.KEY.d:
        //             self.accLeft = false;
        //             self.accRight = true;
        //             break;
        //     }
        // },
        // // unset a flag when key released
        // onKeyReleased: function(keyCode, event) {
        //     switch(keyCode) {
        //         case cc.KEY.a:
        //             self.accLeft = false;
        //             break;
        //         case cc.KEY.d:
        //             self.accRight = false;
        //             break;
        //     }
        // }
    }, self.node);
    //cc.Node.EventType.TOUCH_MOVE

},

这个主要就不会跳屏幕外面去了。

最后构建发布时把初始场景设置为start.fire

下面是游戏运行效果:

————————————————
版权声明:本文为CSDN博主「qq_22109175」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_22109175/article/details/80569911

CocosCreator教程(入门篇)

目录
一、项目结构
二、资源分类
三、资源小知识点
四、场景小知识点
五、子系统重点
六、脚本开发
七、发布游戏

系列教程
CocosCreator教程(初识篇)
CocosCreator教程(编辑器篇)


一、项目结构

ProjectName(新建项目)
├──assets
├──library
├──local
├──packages
├──settings
├──temp
└──project.json
子结构功能
assets与资源管理器的内容同步,游戏的核心目录(每个文件都有相应.meta文件)
library这里文件的结构和资源的格式将被处理成最终游戏发布时需要的形式
local编辑器使用习惯记录(如:窗体布局)
settings项目设置
project.json版本控制,必须与assets共同存在
build打包导出目录,构建项目时,自动生成

PS:.meta文件——记录某资源在项目中的唯一标识,以及其配置信息,只有在编辑器中对资源做修改,.meta文件才会实时变化。因此,不要在编辑器外,对资源的内容进行操作。


二、资源分类

1、场景(scene)

自动释放资源:切换场景后,上一个场景中的资源,从内存中释放。
延迟加载资源:意味着不用等待所有资源加载完毕,才显示场景。(快速切换场景,资源陆续在画面显示)

2、贴图(texture)

普通图,子层为一张spriteFrame。

3、预制(prefab)

创建方式:拖拽场景节点,到资源管理器。

4、图集(atlas)

精灵图,子层为多张spriteFrame。(精灵图合成软件:TexturePacker、Zwoptex)

5、自动图集(auto-atlas)

打包时,将所在目录中的所有碎图,合成为图集。

6、艺术数字(label-atlas)

数字为内容的图集。

7、字体(font)

动态字体:.ttf
位图字体:.fnt + .png(存在于同一目录)

8、粒子(particle)

小型动画

9、声音(audio)

模式:web audio、dom audio

10、骨骼动画(spine / dragonBones)
文件格式功能
.json骨骼数据
.png图集纹理
.txt / .atlas图集数据
11、瓦片图(tiledMap)
文件格式功能
.tmx地图数据
.png图集纹理
.tsx tileset数据配置文件
12、文本(text)
13、脚本(script)
14、json

三、资源小知识点

1、跨项目导入导出资源

操作流程:
(1)导出:文件 => 资源导出,选择 .fire场景文件,输出assets目录的 .zip压缩包。
(2)导入:文件 => 资源导入,选择压缩包源路径、解压路径,输出assets目录内容。

2、图像资源自动剪裁

基于size mode,尽量去除spriteFrame无像素的部分,减小图片尺寸。


四、场景小知识点

1、场景中的元素,即是节点,可内嵌组件。
2、坐标系
类别坐标轴方向
cocos坐标系(世界、本地坐标系)x右、y上、z外
设备屏幕坐标系x右、y下
3、锚点

作用:用于变换、子节点定位基准。


五、子系统重点

1、渲染系统

对摄像机、渲染组件的了解。

2、UI系统

对widget、layout等UI组件的了解。

3、动画系统

(1)创建动画的基本流程
(2)时间曲线(双击动画线,进入编辑窗口)
(3)事件管理(双击游标、加减按钮控制参数个数)
(4)脚本控制

4、物理系统

碰撞组件(普通碰撞)
(1)editing——是否为编辑模式
(2)regenerate points——计算图形边界,自定生成控制点,数值为控制点的生成密度 / 准确度
(3)ctrl + 点击——删除控制点
(4)组件类型:矩形、圆形、多边形
(5)设置碰撞组(项目 => 项目设置 => 分组设置):
制定分组 => 匹配分组 => 碰撞组件所在节点上,设置所属分组
(6)脚本控制

Box2D物理引擎(高级碰撞)

5、音频系统

(1)audioSource组件
(2)脚本控制


六、脚本开发

1、使用 cc.Class 声明类型

(1)定义 CCClass

var Sprite = cc.Class({
    //...
});

(2)实例化

var obj = new Sprite();

(3)判断类型

cc.log(obj instanceof Sprite);       //使用原生JS的instanceof 

(4)构造函数(ctor)

var Sprite = cc.Class({
    //使用ctor声明构造函数
    ctor: function () {
        cc.log(this instanceof Sprite);
    }
});

(5)实例方法

var Sprite = cc.Class({
    // 声明一个名叫 "print" 的实例方法
    print: function () { }
});

(6)继承(extends)

// 父类
var Shape = cc.Class();

// 子类
var Rect = cc.Class({
    //使用 extends 实现继承
    extends: Shape
});

(7)父构造函数

var Shape = cc.Class({
    ctor: function () {
        cc.log("Shape");    // 实例化时,父构造函数会自动调用,
    }
});

var Rect = cc.Class({
    extends: Shape
});

var Square = cc.Class({
    extends: Rect,
    ctor: function () {
        cc.log("Square");   // 再调用子构造函数
    }
});

var square = new Square();

(8)完整声明属性

//简单类型声明
properties: {
    score: {
        //这几个参数分别指定了 score 的默认值为 0,在 属性检查器 里,其属性名将显示为:“Score (player)”,并且当鼠标移到参数上时,显示对应的 Tooltip。
        default: 0,
        displayName: "Score (player)",
        tooltip: "The score of player",
    }
}

//数组声明
properties: {
    names: {
        default: [],
        type: [cc.String]   // 用 type 指定数组的每个元素都是字符串类型
    },

    enemies: {
        default: [],
        type: [cc.Node]     // type 同样写成数组,提高代码可读性
    },
}

//get/set 声明
properties: {
    width: {
        get: function () {
            return this._width;
        },
        set: function (value) {
            this._width = value;
        }
    }
}

properties常用参数

参数作用
default默认值
type限定属性的数据类型
visible若为false,则不在属性检查器面板中显示该属性
serializable若为false,则不序列化(保存)该属性
displayName在属性检查器面板中,显示成指定名字
tooltip在属性检查器面板中,添加属性的Tooltip
2、访问节点和组件

(1)获得组件所在的节点

this.node

(2)获得其它组件

this.getComponent(组件名)

(3)获得其它节点及其组件

// Player.js
cc.Class({
    extends: cc.Component,
    properties: {
        player: {
            default: null,
            type: cc.Node
        }
    }
});

//如果你将属性的 type 声明为 Player 组件,当你拖动节点 "Player Node" 到 属性检查器,player 属性就会被设置为这个节点里面的 Player 组件
// Cannon.js
var Player = require("Player");
cc.Class({
    extends: cc.Component,
    properties: {
        // 声明 player 属性,这次直接是组件类型
        player: {
            default: null,
            type: Player
        }
    }
});

//查找子节点
//返回子节点数组
this.node.children
//返回对应的子节点
this.node.getChildByName(子节点名);
//查找后代节点
cc.find(子节点/.../后代节点, this.node);
//全局查找节点
cc.find(场景/节点/节点/...);

(4)访问已有变量里的值(通过模块访问)

//专门开设一个中介模块,导出接口;在其他模块进行节点、组件、属性的操作
// Global.js
module.exports = {
    backNode: null,
    backLabel: null,
};

// Back.js
var Global = require("Global");
cc.Class({
    extends: cc.Component,
    onLoad: function () {
        Global.backNode = this.node;
        Global.backLabel = this.getComponent(cc.Label);
    }
});

// AnyScript.js
var Global = require("Global");
cc.Class({
    extends: cc.Component,
    start: function () {
        var text = "Back";
        Global.backLabel.string = text;
    }
});
3、常用节点和组件接口

(1)节点状态和层级操作

//激活/关闭节点
this.node.active = true;
this.node.active = false;

//更改节点的父节点
this.node.parent = parentNode;

//索引节点的子节点
//返回子节点数组
this.node.children
//返回子节点数量
this.node.childrenCount

(2)更改节点的变换(位置、旋转、缩放、尺寸)

//更改节点位置
//分别对 x 轴和 y 轴坐标赋值
this.node.x = 100;
this.node.y = 50;
//使用setPosition方法
this.node.setPosition(100, 50);
this.node.setPosition(cc.v2(100, 50));
//设置position变量
this.node.position = cc.v2(100, 50);

//更改节点旋转
this.node.rotation = 90;
this.node.setRotation(90);

//更改节点缩放
this.node.scaleX = 2;
this.node.scaleY = 2;
this.node.setScale(2);
this.node.setScale(2, 2);

//更改节点尺寸
this.node.setContentSize(100, 100);
this.node.setContentSize(cc.size(100, 100));
this.node.width = 100;
this.node.height = 100;

//更改节点锚点位置
this.node.anchorX = 1;
this.node.anchorY = 0;
this.node.setAnchorPoint(1, 0);

(3)颜色和不透明度

//设置颜色
mySprite.node.color = cc.Color.RED;
//设置不透明度
mySprite.node.opacity = 128;

(4)常用组件接口
cc.Component 是所有组件的基类,任何组件都包括如下的常见接口:

接口作用
this.node该组件所属的节点实例
this.enabled是否每帧执行该组件的 update 方法,同时也用来控制渲染组件是否显示
update(dt)作为组件的成员方法,在组件的 enabled 属性为 true 时,其中的代码会每帧执行
onLoad()组件所在节点进行初始化时(节点添加到节点树时)执行
start()会在该组件第一次 update 之前执行,通常用于需要在所有组件的 onLoad 初始化完毕后执行的逻辑
4、生命周期
函数名描述
onLoad在节点首次激活时触发,或者所在节点被激活的情况下触发
start在组件首次激活前
update动画更新前
lateUpdate动画更新后
onEnable当组件的 enabled 属性从 false 变为 true 时,或者所在节点的 active 属性从 false 变为 true 时(倘若节点首次被创建且 enabled 为 true,则会在 onLoad 之后,start 之前被调用)
onDisable当组件的 enabled 属性从 true 变为 false 时,或者所在节点的 active 属性从 true 变为 false 时
onDestroy当组件或者所在节点调用了 destroy()时
5、创建和销毁节点

(1)创建新节点

cc.Class({
  extends: cc.Component,
  properties: {
    sprite: {
      default: null,
      type: cc.SpriteFrame,
    },
  },
  start: function () {
    //动态创建节点,并将它加入到场景中
    var node = new cc.Node('Sprite');
    var sp = node.addComponent(cc.Sprite);
    sp.spriteFrame = this.sprite;
    node.parent = this.node;
  }
});

(2)克隆已有节点

//
cc.Class({
  extends: cc.Component,
  properties: {
    target: {
      default: null,
      type: cc.Node,
    },
  },
  start: function () {
    //克隆场景中的已有节点
    var scene = cc.director.getScene();
    var node = cc.instantiate(this.target);
    node.parent = scene;
    node.setPosition(0, 0);
  }
});

(3)创建预制节点

//
cc.Class({
  extends: cc.Component,
  properties: {
    target: {
      default: null,
      type: cc.Prefab,    //预制
    },
  },
  start: function () {
    var scene = cc.director.getScene();
    var node = cc.instantiate(this.target);
    node.parent = scene;
    node.setPosition(0, 0);
  }
});

(4)销毁节点

//
cc.Class({
  extends: cc.Component,
  properties: {
    target: cc.Node,
  },
  start: function () {
    // 5 秒后销毁目标节点
    //销毁节点并不会立刻被移除,而是在当前帧逻辑更新结束后,统一执行
    setTimeout(function () {
      this.target.destroy();
    }.bind(this), 5000);
  },
  update: function (dt) {
    //判断当前节点是否已经被销毁
    if (cc.isValid(this.target)) {
      this.target.rotation += dt * 10.0;
    }
  }
});

PS:不要使用removeFromParent去销毁节点。
原因:调用一个节点的 removeFromParent 后,它不一定就能完全从内存中释放,因为有可能由于一些逻辑上的问题,导致程序中仍然引用到了这个对象。

6、加载和切换场景

(1)加载和切换

//从当前场景,切换到MyScene场景
cc.director.loadScene("MyScene");

(2)通过常驻节点,进行场景资源管理和参数传递

//常驻节点:不随场景切换,而自动销毁,为所有场景提供持久性信息
//设置常驻节点
cc.game.addPersistRootNode(myNode);
//取消常驻节点,还原为一般场景节点
cc.game.removePersistRootNode(myNode);

(3)场景加载回调

//fn:加载MyScene场景时触发
cc.director.loadScene("MyScene", fn);

(4)预加载场景

//后台预加载场景
cc.director.preloadScene("MyScene", fn);
//有需要时,手动加载该场景
cc.director.loadScene("MyScene", fn);
7、获取和加载资源

(1)资源属性的声明

// NewScript.js
cc.Class({
    extends: cc.Component,
    properties: {
        //所有继承自 cc.Asset 的类型都统称资源,如 cc.Texture2D, cc.SpriteFrame, cc.AnimationClip, cc.Prefab 等
        texture: {
            default: null,
            type: cc.Texture2D
        },
        spriteFrame: {
            default: null,
            type: cc.SpriteFrame
        }
    }
});

(2)静态加载(在属性检查器里设置资源)

// NewScript.js
onLoad: function () {
    //拖拽资源管理器的资源,到属性检查器的脚本组件中,即可在脚本里拿到设置好的资源
    var spriteFrame = this.spriteFrame;
    var texture = this.texture;
    spriteFrame.setTexture(texture);
}

(3)动态加载

//动态加载的资源,需要存放于assets的子目录resources中

//加载单个资源
//cc.loader.loadRes(resources的相对路径, 类型(可选), 回调函数)
//加载Prefab资源
cc.loader.loadRes("test assets/prefab", function (err, prefab) {
    var newNode = cc.instantiate(prefab);
    cc.director.getScene().addChild(newNode);
});
//加载SpriteFrame
var self = this;
cc.loader.loadRes("test assets/image", cc.SpriteFrame, function (err, spriteFrame) {
    self.node.getComponent(cc.Sprite).spriteFrame = spriteFrame;
});

//批量加载资源
//cc.loader.loadResDir(resources的相对路径, 类型(可选), 回调函数)
//加载test assets目录下所有资源
cc.loader.loadResDir("test assets", function (err, assets) {
    // ...
});
//加载test assets目录下所有SpriteFrame,并且获取它们的路径
cc.loader.loadResDir("test assets", cc.SpriteFrame, function (err, assets, urls) {
    // ...
});

//资源浅释放
//cc.loader.releaseRes(resources的相对路径, 类型(可选))
cc.loader.releaseRes("test assets/image", cc.SpriteFrame);
cc.loader.releaseRes("test assets/anim");
//cc.loader.releaseAsset(组件名)
cc.loader.releaseAsset(spriteFrame);

// 资源深释放,释放一个资源以及所有它依赖的资源
var deps = cc.loader.getDependsRecursively('prefabs/sample');

(4)加载远程资源和设备资源

//加载远程资源
//远程 url 带图片后缀名
var remoteUrl = "http://unknown.org/someres.png";
cc.loader.load(remoteUrl, function (err, texture) {
    //...
});
//远程 url 不带图片后缀名,此时必须指定远程图片文件的类型
remoteUrl = "http://unknown.org/emoji?id=124982374";
cc.loader.load({url: remoteUrl, type: 'png'}, function () {
    //...
});

//加载设备资源
//用绝对路径加载设备存储内的资源,比如相册
var absolutePath = "/dara/data/some/path/to/image.png"
cc.loader.load(absolutePath, function () {
    //...
});

加载限制:
1、原生平台远程加载不支持图片文件以外类型的资源。
2、这种加载方式只支持图片、声音、文本等原生资源类型,不支持SpriteFrame、SpriteAtlas、Tilemap等资源的直接加载和解析。(需要后续版本中的AssetBundle支持)
3、Web端的远程加载受到浏览器的CORS跨域策略限制,如果对方服务器禁止跨域访问,那么会加载失败,而且由于WebGL安全策略的限制,即便对方服务器允许http请求成功之后也无法渲染。

(5)资源的依赖和释放

// 直接释放某个贴图
cc.loader.release(texture);
// 释放一个 prefab 以及所有它依赖的资源
var deps = cc.loader.getDependsRecursively('prefabs/sample');
cc.loader.release(deps);
// 如果在这个 prefab 中有一些和场景其他部分共享的资源,你不希望它们被释放,可以将这个资源从依赖列表中删除
var deps = cc.loader.getDependsRecursively('prefabs/sample');
var index = deps.indexOf(texture2d._uuid);
if (index !== -1)
    deps.splice(index, 1);
cc.loader.release(deps);
8、监听和发射事件

(1)监听事件

//target是可选参数,用于绑定响应函数的调用者
//boolean是可选参数,默认为false,表示冒泡流
this.node.on(event, fn, target, boolean);

(2)关闭监听

this.node.off(event, fn, target, boolean);

(3)发射事件

//为事件函数,提供参数,最多5个
this.node.emit(event, arg1, arg2, arg3);

(4)派送事件

//grandson.js
//升级版的on,冒泡到的节点,全部注册事件
this.node.dispatchEvent( new cc.Event.EventCustom('foobar', true) );

//father.js
//在指定的上级节点中,注册相同的事件,阻止事件冒泡,手动停止派送
this.node.on('foobar', function (event) {
  event.stopPropagation();
});

(5)事件对象(回调参数的event对象)

API 名类型意义
typeString事件的类型(事件名)
targetcc.Node接收到事件的原始对象
currentTargetcc.Node接收到事件的当前对象,事件在冒泡阶段当前对象可能与原始对象不同
getTypeFunction获取事件的类型
stopPropagationFunction停止冒泡阶段,事件将不会继续向父节点传递,当前节点的剩余监听器仍然会接收到事件
stopPropagationImmediateFunction立即停止事件的传递,事件将不会传给父节点以及当前节点的剩余监听器
getCurrentTargetFunction获取当前接收到事件的目标节点
detailFunction自定义事件的信息(属于 cc.Event.EventCustom)
setUserDataFunction设置自定义事件的信息(属于 cc.Event.EventCustom)
getUserDataFunction获取自定义事件的信息(属于 cc.Event.EventCustom)
9、节点系统事件

(1)鼠标事件类型和事件对象

枚举对象定义对应的事件名事件触发的时机
cc.Node.EventType.MOUSE_DOWNmousedown当鼠标在目标节点区域按下时触发一次
cc.Node.EventType.MOUSE_ENTERmouseenter当鼠标移入目标节点区域时,不论是否按下
cc.Node.EventType.MOUSE_MOVEmousemove当鼠标在目标节点在目标节点区域中移动时,不论是否按下
cc.Node.EventType.MOUSE_LEAVEmouseleave当鼠标移出目标节点区域时,不论是否按下
cc.Node.EventType.MOUSE_UPmouseup当鼠标从按下状态松开时触发一次
cc.Node.EventType.MOUSE_WHEELmousewheel当鼠标滚轮滚动时
函数名返回值类型意义
getScrollYNumber获取滚轮滚动的 Y 轴距离,只有滚动时才有效
getLocationObject获取鼠标位置对象,对象包含 x 和 y 属性
getLocationXNumber获取鼠标的 X 轴位置
getLocationYNumber获取鼠标的 Y 轴位置
getPreviousLocationObject获取鼠标事件上次触发时的位置对象,对象包含 x 和 y 属性
getDeltaObject获取鼠标距离上一次事件移动的距离对象,对象包含 x 和 y 属性
getButtonNumbercc.Event.EventMouse.BUTTON_LEFT或cc.Event.EventMouse.BUTTON_RIGHT或cc.Event.EventMouse.BUTTON_MIDDLE

(2)触摸事件类型和事件对象

枚举对象定义对应的事件名事件触发的时机
cc.Node.EventType.TOUCH_STARTtouchstart当手指触点落在目标节点区域内时
cc.Node.EventType.TOUCH_MOVEtouchmove当手指在屏幕上目标节点区域内移动时
cc.Node.EventType.TOUCH_ENDtouchend当手指在目标节点区域内离开屏幕时
cc.Node.EventType.TOUCH_CANCELtouchcancel当手指在目标节点区域外离开屏幕时
API 名类型意义
touchcc.Touch与当前事件关联的触点对象
getIDNumber获取触点的 ID,用于多点触摸的逻辑判断
getLocationObject获取触点位置对象,对象包含 x 和 y 属性
getLocationXNumber获取触点的 X 轴位置
getLocationYNumber获取触点的 Y 轴位置
getPreviousLocationObject获取触点上一次触发事件时的位置对象,对象包含 x 和 y 属性
getStartLocationObject获取触点初始时的位置对象,对象包含 x 和 y 属性
getDeltaObject获取触点距离上一次事件移动的距离对象,对象包含 x 和 y 属性

(3)其它事件

枚举对象定义对应的事件名事件触发的时机
position-changed当位置属性修改时
rotation-changed当旋转属性修改时
scale-changed当缩放属性修改时
size-changed当宽高属性修改时
anchor-changed当锚点属性修改时

PS:枚举对象定义、事件名等价,在回调参数中,作用相同。

10、全局系统事件

//全局系统事件的类型
cc.SystemEvent.EventType.KEY_DOWN    //键盘按下
cc.SystemEvent.EventType.KEY_UP    //键盘释放
cc.SystemEvent.EventType.DEVICEMOTION    //设备重力传感

//绑定、解除全局系统事件
cc.systemEvent.on(event, fn, target, boolean);
cc.systemEvent.off(event, fn, target, boolean);
11、动作系统(变换系统)

(1)动作控制

// 执行动作
node.runAction(action);
// 停止一个动作
node.stopAction(action);
// 停止所有动作
node.stopAllActions();

// 给 action 设置 tag
var ACTION_TAG = 1;
action.setTag(ACTION_TAG);
// 通过 tag 获取 action
node.getActionByTag(ACTION_TAG);
// 通过 tag 停止一个动作
node.stopActionByTag(ACTION_TAG);

(2)容器动作

//顺序执行
cc.sequence(action1, action2, ...);
//并发执行
cc.spawn(action1, action2, ...);
//指定次数,重复执行
cc.repeat(action, times)
//无限次数,重复执行
cc.repeatForever(action)
//改变动作速度倍率,再执行
cc.speed(action, rate)

(3)即时动作

cc.show()    //立即显示
cc.hide()    //立即隐藏
...

(4)时间间隔动作

cc.moveTo()    //移动到目标位置
cc.rotateTo()    //旋转到目标角度
cc.scaleTo()    //将节点大小缩放到指定的倍数
...

(5)动作回调

var finished = cc.callFunc(fn, target, arg);

(6)缓动动作

var action = cc.scaleTo(0.5, 2, 2);
//使用easeIn曲线,丰富动作表现
action.easing(cc.easeIn(3.0));
...

PS:可以使用缓动系统,代替动作系统。(缓动系统的API更简约)

12、计时器

//interval:以秒为单位的时间间隔
//repeat:重复次数
//delay:开始延时
this.schedule(fn, interval, repeat, delay)
this.unschedule(fn)
13、脚本执行顺序

editor: {
        //executionOrder越小,该组件相对其它组件就会越先执行(默认为0)
        //executionOrder只对 onLoad, onEnable, start, update 和 lateUpdate 有效,对 onDisable 和 onDestroy 无效
        executionOrder: 1
    }
14、标准网络接口

(1)XMLHttpRequest——短连接
(2)WebSocket——长连接

15、对象池

对象池的概念
在同一场景中,需要多次进行节点的生成、消失时,假如直接进行创建、销毁的操作,就会很浪费性能。因此,使用对象池,存储需要消失的节点,释放需要生成的节点,达到节点回收利用的目的。

工作流程
(1)初始化对象池

properties: {
    enemyPrefab: cc.Prefab    //准备预制资源
},
onLoad: function () {
    this.enemyPool = new cc.NodePool();
    let initCount = 5;
    for (let i = 0; i < initCount; ++i) {
        let enemy = cc.instantiate(this.enemyPrefab); // 创建节点
        this.enemyPool.put(enemy); // 通过 put 接口放入对象池
    }
}

(2)从对象池请求对象

createEnemy: function (parentNode) {
    let enemy = null;
    if (this.enemyPool.size() > 0) { // 通过 size 接口判断对象池中是否有空闲的对象
        enemy = this.enemyPool.get();
    } else { // 如果没有空闲对象,也就是对象池中备用对象不够时,我们就用 cc.instantiate 重新创建
        enemy = cc.instantiate(this.enemyPrefab);
    }
    enemy.parent = parentNode; // 将生成的敌人加入节点树
    enemy.getComponent('Enemy').init(); //接下来就可以调用 enemy 身上的脚本进行初始化
}

(3)将对象返回对象池

onEnemyKilled: function (enemy) {
    // enemy 应该是一个 cc.Node
    this.enemyPool.put(enemy); // 和初始化时的方法一样,将节点放进对象池,这个方法会同时调用节点的 removeFromParent
}

清除对象池

//手动清空对象池,销毁其中缓存的所有节点
myPool.clear();

七、发布游戏

作者:简栋梁
链接:https://www.jianshu.com/p/9d433f967822
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

cocos creator游戏开发

1.初来乍到

打开 Cocos Creator 点击新建空白项目,在默认布局的左下区域,一个黄黄assets文件夹映入眼帘。作为前端的你对这个文件是不是再熟悉不过了。是的,和你想象的一样,开发游戏中所有资源,脚本都会放置到该文件。

2. 初步探索

项目建立好以后,对各区域的功能大致了解下,作为前端的你,主要还是要迅速的掌握cc提供的各种NB的功能。所以,还得赶紧打开 官网 快速浏览一遍。官网也写得很好,提供中文和英文,对于英文能力不好的伙伴来说,简直是不能太好了。是不是找到了当初学习Vue的感觉。作为前端的你,整天写了一堆业务控制,处理各种布局,各种兼容,对奇怪的css优先级搞得云里雾里的。所以是时候换一个更有意思开发场景,给自己做个游戏解闷多好

cc是一个跨平台框架,一端编译多端发布。想想前端的 mpvue taro uni-app,无不是解决此类问题,再加上gulp,webpack,再来一堆node_modules,啥less sass stylus.各种环境配置那是相当的复杂。所以业界流传,前端已经进入深水区,真的一点不假。 然而cc依然可以让你舒适的写JS或者TS ,并且没有繁杂的配置,一键搞定打包发布。

3. 小试牛刀

<p>上边说了一大堆,其实并没有什么鸟用。在官网首页中,给开发者提供了个完整坑爹的游戏《摘星星》,如果打包到微信小游戏,需要横屏,不太友好。本着举一反三的求学态度,我利用此场景,换了一个游戏玩法。开发了自己第一款小游戏《坦克侠》,当然也很坑爹</p>
<p>游戏开发主要是确定游戏规则,我新改编的玩法就是在星空中随机生成不同数量的星星,并一直往下掉落,我的主角坦克必须在星星掉落前接住。丢失一颗星星生命减一,生命为0游戏结束。当然我们主角每收集一颗星星,根据当前的难度会添加一定的分数。累计到一定的分数,又可以给主角添加一点生命值</p>
<p>在官网 下载初始项目 下载一个基础项目,该项目中只有一些项目基本图片和声音。接下来,我们需要建立场景,制作预制资源,添加控制脚本,编译发布微信小游戏,快速开始 </p>

制作一个游戏场景,与官网不同的是,我将Canvas的Size属性,在属性检查器中设置为 288 x 512 ,并且勾选了 Fit Height以及 Fit Width 用以适应同的手机屏幕。然后拖动背景图片到层级管理器中,并在场景编辑器中设置背景Size属性,使其等于Canvas的Size属性。然后依次在层级管理器中新建三个Label控件,依次拖动到背景图片左上角和右上角,用以记录生命值,当前分数,以及最高分数。接着在场景中间添加一个Label控件和一个Button按钮用于显示游戏结束和开始游戏。在场景底部拖动放置我们的主角坦克。所以最新场景的效果应该是如下显示的那样

场景预览
小程序码
微信群

4. 一顿操作猛如虎

游戏场景设计,看似酷炫,无非就是拖拖拖。依稀找到了当年C#开发winform的感觉,随便搞整一下,一个界面就出来了。所以导致很多人开发winform,webform很简单,很傻瓜,其实不是的。重要的还是后边的业务逻辑,解决方案,这些都是超越语言之上的东西。所以cc的场景编辑,就不多说了,直接分析我们游戏实现逻辑。开始之前我们先初始一下typescript开发环境,操作如下图

typescript


依次点击安装vs code 扩展插件,添加 Typescript项目配置。接下来就要编写脚本了,所有还是有必要了解下cc脚本的生命周期

  1. onLoad 首次激活时触发,一般做一些初始化操作。对比Vue我觉得最合适的应该是beforeMount回调
  2. start 首次激活时触发,一般初始化一些中间状态的数据,改方法在onLoad之后,在第一次update之前,对比Vue自然应该是mounted回调
  3. update 该回调会频繁调用,每一帧调用一次。对比Vue应该是beforeUpdate回调,虽然他们性质不一样
  4. lateUpdate 该回调会频繁调用,也是每帧调用一次,对比Vue应该updated回调
  5. onDestroy 根据条件调用,当组件调用了自身的 destroy()方法,会触发此回调
  6. onEnable 根据条件调用, enabled 属性从 false 变为 true 或 active 属性从 false 变为 true 触发此回调
  7. onDisable 根据条件调用, enabled 属性从 true 变为 false 或active 属性从 true 变为 false触发此回调

4.1 让主角动起来

做过前端的你一定知道,要想拖动一个DIV,一定是在Body中监听鼠标的移动事件。在移动端一定是监听触摸移动事件。是的,在cc里边做游戏,希望一个组件动起来依然是这么操作的,那么cc里边是如何注册事件的呢?两个方式,一个在场景编辑器下角的属性中添加脚本里边的方法,另外一种就是直接在脚本里边添加。当然我推荐第二种。虽然IDE会帮我们生成很多代码,如果不自己写一遍,就永远不晓得数据流向。就像当年开发winform时,很多人拖动一个按钮控件,然后双击控件,IDE就自动帮你注册好了一个用户点击事件。殊不知,IDE是在xx.design.cs中通过代码替你注册好的。所以既然刚开始学,一定要了解清楚它的原理。

  • cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this) 注册一个系统事件 ,支持按键事件和重力感应事件
  • this.node.parent.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this) 在某个节点注册一个Node 支持的事件类型

<p>所以,主角移动无非实在TouchMove时改自己的X/Y</p>

 // author:herbert qq:464884492 
 onTouchMove(e: cc.Event.EventTouch) {
        let deltaX = e.getDeltaX(); //获取本次和上次移动的增量
        let deltaY = e.getDeltaY();
        //左移
        if (deltaX < 0 && this.node.x <= this.leftMinX) return;
        if (deltaX > 0 && this.node.x >= this.rightMaxX) return;
        if (deltaY > 0 && this.node.y >= this.topMaxY) return;
        if (deltaY < 0 && this.node.y <= this.bottomMinY) return;
        this.node.x += deltaX;
        this.node.y += deltaY;
    }

4.2 生成坑爹的星星

在cc里边需要重复生成的对象,我们一般会制作成一个预制资源。然后在基本中通过代码实例化。何为预制资源,就权当它是一个模板吧。现在生成我们第一颗小星星

// author:herbert qq:464884492
buildOneStar() {
        let star = cc.instantiate(this.starPrefab);
        this.node.addChild(star);
         return star;
    }

是的,就是这么简单,有没有class.newInstance()的感觉,当然这个只是在场景的默认位置生成了一个星星而已。我们需要更多的信息,位置肯定也不是默认位置,所以还得继续码代码

 // author:herbert qq:464884492
   buildRandomStar() {
        let tempX = 0;
        let tempY = 0;
        tempX = Math.floor(this.starMaxX - Math.random() * this.starMaxX);//生成一个不大于MaxX的坐标
        tempY = Math.floor(this.starMaxY - Math.random() * this.starMaxY);
        if (Math.random() < 0.5) tempX = tempX * -1;
        let star = this.buildOneStar();
        star.setPosition(tempX, tempY);
        star.zIndex = this.tank.zIndex - 1;
        star.name = "star";
        star.getComponent("Star").index = this;
    }

这样感觉好多了,可以生成很多星星了,不过,我们的星星也得往下掉才行,作为前端的你首先想到的是不是更新星星的Y值,是的,我就是这么做的。利用生命周期中start方法,定义一个从上往最小Y运动的动画。后来才了解到所有的游戏引擎都有物理特性,开启了自己就掉下来了。不过原理肯定还是改变y值。何况这么简单的游戏完全没必要使用

    start() {
        // 定义一个Action
        let downAction = cc.moveTo(this.index.starFallSpeed, this.node.x, this.minY - 60);
        downAction.easing(cc.easeSineOut());
        this.node.runAction(downAction);
    }

4.3 是时候接住星星了

只要是游戏少不了做碰撞检测,如果在CC中开启了物理引擎还好,直接跟星星和主角添加一个刚体就好了,不过我们没开启,那就自己来了。不过碰撞检测无非就是判断两个区域有没有重叠地方,简单判断就上下左右,左上右上左下右下八个点。不过我这里偷了个懒,直接判断星星和主角间向量的距离。

    //author:herbert qq:464884492
    ...
    let distance = this.node.position.sub(this.tank.getPosition()).mag();
    if (distance < (this.tank.width / 2 - 5)) {
      console.log("接住了");
    }
    ...

4.4 来点刺激的

游戏嘛,总不能一成不变那多没意思,所以随着时间的推移我们的调整点难度。我这个游戏难度无非就一下两个方面

  1. 生成星星的速度加快
  2. 星星掉落的速度加快
//author:herbert qq:464884492
...
    this.index.scoreNum += this.index.starScoreSpeed;
            this.index.score.string = "得分:" + this.index.scoreNum;
            // 降落速度加
            if (Math.floor(this.index.scoreNum / 100) == this.index.starScoreSpeed - 4 && this.index.starFallSpeed > 1) {
                this.index.starFallSpeed -= 0.2; //下降速度加快
                if (this.index.starBuildTimeOut > 200) {
                    this.index.starBuildTimeOut -= 100; //生成速度加快
                }
                this.index.lifeNum += 1;
                if (this.index.starScoreSpeed < 10) {
                    this.index.starScoreSpeed += 1;
                }
            }
            cc.audioEngine.play(this.index.scoreClip, false, 0.2);
            this.index.allStars.splice(this.index.allStars.indexOf(this.node), 1)
            this.node.destroy();
...

4.5 是时候结束了

游戏嘛,也不能一直玩下去。不然多没挑战。自从调整游戏难度后我的最高分重来就没有超过4000.

//author:herbert qq:464884492
...
 if (this.node.y <= this.minY) {
            this.index.lifeNum -= 1;
            this.index.life.string = "生命:" + this.index.lifeNum;
            this.node.destroy();
            this.index.allStars.splice(this.index.allStars.indexOf(this.node), 1)
            if (this.index.lifeNum <= 0) {
                this.index.gameOver.node.active = true;
                this.index.btnPlay.node.active = true;
                this.index.starIsRunning = false;
                let storageValue = cc.sys.localStorage.getItem(this.index.HIGHSTORAGEKEY);
                if (storageValue && parseInt(storageValue) > this.index.scoreNum) {
                    return;
                }
                cc.sys.localStorage.setItem(this.index.HIGHSTORAGEKEY, this.index.scoreNum);
                this.index.highScore.string = "最高分:" + this.index.scoreNum;
            }
        }
...

5. 来点实际的

做技术嘛,大多都是 Talk is cheap,Show me your code.做点总结吧

  • 开源地址
  • 在基本中定义的属性,切记在编辑器中拖动绑定
  • 多看官网api,多开实例代码
  • 发布微信小游戏一定不要有英文,会导致审核不通过

使用Cocos Creator开发微信小游戏(四)小游戏实战

小游戏介绍


一个左右跳一跳小游戏,点屏幕左边向左跳,点右边向右跳,落水为失败。

PC chrome浏览器下游戏截图:

微信开发者工具截图:

 小游戏场景图结构


 场景结构同上一篇中的挤柠檬汁小游戏结构大体相同

DataManager:记录游戏中的配置数据,游戏逻辑数据(分数 )

SoundManager:管理游戏中的音效

ItemsManager:处理游戏过程中随机出现的道具(金币,玉米等)

Net:处理Http请求

UIManager:管理游戏中的所有UI

代码文件


主要文件代码

玩家控制(驴) Player.js

//驴

cc.Class({

    extends: cc.Component,

    properties: {

        // foo: {

        //    // ATTRIBUTES:

        //    default: null,        // The default value will be used only when the component attaching

        //                          // to a node for the first time

        //    type: cc.SpriteFrame, // optional, default is typeof default

        //    serializable: true,  // optional, default is true

        // },

        // bar: {

        //    get () {

        //        return this._bar;

        //    },

        //    set (value) {

        //        this._bar = value;

        //    }

        // },

        _UIGameNode: cc.Node,

    },

    // LIFE-CYCLE CALLBACKS:

    // onLoad () {},

    start ()

    {

        this._UIGameNode = cc.find(‘Canvas/UIManager/UIGame’);

    },

    //跳跃,相对位置

    jump(duration, destPos)

    {

        var rotAct = cc.jumpBy(duration, destPos, 80, 1);

        var callFunc = cc.callFunc(this.onJumpEnd, this);

        var seq = cc.sequence(rotAct, callFunc);

        if(destPos.x > 0)

        {

            this.node.setScaleX(1);     

        }

        else

        {

            this.node.setScaleX(-1); 

        }

        this.node.runAction(seq);

    },

    //跳跃到目标点, 绝对位置

    jumpTo(duration, destPos)

    {

        var rotAct = cc.jumpTo(duration, destPos, 80, 1);

        var callFunc = cc.callFunc(this.onJumpEnd, this);

        var seq = cc.sequence(rotAct, callFunc);

        if(destPos.x > 0)

        {

            this.node.setScaleX(1);     

        }

        else

        {

            this.node.setScaleX(-1); 

        }

        this.node.runAction(seq); 

    },

    //跳跃结束

    onJumpEnd()

    {

        this._UIGameNode.getComponent(‘UIGame’).onPlayerJumpEnd(); 

    },

    // update (dt) {},

});

ItemManager.js

//游戏中物品池,管理复用的物品

//物品类型

var ItemType =

{

    //没有东西

    IT_None: -1,

    //草

    IT_Grass: 0,

    //玉米

    IT_Corn: 1,

    //萝卜

    IT_Radish: 2,

    //金币

    IT_Coin:3,

};

var ItemManager = cc.Class({

    extends: cc.Component,

    properties: {

        // foo: {

        //    // ATTRIBUTES:

        //    default: null,        // The default value will be used only when the component attaching

        //                          // to a node for the first time

        //    type: cc.SpriteFrame, // optional, default is typeof default

        //    serializable: true,  // optional, default is true

        // },

        // bar: {

        //    get () {

        //        return this._bar;

        //    },

        //    set (value) {

        //        this._bar = value;

        //    }

        // },

        //物品Prefab列表

        ItemPrefabList:

        {

            default: [], 

            type: [cc.Prefab],

        },

        //概率列表

        ItemRateList:

        {

            default:[],

            type: [cc.Integer],                   

        },

        //随机的基数

        _RandBaseNum : 100,

        _RandRateList: [],

        //物品池

        _ItemPoolList: [],

    },

    // LIFE-CYCLE CALLBACKS:

    onLoad ()

    {

        this._RandBaseNum = 0;

        //概率统计:

        for(var i = 0; i < this.ItemRateList.length; ++i)

        {

            this._RandBaseNum += this.ItemRateList[i];

            if(i == 0)

            {

                this._RandRateList[i] = this.ItemRateList[i];

            }

            else

            {

                this._RandRateList[i] =  this._RandRateList[i – 1] + this.ItemRateList[i];

            }

        }

        //物品池,各个物品先预创建3个

        for(let i = 0; i < 4; ++i)

        {

            this._ItemPoolList[i] = new cc.NodePool();

            for(var j = 0; j < 3; ++j)

            {

                var curItem = cc.instantiate(this.ItemPrefabList[i]);

                this._ItemPoolList[i].put(curItem);

                //设置为物品 

                curItem.group = “item”;

                curItem.setTag(i);

            }

        }

    },

    start ()

    {

    },

    //获取当前Block挂载的物品

    getRandomItemType()

    {

        //[0, 1)

        var randNum = parseInt(cc.random0To1() * this._RandBaseNum);

        for(var i = 0; i < this._RandRateList.length; ++i)

        {

            if(randNum < this._RandRateList[i] )

            {

                break;

            }

        }

        //cc.log(“getRandomItemType “,  randNum, );

        return i – 1;

    },

    //获取某类型的Item

    getItemByType( itemType )

    {

        if(itemType == ItemType.IT_None)

        {

            return null;

        } 

        if(itemType > 3 || itemType < 0)

        {

            return null;

        }

        var curItem = this._ItemPoolList[itemType].get();

        if(curItem == null)

        {

            curItem =  cc.instantiate(this.ItemPrefabList[itemType]);

            this._ItemPoolList[itemType].put(curItem);

            //设置为物品 

            curItem.group = “item”;

            curItem.setTag(itemType);

            curItem = this._ItemPoolList[itemType].get();

            cc.log(“new item “, itemType);

        }

        curItem.scale = 0.7;

        return curItem;

    },

    //将Item重新返回到Pool

    putItemToPool(curItem)

    {

        if(curItem.group != ‘item’)

        {

            //cc.log(“putItemToPool invalid  group”);

            return;

        }

        curItem.parent = null;

        var itemType = curItem.getTag();

        if(itemType > 3 || itemType < 0)

        {

            //cc.log(“putItemToPool invalid  itemType”);

            return;

        }

        this._ItemPoolList[itemType].put(curItem);

    },

    // update (dt) {},

});

module.exports =

{

    ItemType: ItemType,

    ItemManager: ItemManager,

}

UIGame.js部分代码

    //游戏状态的处理

    setGameState(state)

    {

        //处理暂停逻辑

        if(this._CurGameState == GameState.GS_Pause && state !=  this._CurGameState)

        {

            cc.director.resume();   

        }

        //新状态的处理

        this._CurGameState = state; 

        //准备状态

        if(this._CurGameState == GameState.GS_Ready)

        {

            this.StartBtn.node.active = true;

            this.PauseBtn.node.active = false;

        }

        //暂停

        else if(this._CurGameState == GameState.GS_Pause)

        {

            cc.director.pause();

            //按钮显示与隐藏

            this.StartBtn.node.active = true;

            this.PauseBtn.node.active = false;

        }

        //等待游戏中的操作

        else if(this._CurGameState == GameState.GS_WaitOP)

        {

            this.StartBtn.node.active = false;

            this.PauseBtn.node.active = true;

            //对当前Block进行下移操作

            if(this._CurBlockIndex < 0 || this._CurBlockIndex >= this._BlockListUse.length)

            {

                cc.log(“GS_WaitOP invalid _CurBlockIndex “, this._CurBlockIndex);

                return;

            }

            var curBlock = this._BlockListUse[this._CurBlockIndex];

            if(curBlock == null)

            {

                cc.log(“GS_WaitOP invalid curBlock null”, this._CurBlockIndex);

                return;   

            }

            //block下移

            var downAct = curBlock.getActionByTag(0);

            if(downAct == null)

            {

                var downActScale = cc.scaleTo(1.5, 1, 0);

                var callFunc = cc.callFunc(this.onBlockDownFinish, this, curBlock);

                downAct = cc.sequence(downActScale, callFunc);

                curBlock.runAction(downAct);

            }

        }

        //游戏结束

        else if(this._CurGameState == GameState.GS_Over)

        {

            //按钮显示与隐藏

            this.StartBtn.node.active = false;

            this.PauseBtn.node.active = false;

            var UIManager = this.node.parent.getComponent(‘UIManager’);

            UIManager.openUI(UIType.UIType_GameOver);

            //向子域发送,上传数据

            var DataManager = this.DataManager.getComponent(‘DataManager’);

            if(window.wx != undefined)

            {

                window.wx.postMessage(

                    {

                        msgType: 1,

                        bestScore: DataManager.getCurScore(),

                    }

                );

            }

            //播放背景音乐

            if(this.SoundManager)

            {

                var soundMgr = this.SoundManager.getComponent(‘SoundManager’);

                soundMgr.stopSound(SoundType.SoundType_Bg);

                soundMgr.playSound(SoundType.SoundType_Fall);

            }

        }

    },

动态场景生成:

    //————————Block操作 begin————————–

    //获取Block

    getBlock()

    {

        if(this._BlockList.length > 0)

        {

            var block = this._BlockList.pop();

            this._BlockListUse.push(block);

          return block; 

        }

        else

        {

            var block = cc.instantiate(this.BlockPrefab);

            this.pushBlock(block);

            return this.getBlock(); 

        }

    },

    //添加Block

    pushBlock(block)

    {

      // this._BlockPool.put(block);

        this._BlockList.push(block);

    },

    //移除Block(移除一个最下面的,屏幕外的Block) 还原到池里

    delelteUseBlock()

    {

      var firstBlock = this._BlockListUse.shift();

      firstBlock.parent = null;

      firstBlock.scaleY = 1;

      //将Block下物品还原到物品池

      this.restoreItemToPool(firstBlock);

      this._BlockList.push(firstBlock);

      this._CurBlockIndex -= 1;       

    },

作者:游戏中的乐趣
链接:https://www.jianshu.com/p/6399f15042fe
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。