cc.debug研究
CCDebug.js:cocos2d\core\CCDebug.js
相关函数
函数列表
方法名 | 作用 | 参数 |
---|---|---|
cc.log | 普通输出 | (msg: string/any, …subst: any[]) |
cc.warn | 警告类型输出,chrome中有黄色背景,黄色警告图标 | |
cc.error | 错误类型输出,chrome中有红色背景,红色错误图标 | |
cc.assert | 断言性输出,当传入的condition为false时才起作用,本质上是cc.error,在调试模式并在chrome中时,会有程序运行挂起的效果,继续运行会跳过后续代码。有助于debug。 | (condition?: boolean, …data: any[]) |
测试代码
onLoad () {
cc.log("这是一条log");
cc.warn("这是一条warn");
cc.error("这是一条error");
cc.log("这是一条log", 111, "其他信息");
cc.assert(true, "这是第一条assert");
cc.assert(false, "这是第二条assert");
cc.log("该行代码被跳过");
},
start () {
cc.log("这是一条log start");
},
测试结果
在Cocos Creator中,点击运行预览按钮(工具栏上类似播放键的那个)使用浏览器模式,预览。
运行到cc.assert(false, “这是第二条assert”);时,被挂起。
点击顶部蓝色按钮继续运行。
- 第一条cc.assert中,由于传入第一个参数为true,故没有任何效果。
- 继续运行后,后续的代码被跳过,但不会影响其他地方的代码,如start()中的输出语句。
- 输出/运行效果(如颜色)在不同的浏览器/运行环境下可能不同。
代码实现
日志等级(DebugMode)
值 | 枚举名 | 作用 |
---|---|---|
0 | NONE | 禁止模式,啥也不打印 |
1 | INFO | 信息模式,啥都打印 |
2 | WARN | 警告模式,警告及以上打印 |
3 | ERROR | 错误模式,仅打印错误 |
4 | INFO_FOR_WEB_PAGE | 在画面上输出,同INFO(仅WEB端下生效) |
5 | WARN_FOR_WEB_PAGE | 在画面上输出,同WARN(仅WEB端下生效) |
6 | ERROR_FOR_WEB_PAGE | 在画面上输出,同ERROR(仅WEB端下生效) |
后面三种模式其实和前面的是一样的,但只有在Web端才生效,因为打印到画面的实现方法是,使用代码动态创建一个HTML节点,并将日志文本输出到节点中,所以在其他端无法使用。
初始化
// CCGame.js cocos2d\core\CCGame.js
_initConfig (config) {
// 若输入不合法的debugMode 修改为0(DebugMode.NONE 啥等级的也不显示)
if (typeof config.debugMode !== 'number') {
config.debugMode = 0;
}
// 此处省略部分代码
// 调用_resetDebugSetting方法进行初始化,此处的debugMode默认为cc.debug.DebugMode.INFO
// DebugMode.INFO:显示所有日志
debug._resetDebugSetting(config.debugMode);
}
resetDebugSetting
// CCDebug.js cocos2d\core\CCDebug.js
module.exports = cc.debug = {
// 此处省略部分代码
_resetDebugSetting: resetDebugSetting,
}
let resetDebugSetting = function (mode) {
// reset
// 重置各个方法,这就是设置日志等级的核心所在。
// 重置后再根据mode参数给对应的方法赋值,即实现了只打印部分等级日志。
cc.log = cc.warn = cc.error = cc.assert = function () {};
// 啥也不打印 所以直接return 哪个方法都不配置
if (mode === DebugMode.NONE)
return;
// 当mode大于ERROR的时候 是要将日志打印到画面中
if (mode > DebugMode.ERROR) {
//log to web page
// 把日志打印到游戏画面上
// 用代码创建一个div节点,固定在左上角,里面放一个textarea,然后把日志写到textarea里
// 日志打印到一个textarea,所以还可以拖动改大小 妙啊
// 是不是可以把创建节点的代码挪到外面去?这样就可以不用每次都判断是否存在logList
function logToWebPage (msg) {
if (!cc.game.canvas)
return;
if (!logList) {
var logDiv = document.createElement("Div");
logDiv.setAttribute("id", "logInfoDiv");
logDiv.setAttribute("width", "200");
logDiv.setAttribute("height", cc.game.canvas.height);
var logDivStyle = logDiv.style;
logDivStyle.zIndex = "99999";
logDivStyle.position = "absolute";
logDivStyle.top = logDivStyle.left = "0";
logList = document.createElement("textarea");
logList.setAttribute("rows", "20");
logList.setAttribute("cols", "30");
logList.setAttribute("disabled", "true");
var logListStyle = logList.style;
logListStyle.backgroundColor = "transparent";
logListStyle.borderBottom = "1px solid #cccccc";
logListStyle.borderTopWidth = logListStyle.borderLeftWidth = logListStyle.borderRightWidth = "0px";
logListStyle.borderTopStyle = logListStyle.borderLeftStyle = logListStyle.borderRightStyle = "none";
logListStyle.padding = "0px";
logListStyle.margin = 0;
logDiv.appendChild(logList);
cc.game.canvas.parentNode.appendChild(logDiv);
}
logList.value = logList.value + msg + "\r\n";
logList.scrollTop = logList.scrollHeight;
}
// 修改cc.error和cc.assert的实现,均为调用上面的logToWebPage方法
// 输出到画面上固然不错,但缺点就是只能通过添加等级前缀,不能通过颜色清晰标识不同等级的日志
cc.error = function () {
// cc.js.formatStr: 格式化字符串 不太相干,但是觉得蛮有意思的,写在最后https://www.yuque.com/yuhuo-2kehw/ehcgn5/klfugv#6TajH
logToWebPage("ERROR : " + cc.js.formatStr.apply(null, arguments));
};
cc.assert = function (cond, msg) {
'use strict';
if (!cond && msg) {
msg = cc.js.formatStr.apply(null, cc.js.shiftArguments.apply(null, arguments));
logToWebPage("ASSERT: " + msg);
}
};
// 等级不是只输出ERROR及以上,那就可以设置cc.warn
if (mode !== DebugMode.ERROR_FOR_WEB_PAGE) {
cc.warn = function () {
logToWebPage("WARN : " + cc.js.formatStr.apply(null, arguments));
};
}
// 等级是输出INFO及以上,那就可以设置cc.log
if (mode === DebugMode.INFO_FOR_WEB_PAGE) {
cc.log = function () {
logToWebPage(cc.js.formatStr.apply(null, arguments));
};
}
}
else if (console && console.log.apply) {
//console is null when user doesn't open dev tool on IE9
//log to console
// 输出日志到控制台。在使用IE9的时候,若没有打开dev tool,console为null
// For JSB
// 兼容JSB??为了JSB??
if (!console.error) console.error = console.log;
if (!console.warn) console.warn = console.log;
/**
* !#en
* Outputs an error message to the Cocos Creator Console (editor) or Web Console (runtime).<br/>
* - In Cocos Creator, error is red.<br/>
* - In Chrome, error have a red icon along with red message text.<br/>
* !#zh
* 输出错误消息到 Cocos Creator 编辑器的 Console 或运行时页面端的 Console 中。<br/>
* - 在 Cocos Creator 中,错误信息显示是红色的。<br/>
* - 在 Chrome 中,错误信息有红色的图标以及红色的消息文本。<br/>
*
* @method error
* @param {any} msg - A JavaScript string containing zero or more substitution strings.
* @param {any} ...subst - JavaScript objects with which to replace substitution strings within msg. This gives you additional control over the format of the output.
*/
// 如果是编辑器模式,则error输出到CocosCreator里的控制台
if (CC_EDITOR) {
cc.error = Editor.error;
}
else if (console.error.bind) {
// use bind to avoid pollute call stacks
// 使用bind以避免污染调用栈。
// 先判断是否有bind方法,是因为在某些js引擎中,log不支持bind函数(极少数情况下才会如此)。
// 至于为什么要bind(console),后面会展开
cc.error = console.error.bind(console);
}
else {
// 若运行在原生环境下(CC_JSB)或运行时环境(CC_RUNTIME)下,使用console.error
// 否则使用apply函数调用,并传递this为console
cc.error = CC_JSB || CC_RUNTIME ? console.error : function () {
return console.error.apply(console, arguments);
};
}
cc.assert = function (cond, msg) {
// 当cond为false时,才执行日志输出
if (!cond) {
if (msg) {
msg = cc.js.formatStr.apply(null, cc.js.shiftArguments.apply(null, arguments));
}
if (CC_DEV) {
// 开发环境下,会挂起程序运行,这就是测试结果中提到的被挂起,方便调试
debugger;
}
if (CC_TEST) {
// 单元测试环境下,调用ok函数,应该是用于输出测试结果,定义于test\qunit\lib\qunit.js
ok(false, msg);
}
else {
// 抛出错误
throw new Error(msg);
}
}
}
}
// 不是仅输出ERROR及以上 定义cc.warn
// 内容大多和上面的cc.error类似
if (mode !== DebugMode.ERROR) {
/**
* !#en
* Outputs a warning message to the Cocos Creator Console (editor) or Web Console (runtime).
* - In Cocos Creator, warning is yellow.
* - In Chrome, warning have a yellow warning icon with the message text.
* !#zh
* 输出警告消息到 Cocos Creator 编辑器的 Console 或运行时 Web 端的 Console 中。<br/>
* - 在 Cocos Creator 中,警告信息显示是黄色的。<br/>
* - 在 Chrome 中,警告信息有着黄色的图标以及黄色的消息文本。<br/>
* @method warn
* @param {any} msg - A JavaScript string containing zero or more substitution strings.
* @param {any} ...subst - JavaScript objects with which to replace substitution strings within msg. This gives you additional control over the format of the output.
*/
if (CC_EDITOR) {
cc.warn = Editor.warn;
}
else if (console.warn.bind) {
// use bind to avoid pollute call stacks
cc.warn = console.warn.bind(console);
}
else {
cc.warn = CC_JSB || CC_RUNTIME ? console.warn : function () {
return console.warn.apply(console, arguments);
};
}
}
// 编辑器模式下 保持cc.log可用 输出到Cocos Creator的控制台中
// 这里有点不解CC_EDITOR指的是在哪运行,测试过程中 模拟器和网页预览 均为false。还可以在引擎内跑预览了?
// 后面的定义也大多和上面的类似
if (CC_EDITOR) {
cc.log = Editor.log;
}
else if (mode === DebugMode.INFO) {
/**
* !#en Outputs a message to the Cocos Creator Console (editor) or Web Console (runtime).
* !#zh 输出一条消息到 Cocos Creator 编辑器的 Console 或运行时 Web 端的 Console 中。
* @method log
* @param {String|any} msg - A JavaScript string containing zero or more substitution strings.
* @param {any} ...subst - JavaScript objects with which to replace substitution strings within msg. This gives you additional control over the format of the output.
*/
if (CC_JSB || CC_RUNTIME) {
if (scriptEngineType === "JavaScriptCore") {
// console.log has to use `console` as its context for iOS 8~9. Therefore, apply it.
// iOS 8~9中,log函数必须使用console作为它的上下文(this),所以使用apply指定this
cc.log = function () {
return console.log.apply(console, arguments);
};
} else {
cc.log = console.log;
}
}
else if (console.log.bind) {
// use bind to avoid pollute call stacks
cc.log = console.log.bind(console);
}
else {
cc.log = function () {
return console.log.apply(console, arguments);
};
}
}
};
为什么bind(console)
cc.error = console.error.bind(console);
不论是cc.error还是.log .warn均有类似的写法。一开始很不解,看不出来有什么作用,在stackoverflow中找到一个说的还不错的,试着自己说说理解。先贴链接What does this statement do? console.log.bind(console)
其中有一段比较重要的
Suppose you wanted a shorter form of
console.log
, likef
. You might do this:
var f = console.log; // <== Suspect!
…but if the
log
function relies onthis
referring to theconsole
object during the call, then callingf("Message here")
won’t work, becausethis
won’t refer toconsole
.
翻译:如果log方法需要this指向console对象,那么当你调用f()函数的时候,就无法实现你想要的效果(输出信息),因为this不指向console。
为什么需要this指向console呢… 想弄明白怕是要去研究引擎… 故先放下。继续看源码发现,源码中已有说明,在cc.log的赋值中有这样一句注释
console.log has to use
console
as its context for iOS 8~9. Therefore, apply it.
大概便是为了兼容ios8~9,才做了这番操作。当然,或许也有其他引擎需要做兼容,这里便不深入研究了…
cc.debug模块属性
属性名 | 类型 | 作用 | 参数 | 备注 |
---|---|---|---|---|
DebugMode | cc.Enum | 调试模式 枚举类型 用于过滤显示日志的级别 | ||
getError | function | 通过 error id和一些参数来生成错误信息 | (errorId: string, param?: any) | 实际上是CCDebug中的getTypedFormatter方法 |
isDisplayStats | function | 是否显示FPS相关信息的分析工具,在左下角 | ||
setDisplayStats | function | 与上一个方法对应。用于设置显示状态 | (displayStats: boolean) | |
_resetDebugSetting | function | 重置调试相关的设置 | (mode: DebugMode) |
DebugMode和_resetDebugSetting上面已经写过,下面来看一下FPS信息(DisplayStats)和一些其他内容
FPS信息(分析工具)
module.exports = cc.debug = {
// 省略部分代码
/**
* !#en Returns whether or not to display the FPS informations.
* !#zh 是否显示 FPS 信息。
* @method isDisplayStats
* @return {Boolean}
*/
isDisplayStats: function () {
return cc.profiler ? cc.profiler.isShowingStats() : false;
},
/**
* !#en Sets whether display the FPS on the bottom-left corner.
* !#zh 设置是否在左下角显示 FPS。
* @method setDisplayStats
* @param {Boolean} displayStats
*/
setDisplayStats: function (displayStats) {
if (cc.profiler && cc.game.renderType !== cc.game.RENDER_TYPE_CANVAS) {
displayStats ? cc.profiler.showStats() : cc.profiler.hideStats();
cc.game.config.showFPS = !!displayStats;
}
},
}
cc.profiler是实现FPS分析工具的模块,定义于cocos2d\core\utils\profiler\CCProfiler.js
内容包括FPS、Draw Call、Frame time等
网页预览和模拟器会显示FPS界面,小游戏中没有,故推测是一些平台下不会有这个模块,所以需要先判断有没有cc.profiler
实现原理是监听事件然后记录、计算、刷新界面。展开看起来也可以写挺长的…这里就不展开了
// 省略了部分代码
cc.profiler = module.exports = {
showStats () {
cc.director.on(cc.Director.EVENT_BEFORE_UPDATE, beforeUpdate);
cc.director.on(cc.Director.EVENT_AFTER_UPDATE, afterUpdate);
cc.director.on(cc.Director.EVENT_AFTER_DRAW, afterDraw);
}
}
其他内容
cc.js.formatStr
// js.js cocos2d\core\platform\js.js
var REGEXP_NUM_OR_STR = /(%d)|(%s)/;
var REGEXP_STR = /%s/;
/**
* A string tool to construct a string with format string.
* @method formatStr
* @param {String|any} msg - A JavaScript string containing zero or more substitution strings (%s).
* @param {any} ...subst - JavaScript objects with which to replace substitution strings within msg. This gives you additional control over the format of the output.
* @returns {String}
* @example
* cc.js.formatStr("a: %s, b: %s", a, b);
* cc.js.formatStr(a, b, c);
* 一个构造有格式化字符串的字符串工具 翻的好拗口...
* 后面的举例更容易看懂,其实就是类似于c语言中的输出里的转换说明符吧
*/
js.formatStr = function () {
var argLen = arguments.length;
if (argLen === 0) {
return '';
}
var msg = arguments[0];
if (argLen === 1) {
return '' + msg;
}
// 是否需要替换,当msg是string且正则表达式匹配得到的情况下为true
var hasSubstitution = typeof msg === 'string' && REGEXP_NUM_OR_STR.test(msg);
if (hasSubstitution) {
// 遍历参数列表,把msg中的%d或%s替换为参数
for (let i = 1; i < argLen; ++i) {
var arg = arguments[i];
var regExpToTest = typeof arg === 'number' ? REGEXP_NUM_OR_STR : REGEXP_STR;
// 如果匹配到%d或%s 把arg替换到文本中,否则把arg添加到msg末尾
if (regExpToTest.test(msg)) {
const notReplaceFunction = '' + arg;
msg = msg.replace(regExpToTest, notReplaceFunction);
}
else
msg += ' ' + arg;
}
}
else {
// 如若不需要替换 则把参数全部加到msg末尾
for (let i = 1; i < argLen; ++i) {
msg += ' ' + arguments[i];
}
}
return msg;
};
游戏内日志输出
cc.debug._resetDebugSetting(cc.debug.DebugMode.INFO_FOR_WEB_PAGE);
cc.log("这是一条log");
cc.warn("这是一条warn");
cc.error("这是一条error");
问题来了… 为什么warn没有生效? 查看控制台,发现warn被打印到控制台上了
回去查看源码,发现在定义warn方法的时候,判断的是
if (mode !== DebugMode.ERROR)
所以warn函数在INFO_FOR_WEB_PAGE模式下,会被覆盖成输出到控制台
而定义log函数的时候,判断的是
if (CC_EDITOR) { cc.log = Editor.log; } else if (mode === DebugMode.INFO) {
所以info函数在INFO_FOR_WEB_PAGE模式下不会被覆盖
这算一个bug吗?
cc.logID
类似的方法有
- cc.logID
- cc.warnID
- cc.errorID
- cc.assertID
在内部其实调用的也是对应的cc.xxx方法,如cc.logId调用cc.log
拿cc.logId出来分析就好。实现的功能是根据id找到对应的错误信息,然后打印日志
// 创建一个日志格式化器
var logFormatter = getTypedFormatter('Log');
cc.logID = function () {
// 调用log方法输出内容
cc.log(logFormatter.apply(null, arguments));
};
function getTypedFormatter (type) {
// 这里会有一个闭包,后面调用logID函数的时候,type都是Log,其他函数也会有对应的type文本。
return function () {
var id = arguments[0];
// 根据id设置错误信息
// 非调试模式下,msg包含type,id,以及cocos错误信息的链接
// const debugInfos = require('../../DebugInfos') || {};
var msg = CC_DEBUG ? (debugInfos[id] || 'unknown id') : `${type} ${id}, please go to ${ERROR_MAP_URL}#${id} to see details.`;
if (arguments.length === 1) {
return msg;
}
else if (arguments.length === 2) {
// 有两个参数,把第二个参数连接到日志信息后面
return CC_DEBUG ? cc.js.formatStr(msg, arguments[1]) :
msg + ' Arguments: ' + arguments[1];
}
else {
// 有多个参数,把参数使用", "连接,添加到日志信息后面
var argsArray = cc.js.shiftArguments.apply(null, arguments);
return CC_DEBUG ? cc.js.formatStr.apply(null, [msg].concat(argsArray)) :
msg + ' Arguments: ' + argsArray.join(', ');
}
};
}
debugInfos定义于"…/…/DebugInfos",但我没有在源码里找到这个文件…
在源码里的错误代码说明文件是EngineErrorMap.md
这个方法主要是用于提示cocos引擎内部的一些错误/提示,所以平时估计也用不上。但可以参考这些方法做一个自己的错误信息表。
试试看效果
// 测试代码
cc.errorID(1400, 'cc.info', 'cc.log');
// EngineErrorMap.md
### 1400
'%s' is deprecated, please use '%s' instead.
可以看到读取了错误代码对应的信息,且字符串中的%s被替换为后面的参数内容
还有就是cc.debug.getError也是使用类似的实现
getError: getTypedFormatter('ERROR')
cc._throw
// 编辑器模式下 输出error到Cocos Creator的控制台中
// 否则使用callInNextTick,在执行完当前代码后,执行回调,抛出错误
cc._throaw = CC_EDITOR ? Editor.error : function (error) {
utils.callInNextTick(function () {
throw error;
});
};
尝试一下
cc._throw("抛出一个异常");
这个函数在cocos引擎中调用的地方不多,多是在try catch中调用,以抛出异常。
收尾
- ccdebug模块的代码其实不多,但还蛮有意思的,尤其是发现可以在游戏界面打日志… 我也不知道在有意思个什么(淦)
- 对于FPS分析工具,一开始没有多想,看到源码之后,却突然很想仔细研究一下实现,或许有机会?
- 水平有限,若有纰漏还望各位大佬指正