creator打包微信小游戏笔记_creator微信小游戏出包脚本-程序员宅基地

异步执行

js是单线程的,分为同步任务和异步任务,同一个时刻只能去处理一个任务
有一个任务执行栈,同步任务都放到同步栈里面,异步任务执行有结果了,会放到异步栈
任务执行栈会从同步栈里取任务执行,当所有的同步栈任务执行结束,会从异步栈里取任务,
异步栈里也可以分的再细一点就是宏观异步栈微观异步栈在同一时刻,微观异步栈要先于宏观异步栈执行
宏观异步栈指的是:setTimeout(func,time);
微观异步栈指的是:(new Promise()).then(func)
浏览器刷新触发函数:window.requestAnimationFrame
上面三个触发器有一个最明显的差别,就是浏览器刷新函数最准确,其它两个异步触发器就算时间到了,也不一定触发,他有一个先后触发顺序,这一点细微的差别要格外注意

运行环境简要

微信小游戏运行在多种平台上:iOS(iPhone/iPad)微信客户端、Android 微信客户端、PC 微信客户端、Mac 微信客户端和用于调试的微信开发者工具。

各平台脚本执行环境是各不相同的:
在 iOS 上,小程序逻辑层的 javascript 代码运行在 JavaScriptCore 中;
在 Android 上,小程序逻辑层的 javascript 代码运行在 V8 中;
在 开发工具上,小程序逻辑层的 javascript 代码是运行在 NW.js
微信内部植入了一个叫runtime的浏览器内核,它和html5浏览器内核有一些区别,没有放入html解析引擎和css解析引擎,但最牛逼的js引擎(JavaScriptCore or v8)还是搞不来还是植入了,自己还做了一些业务封装,框架如下:
普通html5框架
在这里插入图片描述

微信runtime框架
在这里插入图片描述

代码分包

js分包比较简单,就是一个解析js代码,找出之前合并的所有文件(文件名+文件内容),然后以s[‘文件名’]=文件内容;这个为一个最小单位往子包中填充,满4兆则为一个子包,重新再生成一个子包文件,再继续填充,依次类推,关键代码如下,只包含了index.js,module.js,nameMap.js这三个js文件就可以轻松分解js代码,就是这么简单
其中index.js文件内容的末尾那个函数是拆包的入口函数
index.js

const fs = require('fs');
const path = require('path');

const fileUtils = require('../utils/file');

const acorn = require('acorn');
const escodegen = require('escodegen');
const estraverse = require('estraverse');

const {
    scanScripts} = require('./nameMap');

const scriptsToken = '__scripts';

const CODE_NAME = "index.js"

const codegenOpt = {
    
    format: {
    
        compact: true
    }
}

let settingsName;

function getChild(node, child, type) {
    
    let c = node[child]
    if (!c || c.type != type) {
    
        throw new Error('parsed error')
    }

    return c;
}

function isConsoleExpression(node) {
    
    if (node.type == "LogicalExpression"
    && node.right.type == 'CallExpression'
    && node.right.callee.type == 'MemberExpression'
    && node.right.callee.object.type == 'Identifier'
    && node.right.callee.object.name == 'console') {
    
        if (node.right.callee.property.type == 'Identifier' && node.right.callee.property.name == 'log') {
    
            return false;
        }
        return true;
    }

    if (node.type == 'CallExpression'
    && node.callee.type == 'MemberExpression'
    && node.callee.object.type == 'Identifier'
    && node.callee.object.name == 'console') {
    
        if (node.callee.property.type == 'Identifier' && node.callee.property.name == 'log') {
    
            return false;
        }
        return true;
    }

    return false;

}

function removeConsole(ast) {
    
    estraverse.replace(ast, {
    
        enter: function (node) {
    
            if (node.type == 'ExpressionStatement' && isConsoleExpression(node.expression)) {
    
                return this.remove();
            }

            if (isConsoleExpression(node)) {
    
                return this.remove();
            }
        }
    })

    estraverse.traverse(ast, {
    
        enter: function (node, parent) {
    
            if (node.type == 'IfStatement' && !node.consequent) {
    
                node.consequent = {
    
                    type: 'BlockStatement',
                    body: []
                }
            }

            if (node.type == 'ConditionalExpression') {
    
                if (!node.consequent) {
    
                    node.consequent = {
    
                        type: 'BlockStatement',
                        body: []
                    }
                } else if (!node.alternate) {
    
                    node.alternate = {
    
                        type: 'BlockStatement',
                        body: []
                    }
                }

            }

            if (node.type == 'UnaryExpression' && node.operator == 'void' && !node.argument) {
    
                node.argument = {
    
                    type: 'Literal',
                    value: 0
                }
            }
            if (node.type == 'CallExpression'
            && node.callee.type == 'MemberExpression'
            && node.callee.object.type == 'Identifier'
            && node.callee.object.name == 'console') {
    
                // console.log(node.type, parent.type)
            }
        }
    })
}


function slice(ast) {
    
    let body = ast.body[0];

    let expression = getChild(body, 'expression', 'AssignmentExpression');
    let callExp = getChild(expression, 'right', 'CallExpression')
    let args = callExp.arguments;

    let modules = args[0]['properties'];
    args[0] = getScriptsToken();

    //扫描脚本
    scanScripts(modules);

    let scripts = [];
    let script = '';
    let size = 0;
    let maxSize = 4 * 1000 * 1000; //4M;
    for (let i = 0; i < modules.length; i++) {
    
        let module = modules[i];
        //获取文件名
        let keyStr = escodegen.generate(module.key, codegenOpt);
        //获取文件内容
        let valueStr = escodegen.generate(module.value, codegenOpt);
        //此处会多7个字节 因为s['']=;这个结构正好是7个字节
        //我们每个文件都会以这样的形式存放 s['文件名']=文件内容;
        let moduleSize= keyStr.length + valueStr.length + 7;//s['xxx']=yyy;
        if (size + moduleSize > maxSize) {
    
            //满4兆了 OK 保存起来
            scripts.push(script);
            script = '';
            size = 0;
        }
        script += `s['${keyStr}']=${
    valueStr};`
        size += moduleSize;
    }
    
    //每一个子包内容都保存在这个scripts数组中
    scripts.push(script);

    scripts = scripts.map((s) => {
    
        return `(function(s){
    ${
    s}})(window.__scripts||(window.__scripts={
    }))`
    });
    scripts.push(escodegen.generate(ast, codegenOpt))

    return scripts
}

function getScriptsToken() {
    
    return {
    
        type: 'MemberExpression',
        object: {
    
            type: 'Identifier',
            name: 'window'
        },
        property: {
    
            type: 'Identifier',
            name: scriptsToken
        }
    }
}

/**
 * js源码路径
 * @param {*} src 
 */
module.exports =  function(src) {
    
    let code = fs.readFileSync(src, 'utf8');
    let ast = acorn.parse(code);
    removeConsole(ast);
    return slice(ast);
}

nameMap.js

let map = {
    };

function scanScripts(properties) {
    
    for (let i = 0; i < properties.length; i++) {
    
        let property = properties[i];
        let key = property.key;
        let name
        if (key.type == "Literal") {
    
            name = key.value
            property.key = {
    
                type: 'Identifier',
                name: name
            }
        } else if (key.type == "Identifier") {
    
            name = key.name;
        }

        if (!name || map[name]) {
    
            throw new Error('script name is duplicated:' + key);
        }

        map[name] = next();
    }
}

function getShortName(key) {
    
    return map[key];
}

let letters = 'abcdefghijklmnopqrstuvwxyz'
let numbers = '0123456789'
let lettersLen = letters.length;
let numbersLen = numbers.length;

let nameIndex = 0;
function next() {
    
    let index = nameIndex;
    let name = letters.charAt(index % lettersLen);
    index = Math.floor(index / lettersLen);
    while (index > 0) {
    
        name += getCharAt(index % (lettersLen + numbersLen));
        index = Math.floor(index / (lettersLen + numbersLen))
    }

    nameIndex++;
    return name;
}

function getCharAt(i) {
    
    if (i < lettersLen) {
    
        return letters.charAt(i)
    } else {
    
        return numbers.charAt(i - lettersLen)
    }
}

module.exports = {
    
    getShortName: getShortName,
    scanScripts: scanScripts
}

module.js

const {
    getShortName} = require('./nameMap')

function Module(ast) {
    
    this.name = ast.key;
    let value=  ast.value;
    this.function = value[0];
    this.map = value[1];
}

let proto = Module.prototype;

proto.convertName = function() {
    
    this.shortName = getShortName(this.name)
    this.convertedFunc = {
    
        type: this.function.type
    }
}

proto.toString = function() {
    

}

module.exports = Module

防破解

对于微信小游戏而言想要获取它的代码和资源太简单了,现成的脚本工具,几乎用不了几分钟就可以拿到,可是要想连到小游戏的服务端这个问题就复杂了,微信后台做了保护,大致如下:
在这里插入图片描述

//发起登录
public login() {
    
        let wx: any = window['wx'];
        let fail = function () {
    
            UIPopupHelper.showOfflineDialog(Lang.get("login_network_timeout"), null, this.login.bind(this));
            G_WaitingMask.showWaiting(false);
        }.bind(this);
        let this1 = this;
        G_WaitingMask.showWaiting(true);
        wx.login({
    
            success(res) {
    
            //微信登录成功后 会返回一个临时code
                this1._loginServer(res.code, (ret, data) => {
    
                    G_WaitingMask.showWaiting(false);
                    this1._onGetToken(ret, data);
                }, fail);
            },
            fail: fail
        })
    }
     //拿到这个微信返回的code 去访问我们自己的服务器
     //我们的服务器会拿着这个code和appid还有appsecret这三个值去再一次访问微信来判断有效性
     //如果有效 则合法 允许登录
     //否则 登陆不合法
    private _loginServer(code: string, success?: Function, fail?: Function) {
    
        // console.log("NativeAgentWeChat loginServer", code);
        let url = config.LOGIN_URL_TEMPLATE;
        url = url.replace("#domain#", config.LOGIN_URL);

        let requestData = {
    
            appID: this.getGameId(),
            channelID: this.getChannelId(),
            extension: "",
            sdkVersionCode: "1.0",
            deviceID: "",
            userType: "",
            sign: ""
        }
        requestData.extension = JSON.stringify({
     code: code });
        let sign = "appID=" + requestData.appID + "channelID=" + requestData.channelID + "extension=" + requestData.extension + this._appkey;
        // console.log("sign:", sign);
        requestData.sign = window['md5'](sign);


        let srcs = JSON.stringify(requestData);
        // console.log("requestData", srcs);
        let aesRequestData = CryptoJS.AES.encrypt(srcs, this._aesKey, {
     mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
        // console.log(aesRequestData.toString());

        url = url.replace("#data#", encodeURIComponent(aesRequestData));
        // console.log(url);

        let http = new HttpRequest();
        http.get(url, (response) => {
    
            // console.log(response);
            let ret = JSON.parse(response);
            if (ret.state != 1 || ret.data == null) {
    
                fail && fail();
                return;
            }
            let decrypt = CryptoJS.AES.decrypt(ret.data, this._aesKey, {
     mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
            let data = decrypt.toString(CryptoJS.enc.Utf8);
            console.log("decrypt", data);
            success && success(ret.state, data);
        }, fail);
    }

    private _onGetToken(ret, response?: string) {
    
        if (ret != 1) {
    
            this._dispatch({
     event: NativeConst.SDKLoginResult, ret: NativeConst.STATUS_FAILED, param: "" });
            return;
        }
        let responseData = JSON.parse(response);
        console.log("_onGetToken:", ret, responseData);
        this._openid = responseData.extension.openid;
        this._sessionKey = responseData.extension.session_key;
        // if (!ALDStatistics.instance.isFirstLoginGame() && !ALDStatistics.instance.hasMarkAB) {
    
        //     this.abCode = this._openid.charCodeAt(this._openid.length - 1);
        // } else {
    
        //     var code = window['md5'](this._openid);
        //     this.abCode = code.charCodeAt(code.length - 1);
        // }

        // wx.aldUserAB(this.getversionAB());
        // console.log('AB_code: ', this.getversionAB());
        this._topUserID = responseData.topUserID.toString();
        let topUserName = responseData.topUserName.toString();
        this._topUserName = topUserName;

        let data: any = {
    };
        data.topUserID = topUserName;
        data.topUserName = topUserName;
        data.platformID = responseData.platformID;
        data.sdkUserName = "";
        data.sdkUserID = this._openid;
        data.channelID = this.getPlatformId(); // responseData.channelID;
        data.token = "这是一个自定义token";
        data.timestamp = (new Date().getTime() * 1000).toString();
        data.extension = "gptxxxxxxx|1|1";
        let sign = topUserName + topUserName + data.sdkUserID + data.token + config.TOKEN_KEY;
        data.sign = window['md5'](sign);
        console.log("_onGetToken:", data);
        this._data = data;
        if (G_StorageManager.load('server')) {
    
            this._dispatch({
     event: NativeConst.SDKLoginResult, ret: NativeConst.STATUS_SUCCESS, param: data });
        }else {
    
            G_RoleListManager.checkUpdateList();
        }
    }

由代码和图可以知道,如果想要破解微信小游戏和服务器连接,你得知道人家的appid,微信验证登录的时候是结合code+appid+appsecret这三个值来判断的,我们的小游戏在打包成功以后,她如果调用wx.login,那么除了code是临时的,appid和appsecret都是确定的,所以我们还要把我们的小游戏发布到原版的微信后台,什么这不是自投罗网吗?

resources目录下的资源与library目录下的关系

resources目录下的每个资源都会带有一个meta文件,这个meta文件会存放一个唯一的uuid来标记该文件
资源之图片
在这里插入图片描述

resources目录下的.meta文件内容如下,红框中为图片的uuid
在这里插入图片描述
在library库中找到对应的存放位置:
在这里插入图片描述
在library库下还有个配置文件uuid-to-mtime.json,这个是用来建立resources目录下的资源和library/imports目录下资源的桥梁,那么当前这个图片在配置文件的记录如下:
在这里插入图片描述
打包后这个资源的位置如下:
由于构建发布后,资源会进行自动合并到图集,所以这个资源的存储位置可能已经镶嵌到一张大图中了,但是路径是不会变的,我们可以拿着下面的路径去找

"relativePath": "resources\\icon\\achievement\\bg_signpics.png"

构建发布后,主包的资源会放在这个文件夹下
在这里插入图片描述
打开这张图里的config,输入路径,会看到他被放到了一个叫11953下标的位置存放,继续搜索这个下标
在这里插入图片描述
会看到11953这个下标又被一个07d5dfd4a这个下标的位置存放,继续搜索
在这里插入图片描述
会看到这个07d5dfd4a被存到下面这个数组里,ok这个算是到顶层了,打开import这个文件夹,搜索07d5dfd4a
在这里插入图片描述
07d5dfd4a.md5.json
在这里插入图片描述
打开这个07d5dfd4a.md5.json
在这里插入图片描述
注意到texture也存了一个MD5值,拿这个值在native文件夹下搜索就可以找到打包后的资源了啊
在这里插入图片描述
资源之预制体:未完待续
在这里插入图片描述
resources文件下对应文件的.meta文件的记录
在这里插入图片描述
library文件夹的uuid-to-mtime.json中的记录
在这里插入图片描述
library文件夹的imports文件下
在这里插入图片描述
构建成微信小游戏包后的资源存放目录:查看一下这个config.MD5.json文件
输入资源的名字

prefab/achievement/DailyActivityHint

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在import文件夹下输入:04bfb49bb
在这里插入图片描述

资源之声音:未完待续
总结:对于原生资源,比如一张图片或者一个声音文件,一般情况即没有被引用的情况下,会在import文件夹生成一个json,我们通过json可以获取到每个资源的native的路径MD5值,拿这个值在native文件下找到对应的文件,我们在外围访问这个资源的时候是通过和import文件夹同目录的config文件里找到访问路径的
资源的相对路径===========》输入到config.md5.json配置文件中========》获取到import文件下的json文件MD5名========》打开import文件夹下对应的json文件,即可以找到对应native文件夹下资源的MD5名=====》在native文件夹输入对应的MD5名即可以获取到资源
再次化简:
资源的相对路径==》config==》import==》native==>资源

其实这个过程就是config负责建立外围和import的联系,而import是为了建立和native原生资源的联系
发现:图片会被自动的合并起来,如果预制体里使用了图片,图片的json信息也会和预制体的json信息合并成一个json存放在import文件夹下

AB包

ab包通过一个文件夹生成,这个文件夹里包含了所有的图片资源,声音,脚本文件等,那么最后生成一个AB包的时候,最终的产物一个import文件夹,一个naitive文件夹,一个config.md5.json
如果包含脚本的话,会单独生成一个index.js文件,将所有脚本文件合并
注意:
1:Creator 有 4 个 内置AB包,包括 resources、internal、main、start-scene,在设置 Bundle 名称 时请不要使用这四个名称
2:小游戏分包只能放在本地,不能配置为远程包,所以当 压缩类型 设置为 小游戏分包 时,配置为远程包 项不可勾选
3:Zip 压缩类型主要是为了降低网络请求数量,如果放在本地,不用网络请求,则没什么必要,所以要求与 配置为远程包 搭配使用
进一步说一下:import naitive config.md5.json
import:你可以理解为creator将一个显示页面导出一个配置文件,creator加载这个配置文件还是可以还原显示页面的
native:这个是资源,是实实在在的资源
config.md5.json:每一个AB包生成以后最外层都会有一个config文件,看他的名字很特别,加了一个MD5值,你可以理解这个MD5值就是当前这个AB包的名字,因为最后creator会根据这个值来找这个AB包

过程

下面这张图是位于src/setting.js的文件内容,他是一份配置
bundleVers:这个字段记录了当前包使用的各个AB包的版本,用一个MD5值去记录,内部会自动根据这个MD5值来找到对应的配置
关于AB包说一下:creator自己内置四个AB包
1:interval这个内置包主要是creator自己使用的资源(shader,图片等)
2:start-scene这个内置包主要是我们如果在构建工程的时候,如果勾选了初始场景分包,那么就会为我们生成这个子包,这里面主要的内容就是我们的启动场景所用到的所有资源和脚本,
3:remote这个内置包是我们构建的时候,勾选了将主包设置为远程资源包,那么就会生成这么一个内置包,
4:resources这个内置包是游戏主资源包,如果我们勾选了主包设置为远程资源包,那么该文件夹下的所有资源就会跑到同级remote文件夹下,这个remote下的所有资源可以考虑打成zip包,上传到资源服,然后再资源服将其解压
特别注意1:我们一般将主要资源都放在resources这个文件夹中,资源主要包括预制体声音图片动画脚本等文件,其中脚本文件会单独的放到根目录的src文件夹下,假设我们之前用的是js文件,那么此处src中将会放置相对路径的js文件,如果之前都是ts脚本文件,那么将统一合并到相对路径的index.js文件中,这里的相对路径指的是之前是基于resources为根目录,现在是基于src为根目录
特别注意2:小游戏如果代码包超过了限制,那么就要分包,这里分包的代码其实就是分的主包的代码,也是上面提到的index.js,我们一般都是使用ts开发的,这些放在resouces文件夹下的ts,最终都会合并到src/scripts/resources/index.js中,我们只要读取这个文件进行代码拆分即可
特别注意3:start-scene中也会包含脚本文件,这个是不会合并到src目录下的index.js中的,它属于启动场景,不是主包,所以在自己的文件夹进行合并生成index.js,
在这里插入图片描述
ccRequire.js:这里面包含要加载的脚本文件,一部分脚本文件是我们在工程中自己使用的,还有一部分是生成assetboudle过程中产生,assetboudle对应的资源也有可能包含脚本代码,那这个代码就会自动合并到index.js文件夹中
在这里插入图片描述
在这里插入图片描述

游戏启动
调用了下面这句话来加载各个AB包,如果当前这个AB包是启动场景,那么游戏就会依据启动场景的逻辑代码开始加载游戏资源,从而进入游戏
cc.assetManager.loadBundle(bundleRoot[i], cb);
main.js

"use strict";

window.boot = function () {
    
  var settings = window._CCSettings;
  window._CCSettings = undefined;

  var onStart = function onStart() {
    
    cc.view.enableRetina(true);
    cc.view.resizeWithBrowserSize(true);
    var launchScene = settings.launchScene; // load scene

    cc.director.loadScene(launchScene, null, function () {
    
      console.log('Success to load scene: ' + launchScene);
    });
  };

  var isSubContext = cc.sys.platform === cc.sys.WECHAT_GAME_SUB;
  var option = {
    
    id: 'GameCanvas',
    debugMode: settings.debug ? cc.debug.DebugMode.INFO : cc.debug.DebugMode.ERROR,
    showFPS: !isSubContext && settings.debug,
    frameRate: 60,
    groupList: settings.groupList,
    collisionMatrix: settings.collisionMatrix
  };
  cc.assetManager.init({
    
    bundleVers: settings.bundleVers,
    subpackages: settings.subpackages,
    remoteBundles: settings.remoteBundles,
    server: settings.server,
    subContextRoot: settings.subContextRoot
  }); 

  var _cc$AssetManager$Buil = cc.AssetManager.BuiltinBundleName,
      RESOURCES = _cc$AssetManager$Buil.RESOURCES,
      INTERNAL = _cc$AssetManager$Buil.INTERNAL,
      START_SCENE = _cc$AssetManager$Buil.START_SCENE;
  var bundleRoot = [INTERNAL];
  settings.hasStartSceneBundle && bundleRoot.push(START_SCENE);
  settings.hasResourcesBundle && bundleRoot.push(RESOURCES);
  var count = 0;

  function cb(err) {
    
    if (err) return console.error(err.message, err.stack);
    count++;

    if (count === bundleRoot.length + 1) {
    
      cc.game.run(option, onStart);
    }
  } // load plugins

  //加载脚本
  cc.assetManager.loadScript(settings.jsList.map(function (x) {
    
    return 'src/' + x;
  }), cb); // load bundles
  
  //加载所有bundle里生成的index.js脚本
  //这里包含了内置的start-scene这个AB包所包含的启动脚本
  for (var i = 0; i < bundleRoot.length; i++) {
    
    cc.assetManager.loadBundle(bundleRoot[i], cb);
  }
};

拓展

1:微信小程序开发者模式右上角的RT-FPS和Min-FPS和EX-FPS分别含义

rt-fps  : runtime fps  实时 帧率
ex-fps :是极限帧率,可以理解为在不受驱动帧率的限制下(大部分手机微 60fps),仅仅计算 js 运行耗时,可以达到的极限帧率。这个数字可以用于评估在满帧的前提下,运行性能是否有变化
min-fps: 最小帧率

那creator中又是如何设置帧率的呢
window.requestAnimationFrame这个是浏览器的刷新界面函数,每秒调用60次,这个是死的,如果我们想一秒钟调用30次,下面的实现的逻辑就是奇偶帧错开渲染,这不就是30帧了吗,目前只支持这两种帧率,如果低于这个帧率,那么就会显示不正常,但是根据这个规律,其实可以自定义各种帧率,只是没有意义而已
如果想该改变window.requestAnimationFrame这个函数的刷新时间,可以通过wx.setPreferredFramesPerSecond(fps)这个函数

//  @Game play control
    /**
     * !#en Set frame rate of game.
     * !#zh 设置游戏帧率。
     * @method setFrameRate
     * @param {Number} frameRate
     */
    setFrameRate: function (frameRate) {
    
        var config = this.config;
        config.frameRate = frameRate;
        if (this._intervalId)
            window.cancelAnimFrame(this._intervalId);
        this._intervalId = 0;
        this._paused = true;
        this._setAnimFrame();
        this._runMainLoop();
    },
//  @Time ticker section
    _setAnimFrame: function () {
    
        this._lastTime = performance.now();
        var frameRate = game.config.frameRate;
        this._frameTime = 1000 / frameRate;
        cc.director._maxParticleDeltaTime = this._frameTime / 1000 * 2;
        if (CC_JSB || CC_RUNTIME) {
    
            jsb.setPreferredFramesPerSecond(frameRate);
            window.requestAnimFrame = window.requestAnimationFrame;
            window.cancelAnimFrame = window.cancelAnimationFrame;
        }
        else {
    
            if (frameRate !== 60 && frameRate !== 30) {
    
                window.requestAnimFrame = this._stTime;
                window.cancelAnimFrame = this._ctTime;
            }
            else {
    
                window.requestAnimFrame = window.requestAnimationFrame ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                window.oRequestAnimationFrame ||
                window.msRequestAnimationFrame ||
                this._stTime;
                window.cancelAnimFrame = window.cancelAnimationFrame ||
                window.cancelRequestAnimationFrame ||
                window.msCancelRequestAnimationFrame ||
                window.mozCancelRequestAnimationFrame ||
                window.oCancelRequestAnimationFrame ||
                window.webkitCancelRequestAnimationFrame ||
                window.msCancelAnimationFrame ||
                window.mozCancelAnimationFrame ||
                window.webkitCancelAnimationFrame ||
                window.oCancelAnimationFrame ||
                this._ctTime;
            }
        }
    },
    _stTime: function(callback){
    
        //获取页面加载到现在的时间 单位(毫秒)
        var currTime = performance.now();
        var timeToCall = Math.max(0, game._frameTime - (currTime - game._lastTime));
        var id = window.setTimeout(function() {
     callback(); },
            timeToCall);
        game._lastTime = currTime + timeToCall;
        return id;
    },
    _ctTime: function(id){
    
        window.clearTimeout(id);
    },
//Run game.
   
    _runMainLoop: function () {
    
    
        if (CC_EDITOR) {
    
            return;
        }
        if (!this._prepared) return;

        var self = this, callback, config = self.config,
            director = cc.director,
            skip = true, frameRate = config.frameRate;

        debug.setDisplayStats(config.showFPS);

        callback = function (now) {
    
            if (!self._paused) {
    
                self._intervalId = window.requestAnimFrame(callback);
                if (!CC_JSB && !CC_RUNTIME && frameRate === 30) {
    
                    if (skip = !skip) {
    
                        return;
                    }
                }
                director.mainLoop(now);
            }
        };

        self._intervalId = window.requestAnimFrame(callback);
        self._paused = false;
    },

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/HzjCsdn/article/details/111415707

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签