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

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法