• 《游戏引擎架构》笔记九


    调试及开发工具

    日志及跟踪

    printf调试法有时是很好的调试方法。因为有些bug很难用断点和监视窗口跟踪;有些bug有时间依赖性,只有全速运行时才会出现等等。这事打印信息就是很好的调试方法。

    win32窗口应用程序没有控制台显示输出的函数,但是visual studio中有函数OutputDebugString()打印信息。但是它不支持格式化输出,所以Windows游戏引擎以自定义函数包装此函数

    #include<stdio.h>
    #ifndef WIN32_LEAN_AND_MEAN
    #define WIN32_LEAN_AND_MEAN 1
    #endif
    #include<windows.h>
    
    int VDebugPrintF(const char* format, va_list argList){
        const U32 MAX_CHARS = 1023;
        static char s_buffer[MAX_CHARS + 1];
        int charsWritten = vsnprintf(s_buffer, MAX_CHARS, format, argList);
        s_buffer[MAX_CHARS] = '';//字符串以''结尾
    
        OutputDebugStringA(s_buffer);
        //OutputDebugString(s_buffer);//VS2012不兼容
        return charsWritten;
    }
    
    int DebugPrintF(const char *format, ...){
        va_list argList;//处理变参(...)的一组宏,具体参考:http://blog.csdn.net/aihao1984/article/details/5953668
        _crt_va_start(argList, format);//VS2008定义方式:#define va_start _crt_va_start
        int charsWritten = VDebugPrintF(format, argList);
        _crt_va_end(argList);
        return charsWritten;
    }

    冗长级别

    当你在代码中添加了适当的打印语句后,最好能保留它们,以便以后需要时使用。为此,引擎通常提供一些机制来控制冗长级别。根据全局的冗长级别,打印出对应的信息。简单的实现方式:把当前系统的冗长级别存储在一个全局整数变量中,或可命名为g_verbosity。然后提供一个函数VerboseDebugPrintF()函数,其首个参数是冗长级别。

    int g_verbosity = 0;
    
    void VerboseDebugPrintF(int verbosity, const char *format, ...){
        if (g_verbosity >= verbosity){
            va_list argList;
            _crt_va_start(format, argList);
            VDebugPrintF(format, argList);
            _crt_va_end(argList);
        }
    }

    频道

    将调试输出分类位频道是很有用的功能。例如PlayStation3,可以把调试输出到14个TTY窗口之一,而且每条消息还会抄送至一个特别的TTY窗口(它包含所有的输出)。

    就算是一些只有单个输出窗口的系统中,也可以通过把每个频道显示不同颜色来划分输出信息。而且还可以实现过滤器(filter),开关相应的过滤器,能够只显示对应频道的调试输出。

    实现方法是在调试打印函数中加入频道参数来实现该功能,频道可以用数字标识,使用enum表示更好,也可以使用字符串或字符串散列标识符命名频道。如果少于32或64个频道,直接用32位或64位掩码过滤频道。

    把输出同时抄写到日志文件中

    把所有调试信息同时抄写到一个或多个日志文件中,可以方便事后诊断问题。应当不管当前的冗长级别和频道,而把所有调试信息都写入日志文件。

    每次调用调试输出函数后都对日志文件清空缓冲,以确保万一游戏崩溃时日志文件仍会包含最后的输出。通常最后的输出对确定崩溃的原因很关键,但是清空缓冲成本很高。因此,以下情况下才应该清空缓冲:

    • 程序输出的日志量不多;
    • 某平台有这样做的必要。

    崩溃报告

    有些游戏引擎会在崩溃时放出特别的文本或日志。大多数系统都有一个顶层的异常处理函数,它可以捕获大部分的崩溃情形。你可以在此函数中打印各种有用信息。

    崩溃报告可包含的信息:

    • 崩溃时玩家在玩的关卡,玩家角色所在的世界空间位置,玩家角色的动画/动作状态;
    • 崩溃时正在运行的一个或多个游戏脚本;
    • 堆栈跟踪:系统通常提供获取调用堆栈的机制。通过这些机制可以获得崩溃时,堆栈中所有非内联函数的符号。
    • 引擎中所有内存分配器的状态。崩溃和内存相关,这个信息会很有用。
    • 其他和崩溃相关的信息。

    调试用的绘图功能

    大部分游戏引擎会提供一组API,去绘画有颜色的线条、简单图形及三维文本。这些API称为调试绘图。因为这些绘画仅为了在开发及调试期间做可视化,在游戏的发布版中会移除。相对于看代码中的数学公式,直接通过图形看绘图结果能更快的知道逻辑和数学错误。

    调试绘图API

    满足的要求:

    • API应简单且易用;
    • API支持一组有用的图元:直线、球体、点、坐标轴、包围盒、格式化文本等。
    • API应能弹性控制图元如何绘画:颜色、线的宽度、球体半径、点的大小、坐标轴的长度及其他图元的尺寸等。
    • API应可以把图元绘画至世界空间或屏幕空间。
    • API应选择是否使用深度测试来绘画图元:
      • 当开启深度测试,图元会被场景中的真实物体所遮挡。这样能显示图元的前后关系。
      • 当关闭深度测试,图元会“漂浮”在场景中所有真实物体之前。
    • 应该可以在代码的任何地方调用此API。
    • 每个图元应该包含生命期,它控制图元提交后维持在屏幕上的时间。例如,若某个图元每帧都会显示在屏幕上,生命期应该设置为1帧,这样每帧刷新时,它都存在;如果只是间歇存在,则可以以秒为单位设置它的生命期。
    • 调试绘图系统应能高效处理大量调试图元。

    游戏内置菜单

    在游戏运行期间,开发人员能直接配置各个子系统的配置选项,这样会很方便。因为,它不需要重新编辑代码,编译连接。游戏中配置菜单选项,最简单有效的方法是提供游戏内置菜单:

    • 切换全局布尔设定
    • 调校全局整数及浮点数值
    • 调用一些引擎函数,执行任务
    • 开启副菜单,是菜单按阶层式管理,方便浏览

    游戏内置主控台

    有些引擎提供游戏内置主控台,他提供命令式的接口让用户使用引擎功能;相对游戏内置菜单,游戏内置主控台虽然不太方便,但是他提供更丰富的接口,使用户几乎能使用所有引擎功能。

    调用摄像机和游戏暂停

    游戏内置主控台或游戏内置菜单最好附有两个功能:

    • 把摄像机从游戏角色分离出来,控制其观察游戏世界的所有细致场景的细节;
    • 暂停、恢复暂停、单步执行游戏

    暂停游戏时,仍需控制摄像机;可以通过停止逻辑时钟,保持渲染引擎和摄像机控制系统来实现。

    慢动作模式也很有用,可以通过游戏时钟和真实时钟的更新速率不同来实现。

    作弊

    作弊是调试游戏的重要方法。如果为了调试游戏还要死命玩到某关,在调试,效率上太差了。因此,需要作弊。像是不死身、给玩家武器、无尽弹药、选择角色网络等。

    屏幕截图及录像

    获取屏幕截图是有用的工具。通常这些截图会放到某个预设的文件夹中,并以日期来命名保证文件的唯一性。

    有些引擎也提供全面的录像功能。系统是以目标帧率来获取屏幕截图,然后存成视频格式文件。

    获取屏幕截图很慢,因为从显存传送帧缓冲至内存的时间开销(图形硬件通常不会优化此操作)和图像存盘。

    游戏内置性能剖析

    前面提到过需到第三方的剖析工具,但是不一定能在该游戏机上运行,因此,游戏通常会内置性能剖析工具。

    层阶式剖析:层阶式的函数调用;C/C++中根函数一般是main()或WinMain(),但从技术上说真正的根是C标准运行时库中的启动函数。

    两个方面度量函数的耗时:函数的执行时间和函数的调用次数;

    游戏内性能剖析工具通常手动在程序中添加测控,来得到函数的执行时间:

    //一个典型的游戏循环如下:
    while (!quitGame){
        PollJoypad();
        UpdateGameObjects();
        UpdateAllAnimateions();
        PostProcessJoints();
        DetectCollisions();
        RunPhysics();
        GenerateFinalAnimationPoses();
        UpdateCameras();
        RenderScene();
        UpdateAudio();
    }

    如果要剖析上面代码的性能,可能这样插入测控:

    while (!quitGame){
        {
            PROFILE("Poll Joypad");
            PollJoypad();
        }
        {
            PROFILE("Game Objects Update");
            UpdateGameObjects();
        }
        {
            PROFILE("Animateions");
            UpdateAllAnimateions();
        }
        {
            PROFILE("Joint Post-Processing");
            PostProcessJoints();
        }
        {
            PROFILE("Collisions");
            DetectCollisions();
        }
        {
            PROFILE("Physics");
            RunPhysics();
        }
        {
            PROFILE("Animation Finaling");
            GenerateFinalAnimationPoses();
        }
        {
            PROFILE("Cameras");
            UpdateCameras();
        }
        {
            PROFILE("Rendering");
            RenderScene();
        }
        {
            PROFILE("Audio");
            UpdateAudio();
        }
    }

    上面代码的PROFILE()宏会以一个类实现,该类的构造函数负责计时,析构函数停止计时,并以指定的名字记录执行时间。它只会为块作用域内代码计时

    struct AutoProfile
    {
        AutoProfile(const char* name){
            m_name = name;
            m_startTime = QueryPerformanceCounter();
        }
    
        ~AutoProfile(){
            __int64 endtime = QueryPerformanceCounter();
            __int64 elapsedTime = endtime - m_startTime;
            g_profileManager.storeSample(m_name, elapsedTime);
        }
    
        const char *m_name;
        __int64 m_startTime;
    };
    
    #define PROFILE(name) AutoProfile p(name)

    通过加入一些代码去描述剖析采样的层阶关系。

    //此代码声明多个剖析样本箱,指明样本箱的名字,以及父样本箱的名字(若有)
    ProfilerDeclareSampleBin("Rendering", NULL);
        ProfilerDeclareSampleBin("Visibility", "Rendering");
        ProfilerDeclareSampleBin("ShaderSetUp", "Rendering");
            ProfilerDeclareSampleBin("Materials", "ShaderSetUp");
        ProfilerDeclareSampleBin("SubmitGeo", "Rendering");
    ProfilerDeclareSampleBin("Audio", NULL);
    //......

    游戏内置的内存统计和泄漏检测

    很多游戏引擎会实现自定义的内存追踪工具。该工具的难点:

    • 不能控制他人代码的分配行为。游戏中调用的第三方库,好的库会提供内存分配钩子,但有的库没有,这样就没办法控制它的内存分配了。
    • 内存的不同形式。通常有主存和显存,PC中的显存的分配对开发者是隐藏的。
    • 分配器的不同形式。有的引擎会有多个分配器,这样需要追踪到分配器内部的内存分配情况,才能了解到实际的内存情况。

    好的内存追踪工具:

    • 提供准确的信息
    • 把数据以方便及令问题显而易见的方式呈现
    • 提供上下文信息以协助团队追踪问题根源
  • 相关阅读:
    Java集合(二)-Set集合
    Java集合类
    Java构造器和初始化块
    学习OpenStack-Neutron网络服务
    Error response from daemon: Get https://index.docker.io/v1/search?q=tomcat&n=25: net/http: TLS handshake timeout
    学习OpenStack-Nova计算服务
    学习OpenStack-Glance组件部署
    报错:rsync同步报错
    报错:创建nginx镜像时出现报错
    报错:重启Docker报错如何解决
  • 原文地址:https://www.cnblogs.com/yeqluofwupheng/p/7711575.html
Copyright © 2020-2023  润新知