一、调试时print日志过多的痛点拆解

很多开发者在写代码调试时,都会遇到一个头疼的问题:为了找bug,到处加print语句,结果运行的时候控制台刷满了杂乱的日志——有用的报错信息被一堆调试日志淹没,找半天找不到问题;要是删了调试日志,下次改代码又得重新加,来回折腾特别费时间。比如做游戏开发时,角色移动、碰撞、状态切换这些环节都要打日志,运行一次控制台能滚几千行,翻页都翻不动,这种场景下,要么找日志找瞎,要么改代码改疯,完全影响开发效率。

要解决这个问题,核心思路有两个:一是只在需要的时候显示特定的日志,不需要的时候自动隐藏;二是把日志按重要程度分类,方便快速筛选。接下来就结合Godot引擎的GDScript语言,一步步讲怎么实现这两个思路,从基础的条件编译到自定义完整的日志系统。

二、GDScript条件编译的基础用法

条件编译简单说,就是让代码在不同的编译条件下,只执行部分逻辑,另一部分直接被编译器忽略,相当于给代码加了“开关”——打开开关就执行这段代码,关闭就直接跳过,不会占用运行资源。GDScript里的条件编译用#if#elif#else#endif这几个关键字,和普通的if判断不一样,普通if是运行时判断,条件编译是编译时就确定哪些代码要保留,哪些要删掉,不会出现在最终的运行程序里。

2.1 条件编译的基本语法示例

技术栈:Godot GDScript 比如我们可以定义一个调试开关,开发时打开,发布时关闭,所有调试用的print语句都包在这个开关里,发布时就不会生成这些代码。示例代码如下:

# 定义调试模式开关,1代表开启,0代表关闭,发布时改成0
#define DEBUG_MODE 1

func _ready():
    # 条件编译:只有DEBUG_MODE为1时,才会执行下面的代码
    #if DEBUG_MODE
        print("这是调试日志,发布时会被自动去掉")
        print("角色初始位置:", position)
    #endif
    # 下面是正式的业务代码,不管调试开关开不开都会执行
    var speed = 100

这里要注意,#define定义的开关是全局的,只要在一个脚本里定义了,整个项目的所有脚本都能用到这个开关。比如我们可以在Godot的项目设置里定义全局开关,不用每个脚本都写#define,步骤是打开项目设置->自定义配置->全局定义,加一个DEBUG_MODE,值设为1,这样所有脚本都能直接用这个开关,发布时只要改成0就行。

2.2 条件编译的应用场景与优缺点

应用场景:除了控制调试日志的显示,条件编译还可以用来区分不同平台的代码,比如安卓和iOS的权限判断,PC和主机的输入处理;或者做功能开关,比如某个付费功能,只有付费用户的版本才会编译对应的代码,免费版本直接去掉。

优点:一是完全不占用发布程序的运行资源,因为不需要的代码编译时就被删掉了,比运行时的if判断效率更高;二是简单易用,只要加几个关键字就能实现开关,不需要额外的代码逻辑;三是能避免发布时忘记删调试代码的问题,只要改全局开关就行。

缺点:一是开关只能是编译时确定的常量,不能用运行时的变量当条件,比如不能根据用户的操作来开关调试日志;二是如果开关定义错了,比如发布时忘了改成0,就会导致发布版本还带调试日志,存在安全风险;三是如果项目里开关太多,会导致代码混乱,维护起来麻烦。

三、自定义日志系统的搭建

条件编译能解决发布时去掉调试日志的问题,但开发时还是会有很多不同类型的日志,比如调试用的、报错用的、提示用的,混在一起还是不好找。这时候就需要自定义一个日志系统,把日志按类型分类,还能控制哪些类型的日志显示,哪些隐藏。

3.1 自定义日志系统的核心需求

一个好用的自定义日志系统,至少要满足这几个需求:一是能区分日志的类型,比如调试、信息、警告、错误;二是能给日志加前缀,比如显示日志的类型、时间、脚本名,方便定位;三是能控制日志的输出,比如只显示错误日志,或者只显示某个脚本的日志;四是能把日志保存到文件,方便后续查看。

3.2 自定义日志系统的完整实现

技术栈:Godot GDScript 我们可以做一个单例的日志类,因为日志系统在整个项目里只需要一个实例,用单例就能全局调用,不用每次都实例化。首先新建一个脚本,命名为Logger.gd,代码如下:

# 日志类型枚举,定义四种常用的日志类型
enum LogType {
    DEBUG,  # 调试日志,开发时用
    INFO,   # 信息日志,比如操作提示
    WARNING, # 警告日志,比如参数不对但不影响运行
    ERROR   # 错误日志,比如报错、异常
}

# 日志配置:设置允许输出的最低日志级别,低于这个级别的日志会被隐藏
# 比如设为WARNING,就只会显示WARNING和ERROR级别的日志
var min_log_level: LogType = LogType.DEBUG

# 日志前缀配置:是否显示日志类型、时间、脚本名
var show_log_type: bool = true
var show_time: bool = true
var show_script_name: bool = true

# 单例实例,确保只有一个日志实例
static var instance: Logger = null

func _ready():
    # 初始化单例实例
    instance = self
    # 给Godot的输出系统加钩子,把所有输出都转到自定义日志系统
    OS.set_logger(self)

# 核心日志输出方法,所有日志都通过这个方法输出
func log(log_type: LogType, message: String, script_name: String = ""):
    # 先判断当前日志级别是否符合要求,不符合就直接返回
    if log_type < min_log_level:
        return
    
    # 组装日志前缀
    var prefix: String = ""
    # 加时间前缀
    if show_time:
        prefix += "[" + Time.get_datetime_string_from_system() + "] "
    # 加日志类型前缀
    if show_log_type:
        prefix += "[" + str(log_type) + "] "
    # 加脚本名前缀
    if show_script_name and script_name != "":
        prefix += "[" + script_name + "] "
    
    # 组装完整日志内容
    var full_message: String = prefix + message
    
    # 不同类型的日志用不同的颜色显示,方便区分
    match log_type:
        LogType.DEBUG:
            print(full_message) # 调试日志用默认颜色
        LogType.INFO:
            print_rich("[color=cyan]" + full_message + "[/color]") # 信息日志用青色
        LogType.WARNING:
            print_rich("[color=yellow]" + full_message + "[/color]") # 警告日志用黄色
        LogType.ERROR:
            print_rich("[color=red]" + full_message + "[/color]") # 错误日志用红色
    
    # 可选:把日志保存到文件
    save_log_to_file(full_message)

# 保存日志到文件的方法
func save_log_to_file(message: String):
    # 打开日志文件,不存在就创建
    var file: File = File.new()
    # 日志文件路径,放在项目的用户目录下,不同平台路径不同
    var log_path: String = OS.get_user_data_dir() + "/game_logs.txt"
    # 以追加模式打开文件
    if file.open(log_path, File.READ_WRITE) == OK:
        # 移动到文件末尾,追加内容
        file.seek_end()
        file.store_string(message + "\n")
        file.close()

# 给不同日志类型封装快捷方法,方便调用
func debug(message: String, script_name: String = ""):
    log(LogType.DEBUG, message, script_name)

func info(message: String, script_name: String = ""):
    log(LogType.INFO, message, script_name)

func warning(message: String, script_name: String = ""):
    log(LogType.WARNING, message, script_name)

func error(message: String, script_name: String = ""):
    log(LogType.ERROR, message, script_name)

接下来要把这个日志脚本设为单例,这样所有脚本都能直接调用。步骤是打开项目设置->自动加载->添加脚本,选Logger.gd,名称设为Logger,点确定就行。

然后在其他脚本里调用这个日志系统,示例代码如下:

# 比如在角色控制脚本里调用
func _process(delta):
    # 调试日志,显示角色的位置
    Logger.debug("角色当前位置:" + str(position), "PlayerController")
    # 信息日志,显示角色移动速度
    Logger.info("角色移动速度:" + str(speed), "PlayerController")
    # 警告日志,当速度超过100时提示
    if speed > 100:
        Logger.warning("角色速度过快,可能出现异常", "PlayerController")
    # 错误日志,当位置超出边界时提示
    if position.x < -1000 or position.x > 1000:
        Logger.error("角色位置超出边界,需要重置", "PlayerController")

这样输出的日志就会有前缀,不同类型的日志颜色不一样,比如错误日志是红色,一眼就能看到,调试日志可以随时关掉,只要把min_log_level改成LogType.INFO,调试日志就不会显示了。

3.3 自定义日志系统的应用场景与优缺点

应用场景:除了游戏开发,任何需要调试的项目都能用,比如工具开发、小程序开发、后端服务开发;还能用来做用户行为日志,比如记录用户的操作路径,方便分析用户行为;或者做错误监控,把错误日志上传到服务器,方便后续排查线上问题。

优点:一是灵活度高,可以根据需求定制日志的格式、颜色、输出方式;二是能分类筛选日志,方便快速定位问题;三是可以保存日志到文件,方便后续查看;四是不依赖编译器,运行时就能调整日志级别,比如在游戏里加一个调试开关,玩家可以手动打开调试日志。

缺点:一是需要自己写代码实现,比直接用print语句麻烦;二是如果实现不好,可能会影响运行效率,比如频繁写日志到文件会占用IO资源;三是如果日志系统有bug,会导致日志输出混乱,反而影响调试。

四、两种方案的结合使用与注意事项

条件编译和自定义日志系统不是二选一的关系,而是可以结合使用,比如开发时用自定义日志系统分类筛选日志,发布时用条件编译把调试日志的代码去掉,这样既能保证开发效率,又能保证发布版本的性能和安全。

4.1 结合使用的示例

技术栈:Godot GDScript 我们可以在自定义日志系统里加条件编译,把调试日志的代码包在条件编译里,发布时自动去掉。示例代码如下:

# 定义调试模式开关,发布时改成0
#define DEBUG_MODE 1

enum LogType {
    DEBUG,
    INFO,
    WARNING,
    ERROR
}

var min_log_level: LogType = LogType.DEBUG

func log(log_type: LogType, message: String, script_name: String = ""):
    # 条件编译:只有DEBUG_MODE为1时,才会输出调试日志
    #if DEBUG_MODE
        if log_type < min_log_level:
            return
        # 组装前缀、输出日志的代码
    #else
        # 发布时,只输出INFO及以上级别的日志,调试日志直接去掉
        if log_type < LogType.INFO:
            return
        # 组装前缀、输出日志的代码
    #endif

这样开发时可以随意用调试日志,发布时只要把DEBUG_MODE改成0,所有调试日志的代码就会被编译器去掉,既安全又高效。

4.2 注意事项

  1. 条件编译的开关要统一管理,最好在项目设置里定义全局开关,不要在每个脚本里单独定义,避免开关不一致的问题。
  2. 自定义日志系统的单例要确保初始化成功,避免调用时出现空指针错误。
  3. 日志级别要合理设置,不要把所有日志都设为最高级别,也不要把重要的日志设为最低级别,方便筛选。
  4. 保存日志到文件时,要注意文件大小,避免日志文件过大占用过多存储空间,可以定期清理旧日志。
  5. 发布版本要确保去掉所有调试日志,避免泄露项目的内部信息,比如代码路径、变量名等。

五、总结

调试时print日志过多是很多开发者都会遇到的问题,解决这个问题的核心是给日志加“开关”和“分类”。GDScript的条件编译可以实现编译时的开关,简单高效,适合控制发布版本的日志;自定义日志系统可以实现运行时的分类和筛选,灵活度高,适合开发时的调试。把两种方案结合使用,既能提高开发效率,又能保证发布版本的质量。

无论是条件编译还是自定义日志系统,本质上都是为了让开发者更清晰地看到有用的信息,减少无效信息的干扰。在实际开发中,可以根据项目的需求选择合适的方案,小项目可以用条件编译,大项目可以用自定义日志系统,或者两者结合,找到最适合自己的开发流程。