Files
tracy-for-unity/UNITY_INTEGRATION_GUIDE.md
2025-11-26 14:35:58 +08:00

19 KiB
Raw Blame History

Tracy 集成到 Unity 的完整指南

概述

Tracy 是一个强大的 C++ 性能分析工具,要在 Unity 中使用它,需要通过 Native Plugin 的方式集成。本指南将介绍两种主要的集成方法。


方法一:通过 Native Plugin 集成(推荐)

这是最常用和最灵活的方法适用于所有Unity项目。

1. 创建 Tracy Native Plugin

步骤 1创建 C++ 动态库项目

创建一个新的 C++ DLL/Shared Library 项目:

目录结构:

UnityTracyPlugin/
├── src/
│   ├── TracyUnityPlugin.cpp
│   └── TracyUnityPlugin.h
├── include/
│   └── tracy/              # Tracy 源代码
├── CMakeLists.txt
└── build/

步骤 2编写 Plugin 代码

TracyUnityPlugin.h:

#ifndef TRACY_UNITY_PLUGIN_H
#define TRACY_UNITY_PLUGIN_H

// Unity Native Plugin 需要使用 C 导出
#ifdef __cplusplus
extern "C" {
#endif

// 平台定义
#if defined(_WIN32) || defined(_WIN64)
    #define UNITY_PLUGIN_EXPORT __declspec(dllexport)
#elif defined(__APPLE__) || defined(__linux__)
    #define UNITY_PLUGIN_EXPORT __attribute__((visibility("default")))
#else
    #define UNITY_PLUGIN_EXPORT
#endif

// Tracy API
UNITY_PLUGIN_EXPORT void TracyInit();
UNITY_PLUGIN_EXPORT void TracyShutdown();
UNITY_PLUGIN_EXPORT void TracyFrameMark();
UNITY_PLUGIN_EXPORT void TracyBeginZone(const char* name, const char* function, const char* file, int line);
UNITY_PLUGIN_EXPORT void TracyEndZone();
UNITY_PLUGIN_EXPORT void TracyPlotValue(const char* name, double value);
UNITY_PLUGIN_EXPORT void TracyMessage(const char* message);
UNITY_PLUGIN_EXPORT void TracySetThreadName(const char* name);

#ifdef __cplusplus
}
#endif

#endif // TRACY_UNITY_PLUGIN_H

TracyUnityPlugin.cpp:

#include "TracyUnityPlugin.h"
#include "tracy/Tracy.hpp"
#include <string>
#include <unordered_map>
#include <mutex>

// 用于管理 Zone 的结构
struct ZoneContext {
    tracy::ScopedZone* zone;
};

static std::unordered_map<int, ZoneContext> g_zones;
static std::mutex g_zonesMutex;
static int g_nextZoneId = 1;

extern "C" {

UNITY_PLUGIN_EXPORT void TracyInit() {
    // Tracy 会自动初始化,这里可以做一些额外的设置
}

UNITY_PLUGIN_EXPORT void TracyShutdown() {
    std::lock_guard<std::mutex> lock(g_zonesMutex);
    g_zones.clear();
}

UNITY_PLUGIN_EXPORT void TracyFrameMark() {
    FrameMark;
}

UNITY_PLUGIN_EXPORT int TracyBeginZone(const char* name, const char* function, const char* file, int line) {
    std::lock_guard<std::mutex> lock(g_zonesMutex);
    
    int zoneId = g_nextZoneId++;
    
    // 创建 Zone注意这是简化版本实际使用中需要更复杂的处理
    tracy::SourceLocationData loc;
    loc.name = name;
    loc.function = function;
    loc.file = file;
    loc.line = (uint32_t)line;
    
    // 实际使用中,你可能需要使用 Tracy 的宏来正确创建 Zone
    // 这里仅作示例
    
    return zoneId;
}

UNITY_PLUGIN_EXPORT void TracyEndZone(int zoneId) {
    std::lock_guard<std::mutex> lock(g_zonesMutex);
    
    auto it = g_zones.find(zoneId);
    if (it != g_zones.end()) {
        delete it->second.zone;
        g_zones.erase(it);
    }
}

UNITY_PLUGIN_EXPORT void TracyPlotValue(const char* name, double value) {
    TracyPlot(name, value);
}

UNITY_PLUGIN_EXPORT void TracyMessage(const char* message) {
    TracyMessage(message, strlen(message));
}

UNITY_PLUGIN_EXPORT void TracySetThreadName(const char* name) {
    tracy::SetThreadName(name);
}

} // extern "C"

步骤 3CMakeLists.txt 配置

cmake_minimum_required(VERSION 3.10)
project(UnityTracyPlugin)

set(CMAKE_CXX_STANDARD 17)

# Tracy 配置
option(TRACY_ENABLE "Enable Tracy profiling" ON)
option(TRACY_ON_DEMAND "Tracy on-demand profiling" ON)

add_definitions(-DTRACY_ENABLE)
add_definitions(-DTRACY_ON_DEMAND)

# Tracy 源文件
set(TRACY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/tracy")
set(TRACY_SOURCES
    ${TRACY_DIR}/public/TracyClient.cpp
)

# Plugin 源文件
set(PLUGIN_SOURCES
    src/TracyUnityPlugin.cpp
    ${TRACY_SOURCES}
)

# 包含目录
include_directories(
    ${CMAKE_CURRENT_SOURCE_DIR}/src
    ${TRACY_DIR}/public
)

# 创建动态库
add_library(UnityTracyPlugin SHARED ${PLUGIN_SOURCES})

# Windows 平台设置
if(WIN32)
    target_link_libraries(UnityTracyPlugin ws2_32 dbghelp)
endif()

# macOS/iOS 平台设置
if(APPLE)
    set_target_properties(UnityTracyPlugin PROPERTIES
        FRAMEWORK FALSE
        MACOSX_RPATH TRUE
    )
endif()

# Linux/Android 平台设置
if(UNIX AND NOT APPLE)
    target_link_libraries(UnityTracyPlugin pthread dl)
endif()

步骤 4编译 Plugin

# Windows
mkdir build
cd build
cmake .. -G "Visual Studio 17 2022" -A x64
cmake --build . --config Release

# macOS
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make

# Linux
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make

2. Unity 端集成

步骤 1将编译好的 DLL 放入 Unity 项目

UnityProject/
└── Assets/
    └── Plugins/
        ├── x86_64/
        │   └── UnityTracyPlugin.dll        # Windows 64位
        ├── x86/
        │   └── UnityTracyPlugin.dll        # Windows 32位
        ├── Android/
        │   ├── arm64-v8a/
        │   │   └── libUnityTracyPlugin.so  # Android ARM64
        │   └── armeabi-v7a/
        │       └── libUnityTracyPlugin.so  # Android ARM32
        ├── iOS/
        │   └── libUnityTracyPlugin.a       # iOS 静态库
        └── macOS/
            └── libUnityTracyPlugin.dylib   # macOS

步骤 2创建 C# Wrapper

Tracy.cs:

using System;
using System.Runtime.InteropServices;
using UnityEngine;

namespace TracyProfiler
{
    /// <summary>
    /// Tracy 性能分析器的 Unity 封装
    /// </summary>
    public static class Tracy
    {
        private const string DLL_NAME = "UnityTracyPlugin";

        #region Native Methods

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        private static extern void TracyInit();

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        private static extern void TracyShutdown();

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        private static extern void TracyFrameMark();

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        private static extern int TracyBeginZone(string name, string function, string file, int line);

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        private static extern void TracyEndZone(int zoneId);

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        private static extern void TracyPlotValue(string name, double value);

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        private static extern void TracyMessage(string message);

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        private static extern void TracySetThreadName(string name);

        #endregion

        private static bool s_initialized = false;

        /// <summary>
        /// 初始化 Tracy
        /// </summary>
        public static void Initialize()
        {
            if (s_initialized) return;

            try
            {
                TracyInit();
                s_initialized = true;
                Debug.Log("Tracy Profiler 已初始化");
            }
            catch (Exception e)
            {
                Debug.LogError($"Tracy 初始化失败: {e.Message}");
            }
        }

        /// <summary>
        /// 关闭 Tracy
        /// </summary>
        public static void Shutdown()
        {
            if (!s_initialized) return;

            try
            {
                TracyShutdown();
                s_initialized = false;
            }
            catch (Exception e)
            {
                Debug.LogError($"Tracy 关闭失败: {e.Message}");
            }
        }

        /// <summary>
        /// 标记帧边界(通常在每帧末尾调用)
        /// </summary>
        public static void MarkFrame()
        {
            if (!s_initialized) return;
            TracyFrameMark();
        }

        /// <summary>
        /// 绘制数值(用于实时监控变量)
        /// </summary>
        public static void Plot(string name, double value)
        {
            if (!s_initialized) return;
            TracyPlotValue(name, value);
        }

        /// <summary>
        /// 发送消息到 Tracy
        /// </summary>
        public static void Message(string message)
        {
            if (!s_initialized) return;
            TracyMessage(message);
        }

        /// <summary>
        /// 设置当前线程名称
        /// </summary>
        public static void SetThreadName(string name)
        {
            if (!s_initialized) return;
            TracySetThreadName(name);
        }

        /// <summary>
        /// Tracy Zone 的作用域包装器(使用 using 语句自动管理生命周期)
        /// </summary>
        public struct ZoneScope : IDisposable
        {
            private int zoneId;
            private bool isValid;

            public ZoneScope(string name, 
                           [System.Runtime.CompilerServices.CallerMemberName] string function = "",
                           [System.Runtime.CompilerServices.CallerFilePath] string file = "",
                           [System.Runtime.CompilerServices.CallerLineNumber] int line = 0)
            {
                if (s_initialized)
                {
                    zoneId = TracyBeginZone(name, function, file, line);
                    isValid = true;
                }
                else
                {
                    zoneId = -1;
                    isValid = false;
                }
            }

            public void Dispose()
            {
                if (isValid && s_initialized)
                {
                    TracyEndZone(zoneId);
                }
            }
        }

        /// <summary>
        /// 创建一个 Tracy Zone性能追踪区域
        /// 使用 using 语句确保自动结束
        /// </summary>
        public static ZoneScope BeginZone(string name)
        {
            return new ZoneScope(name);
        }
    }
}

步骤 3创建 Tracy Manager

TracyManager.cs:

using UnityEngine;

namespace TracyProfiler
{
    /// <summary>
    /// Tracy 管理器 - 负责初始化和每帧更新
    /// </summary>
    public class TracyManager : MonoBehaviour
    {
        [Header("Tracy Settings")]
        [SerializeField] private bool enableOnStart = true;
        [SerializeField] private bool markFrames = true;

        private void Awake()
        {
            // 确保只有一个实例
            if (FindObjectsOfType<TracyManager>().Length > 1)
            {
                Destroy(gameObject);
                return;
            }

            DontDestroyOnLoad(gameObject);

            if (enableOnStart)
            {
                Tracy.Initialize();
            }
        }

        private void LateUpdate()
        {
            // 在每帧末尾标记帧边界
            if (markFrames)
            {
                Tracy.MarkFrame();
            }

            // 示例:绘制一些有用的性能数据
            Tracy.Plot("FPS", 1.0f / Time.deltaTime);
            Tracy.Plot("Frame Time (ms)", Time.deltaTime * 1000.0f);
            Tracy.Plot("Total Allocated Memory (MB)", GC.GetTotalMemory(false) / (1024.0 * 1024.0));
        }

        private void OnDestroy()
        {
            Tracy.Shutdown();
        }

        private void OnApplicationQuit()
        {
            Tracy.Shutdown();
        }
    }
}

步骤 4使用示例

GameManager.cs:

using UnityEngine;
using TracyProfiler;

public class GameManager : MonoBehaviour
{
    private void Update()
    {
        // 追踪整个 Update 方法
        using (Tracy.BeginZone("GameManager.Update"))
        {
            ProcessInput();
            UpdateGameLogic();
            UpdateAI();
        }
    }

    private void ProcessInput()
    {
        using (Tracy.BeginZone("ProcessInput"))
        {
            // 输入处理代码
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Tracy.Message("玩家按下空格键");
            }
        }
    }

    private void UpdateGameLogic()
    {
        using (Tracy.BeginZone("UpdateGameLogic"))
        {
            // 游戏逻辑代码
            for (int i = 0; i < 1000; i++)
            {
                // 一些计算
            }
        }
    }

    private void UpdateAI()
    {
        using (Tracy.BeginZone("UpdateAI"))
        {
            // AI 更新代码
            Tracy.Plot("Enemy Count", GameObject.FindGameObjectsWithTag("Enemy").Length);
        }
    }
}

PhysicsController.cs:

using UnityEngine;
using TracyProfiler;

public class PhysicsController : MonoBehaviour
{
    private void FixedUpdate()
    {
        using (Tracy.BeginZone("PhysicsController.FixedUpdate"))
        {
            PerformPhysicsCalculations();
        }
    }

    private void PerformPhysicsCalculations()
    {
        using (Tracy.BeginZone("Physics Calculations"))
        {
            // 物理计算
            Rigidbody[] rigidbodies = FindObjectsOfType<Rigidbody>();
            Tracy.Plot("Active Rigidbodies", rigidbodies.Length);
            
            foreach (var rb in rigidbodies)
            {
                // 处理刚体
            }
        }
    }
}

方法二IL2CPP 后端集成

当 Unity 使用 IL2CPP 作为脚本后端时,可以直接在生成的 C++ 代码中集成 Tracy。

1. 配置 IL2CPP

在 Unity 项目中创建 il2cpp_custom.cpp

// 放置在: Assets/Plugins/IL2CPP/il2cpp_custom.cpp

#include "il2cpp-config.h"

// 包含 Tracy
#define TRACY_ENABLE
#include "tracy/Tracy.hpp"

// IL2CPP 钩子函数
extern "C" void UnityPluginLoad(void* unityInterfaces)
{
    // Tracy 会自动初始化
}

extern "C" void UnityPluginUnload()
{
    // Tracy 清理
}

2. 修改 IL2CPP 构建设置

在 Unity 的 Player Settings 中:

  1. 切换到 IL2CPP 脚本后端
  2. 添加额外的编译参数:-DTRACY_ENABLE -DTRACY_ON_DEMAND
  3. 链接 Tracy 库

方法三Unity Profiler 集成(可选)

可以创建一个桥接层,将 Unity Profiler 的数据转发到 Tracy。

UnityToTracyBridge.cs:

using UnityEngine;
using UnityEngine.Profiling;
using TracyProfiler;

public class UnityToTracyBridge : MonoBehaviour
{
    private CustomSampler[] samplers;

    private void Start()
    {
        // 创建自定义采样器
        samplers = new CustomSampler[]
        {
            CustomSampler.Create("Render"),
            CustomSampler.Create("Physics"),
            CustomSampler.Create("Scripts"),
            CustomSampler.Create("GarbageCollector"),
        };
    }

    private void LateUpdate()
    {
        // 将 Unity Profiler 数据转发到 Tracy
        foreach (var sampler in samplers)
        {
            Tracy.Plot(sampler.name + " (ms)", sampler.GetRecorder().elapsedNanoseconds / 1000000.0);
        }

        // 内存统计
        Tracy.Plot("Used Heap Size (MB)", Profiler.usedHeapSizeLong / (1024.0 * 1024.0));
        Tracy.Plot("Total Allocated Memory (MB)", Profiler.GetTotalAllocatedMemoryLong() / (1024.0 * 1024.0));
        Tracy.Plot("Total Reserved Memory (MB)", Profiler.GetTotalReservedMemoryLong() / (1024.0 * 1024.0));
    }
}

平台特定配置

Windows

  • 使用 Visual Studio 编译 DLL
  • 确保链接 ws2_32.libdbghelp.lib

macOS / iOS

  • 使用 Xcode 编译动态库或框架
  • 注意代码签名要求

Android

  • 为不同的 ABI 编译armeabi-v7a, arm64-v8a, x86, x86_64
  • gradle.properties 中配置 NDK 路径

Linux

  • 链接 pthreaddl
  • 确保 GLIBC 版本兼容

使用 Tracy Profiler 查看数据

1. 启动 Tracy Profiler

# 从 Tracy 仓库构建或下载预编译版本
# 运行 Tracy.exe (Windows) 或 Tracy (macOS/Linux)

2. 连接到 Unity 应用

  • Tracy Profiler 会自动发现本地网络中的 Tracy 客户端
  • 点击连接即可开始分析

3. 查看性能数据

  • 时间线视图: 查看各个 Zone 的执行时间
  • 统计视图: 查看函数调用次数和平均耗时
  • 内存视图: 查看内存分配情况
  • 图表视图: 查看 Plot 数据的实时曲线

最佳实践

1. 性能开销

  • Tracy 的开销很小,但仍建议在 Release 构建中禁用细粒度的 Zone
  • 使用条件编译:#if TRACY_ENABLE

2. Zone 命名

  • 使用清晰、描述性的名称
  • 包含类名和方法名,如 "PlayerController.Move"

3. 合理使用 Plot

  • 不要在每帧绘制过多数据点
  • 专注于关键性能指标

4. 多线程支持

  • Unity 的 Job System 需要特殊处理
  • 在每个 Job 中调用 Tracy.SetThreadName()

5. 移动平台

  • 注意无线网络连接的延迟
  • 可以使用 Tracy 的文件保存功能,之后再分析

故障排除

问题 1: DLL 加载失败

解决方案:

  • 检查 Plugin 导入设置中的平台配置
  • 确保 DLL 与 Unity 架构匹配x64/x86
  • 检查依赖库是否缺失

问题 2: Tracy Profiler 无法连接

解决方案:

  • 确保防火墙允许 TCP 端口 8086
  • 检查 TRACY_ON_DEMAND 是否正确定义
  • 确认 Tracy.Initialize() 已被调用

问题 3: 性能数据不准确

解决方案:

  • 使用 Release 构建测试
  • 禁用 Unity Editor 的 Deep Profiling
  • 确保 V-Sync 和帧率限制的设置符合预期

问题 4: Android 构建失败

解决方案:

  • 检查 NDK 版本兼容性
  • build.gradle 中添加必要的链接器标志
  • 确保所有 ABI 的库都已编译

进阶功能

1. 自定义内存分配追踪

// 在 Plugin 中添加
UNITY_PLUGIN_EXPORT void TracyAllocNamed(void* ptr, size_t size, const char* name) {
    TracyAllocN(ptr, size, name);
}

UNITY_PLUGIN_EXPORT void TracyFreeNamed(void* ptr, const char* name) {
    TracyFreeN(ptr, name);
}
// 在 C# 中使用
public class TrackedMemoryPool
{
    public void Allocate(int size)
    {
        IntPtr ptr = Marshal.AllocHGlobal(size);
        Tracy.AllocNamed(ptr, size, "Memory Pool");
    }

    public void Free(IntPtr ptr)
    {
        Tracy.FreeNamed(ptr, "Memory Pool");
        Marshal.FreeHGlobal(ptr);
    }
}

2. GPU 性能追踪

Tracy 支持 OpenGL、Vulkan、DirectX 的 GPU 追踪,但需要在渲染管线中集成。

3. 锁竞争分析

// 在 Plugin 中
#define LockableName(type, varname) TracyLockableN(type, varname, #varname)

总结

将 Tracy 集成到 Unity 中可以提供比 Unity Profiler 更详细的性能分析数据,特别是对于:

  1. Native Code 性能 - 如果你使用了大量 Native Plugin
  2. 多线程分析 - Tracy 的线程视图非常直观
  3. 跨平台分析 - 统一的工具链
  4. 帧级别分析 - 精确到微秒的时间线

根据你的项目需求,选择合适的集成方法。对于大多数项目,方法一Native Plugin 是最推荐的方式。


参考资源