StartGame:Unity TDD 外部工程指南

这一篇是详细介绍如何开始给 Unity 配置 TDD 环境的文章,具体工程原理见:TDD With Unity & LLM Agent-CSDN博客,这里写的是实操指南。

双轨架构

核心原则:

  • 测试源码只维护一份(Assets/Scripts/Tests/),外部工程通过 <Compile Include> 直接引用

  • Unity 内每个程序集对应一个外部 .csproj,依赖关系与 Unity asmdef 一致

  • 外部工程捕获纯逻辑编译/测试错误,Unity 依赖的测试跳过

  • 外部 .NET 工程视为真机(Player) :编译时无 UNITY_EDITOR 系列宏,#if UNITY_EDITOR 分支不参与编译

  • 编译环境与 Unity 内部完全一致:编译器(Unity 内置 Roslyn 4.3.1)、目标框架(netstandard2.1)、语言版本(C# 9.0)、条件编译宏均与 Unity 2022.3.62 相同

编译一致性对齐

外部工程通过 Tests/Directory.Build.props 与 Unity 内部编译器对齐,参数取自 Unity 生成的 Main/Core.csprojEditor/Data/DotNetSdkRoslyn(Unity 2022.3.62f3c1):

参数 Unity 内部 外部工程 说明
编译器 Roslyn 4.3.1(内置) Roslyn 4.3.1(Lib/Roslyn/,离线自带) 通过 CscToolPath/CscToolExe 指向,警告/诊断行为与 Unity 一致
LangVersion 9.0 9.0 限制 C# 语法,防止外部用 Unity 不支持的语法
TargetFramework netstandard2.1 netstandard2.1 类库与 Unity API 面一致(测试工程 net9.0 承载运行)
DefineConstants Player 真机宏集 相同 不含 UNITY_EDITOR 系列 ;可以包含 ODIN_INSPECTORSTEAMWORKS_NETDOTWEENUNITY_INCLUDE_TESTS 等自定宏,保证 #if 分支与真机编译一致
AllowUnsafeBlocks false false 与 asmdef allowUnsafeCode 一致
NoWarn 0169;USG0001 0169;USG0001 抑制与 Unity 相同的警告

真机视角 :外部工程模拟 Player 编译,代码中 #if UNITY_EDITOR 分支(Editor 专属初始化、调试工具)不参与编译。这与 run-unity-tests.ps1(Editor 视角)互为补充------轨道 A 验证真机逻辑编译,轨道 B 验证 Editor 内完整行为。

编译器版本差异实测 :改用 Unity Roslyn 4.3.1 后,LubanUtil.cs:41CS1998(async 方法缺 await)警告被正确暴露,而 .NET SDK Roslyn 5.6 不报告------证明编译器版本对齐是必须的。

注意LangVersionDefineConstants 变更后,代码中 #if ODIN_INSPECTOR 等分支会被激活,必须与 Unity 内验证结果一致。若 Unity 内某分支不编译,外部同样应报错------这是特性不是缺陷。

映射方法

Unity 中每个 *.asmdef 程序集,在 Tests/ 下各有一个对应的 .csproj,依赖关系与 asmdef 的引用一致。

本项目映射表

Unity 程序集 外部 csproj 路径 产出 角色
Core Tests/Core/AsterStartGame.Core.csproj Core.dll 类库,仅编译 Core 源码
Code Tests/Code/AsterStartGame.Code.csproj Code.dll 类库,仅编译 Code 源码,ProjectReference 引用 Core
Tests Tests/Tests/AsterStartGame.Tests.csproj Tests.dll 测试项目,ProjectReference 引用 Core + Code

外部目录结构

复制代码
Tests/
├── Core/AsterStartGame.Core.csproj     ← 类库,对应 Core 程序集
├── Code/AsterStartGame.Code.csproj     ← 类库,对应 Code 程序集
├── Tests/AsterStartGame.Tests.csproj   ← 测试工程,对应 Tests 程序集
├── Lib/UnityEngine/                    ← Unity 引擎 DLL
├── Lib/Plugin/                         ← 包依赖 DLL + Assets/Plugins DLL
├── Directory.Build.props               ← 共享编译属性和 DLL 引用
├── run-tests.ps1
└── .gitignore

离线环境说明

Lib/ 目录包含了脱离 Unity Editor 独立编译所需的全部 DLL:

  • Lib/UnityEngine/ --- 从 Unity Editor Editor/Data/Managed/UnityEngine/ 复制的引擎模块 DLL

  • Lib/Plugin/ --- 从项目 Library/ScriptAssemblies/ 复制的包依赖 DLL + Assets/Plugins/ 的预编译 DLL

  • Lib/Roslyn/ --- 从 Unity Editor Editor/Data/DotNetSdkRoslyn/ 复制的编译器(Roslyn 4.3.1),保证离线环境与 Unity 内部编译器版本一致

在无 Unity Editor 的设备上只需确保 .NET SDK 9.0+ 已安装,执行 ./run-tests.ps1 即可编译测试。

更新 DLL:在 Unity 内执行 Tools/TDD Sync → 同步 Unity 真 DLL(需先创建该工具),或手动重新复制。

DLL 引用

所有外部工程通过 Tests/Directory.Build.props 共享以下 DLL 引用:

Unity 引擎模块

全部引擎模块 DLL 位于 Lib/UnityEngine/(含 UnityEngine.、UnityEditor. 等 80+ 个模块),由 Directory.Build.props 统一引用。

第三方包 DLL(Library/ScriptAssemblies)

Library/ScriptAssemblies/ 复制以下 DLL 到 Lib/Plugin/

类别 DLL
Unity DOTS Unity.Entities, Unity.Entities.Hybrid, Unity.Collections, Unity.Mathematics, Unity.Transforms, Unity.Scenes, Unity.Burst, Unity.Profiling.Core
Unity 包 Unity.TextMeshPro, Unity.InputSystem
第三方 UniTask, YooAsset, Luban.Runtime, Google.Protobuf, com.rlabrecque.steamworks.net
其他 Coffee.UIParticle, DOTween.Modules

Assets/Plugins 预编译 DLL

Assets/Plugins/ 复制到 Lib/Plugin/

DLL 来源
Sirenix.OdinInspector.Attributes Sirenix/Assemblies/
Sirenix.Serialization Sirenix/Assemblies/
Sirenix.Utilities Sirenix/Assemblies/
DOTween Demigiant/DOTween/
DemiLib Demigiant/DemiLib/

搭建步骤(已完成,供参考)

1. 配置 Unity 测试程序集

Assets/Scripts/Tests/Tests.asmdef 引用 Core 和 Code:

这部分建议在 Unity 内手动操作,通过 Window / General / Test Runner ,之后再 Unity 的交互窗口中创建测试用程序集。

复制代码
{
    "name": "Tests",
    "references": ["Core", "Code"],
    "includePlatforms": [],
    "optionalUnityReferences": ["TestAssemblies"]
}

2. 创建外部 csproj

每个非测试程序集对应一个类库 csproj,测试程序集对应测试项目 csproj。三个 csproj 共享 Directory.Build.props 中的编译器属性和 DLL 引用。

3. 运行测试

复制代码
cd Main/Tests
./run-tests.ps1

运行脚本与测试报告

两个脚本运行后都会生成测试报告,遵循相同的报告策略:

  • 历史报告 :每次运行按时间戳命名,存放在 Tests/Log/ 子文件夹,该目录被 git 忽略

  • 基线报告 :每次运行后将最新报告复制到 Tests/ 根目录,覆盖旧基线,随 git 仓库提交

run-tests.ps1(轨道 A:外部 .NET 纯逻辑测试)

复制代码
$root = $PSScriptRoot
$testProj = Join-Path $root "Tests\AsterStartGame.Tests.csproj"
$logDir = Join-Path $root "Log"
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$resultFile = Join-Path $logDir "dotnet-tests-${timestamp}.trx"
$baselineFile = Join-Path $root "latest-dotnet-tests.trx"
​
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
​
dotnet test $testProj --nologo --logger "trx;LogFileName=$resultFile" 2>&1 | Out-Host
$exitCode = $LASTEXITCODE
​
if (Test-Path $resultFile) {
    Copy-Item -Path $resultFile -Destination $baselineFile -Force
    Write-Host "`nReport   : $resultFile" -ForegroundColor Cyan
    Write-Host "Baseline : $baselineFile" -ForegroundColor Cyan
} else {
    Write-Host "[WARN] Result file not found" -ForegroundColor Yellow
}
​
exit $exitCode

产出:

  • 历史报告:Tests/Log/dotnet-tests-<时间戳>.trx

  • 基线报告:Tests/latest-dotnet-tests.trx

run-unity-tests.ps1(轨道 B:Unity Test Framework)

产出:

  • 历史报告:Tests/Log/unity-<平台>-results-<时间戳>.xml

  • 历史日志:Tests/Log/unity-<平台>-log-<时间戳>.txt

  • 基线报告:Tests/latest-unity-<平台>.xml(EditMode / PlayMode)

两个脚本的基线报告文件名不同(latest-dotnet-tests.trxlatest-unity-*.xml),互不覆盖,均提交到 git。

新设备初始配置:Unity 编辑器路径

run-unity-tests.ps1 需要一个 Unity Editor 可执行文件路径。每台新设备首次运行前都必须配置

配置位置(脚本顶部)

打开 Tests/run-unity-tests.ps1,找到文件顶部的 【新设备初始配置区】 ,修改 $UnityEditorPath 为本机 Unity 实际路径:

复制代码
# ============================================================
# 【新设备初始配置区】请在此配置本机的 Unity 编辑器路径
# ============================================================
$UnityEditorPath = "D:\Unity\2022.3.62f3c1\Editor\Unity.exe"

常见路径示例:

安装方式 路径
Unity Hub 默认安装 C:\Program Files\Unity\Hub\Editor\2022.3.62f3c1\Editor\Unity.exe
自定义安装 D:\Unity\2022.3.62f3c1\Editor\Unity.exe(本项目当前用此路径)

三种配置方式(优先级从高到低)

  1. 命令行参数.\run-unity-tests.ps1 -TestPlatform EditMode -UnityPath "C:\...\Unity.exe"

  2. 环境变量$env:UNITY_EDITOR_PATH = "C:\...\Unity.exe" 后再运行脚本

  3. 脚本顶部配置区 :修改 $UnityEditorPath(最常用)

未配置时的提示

若脚本找不到 Unity(配置区留空、参数/环境变量均未提供、自动探测也失败),会打印红色错误提示,引导你填写脚本顶部的配置区:

复制代码
[ERROR] Unity Editor not found.
请在脚本顶部【新设备初始配置区】中配置 Unity 路径:
    E:\Project\AsterStartGame\Main\Tests\run-unity-tests.ps1
或使用参数 / 环境变量指定,例如:
    .\run-unity-tests.ps1 -UnityPath "D:\Unity\2022.3.62f3c1\Editor\Unity.exe"
    $env:UNITY_EDITOR_PATH = "D:\Unity\2022.3.62f3c1\Editor\Unity.exe"

自动探测(无需手动配置时的兜底)

脚本依次尝试以下位置,全部命中才使用,否则报错要求配置:

  1. 命令行 -UnityPath 参数

  2. 环境变量 UNITY_EDITOR_PATH

  3. 脚本顶部 $UnityEditorPath

  4. Unity Hub 默认目录 C:\Program Files\Unity\Hub\Editor\<版本>\Editor\Unity.exe

  5. Windows 注册表 HKLM\SOFTWARE\Unity Technologies\Installer\Unity <版本>

编译一致性验证

核心目标 :确认外部 .NET 工程的编译结果与 Unity 内部真机(Player)编译一致,且两环境编译错误相同。验证通过后,其他设备默认信任外部编译,无需每台重复验证。

验证脚本:run-verify.ps1

复制代码
Tests/run-verify.ps1

执行流程(4 步):

  1. 外部编译dotnet build 外部 Core 工程,统计 error CS

  2. Unity 真机编译 :命令行调 Unity 执行 TddVerify.TddVerifyBuild.CompilePlayerAssembliesBuildPipeline.BuildPlayer + BuildScriptsOnly),统计 Unity 日志中 error CS

  3. 产物对比 :对比 Unity 真机产物(Library/Bee/PlayerScriptAssemblies/Core.dll)与外部产物

    • 业务引用集(排除 Unity 生成代码引入的引用)

    • 业务类型表面(排除 Unity 自动生成的辅助类型)

  4. 结论 :PASS / FAIL,报告写入 Tests/Log/verify-<时间戳>.txt

验证判据

判据 通过条件
编译错误一致 外部 dotnet build 错误数 = Unity 真机编译错误数(正常源码均为 0)
业务引用集一致 两 Core.dll 排除生成代码引入引用后完全一致
业务类型表面一致 两 Core.dll 排除生成辅助类型后完全一致

允许的预期差异(非缺陷)

Unity 内部编译会自动生成两类辅助代码,外部工程不包含(不影响编译错误与业务逻辑):

生成类型 来源 说明
AssemblyTypeRegistry Unity.Entities 源码生成器 Entities 运行时类型注册表
UnitySourceGeneratedAssemblyMonoScriptTypes_v1 Unity 内置 MonoScript 生成器 编辑器 MonoScript 序列化映射

因此 Unity 真机产物会比外部多出上述类型及其引入的程序集引用(Unity.EntitiesUnity.Burst),脚本已自动排除,不会误报 FAIL。

固化标准

  • 验证通过后 :其他设备默认信任外部编译结果,直接使用 run-tests.ps1,无需每台跑 run-verify.ps1

  • 需要重新验证 :Unity 升级、Entities/包版本变化、Directory.Build.props 编译参数变更

  • 排查 :若某次出现"外部编译通过但 Unity 真机报错"或反之,运行 run-verify.ps1 定位差异

注意:Unity 真机编译依赖 Library/Bee/PlayerScriptAssemblies/ 缓存。首次运行或清理缓存后,脚本会自动触发 Unity 编译。若 BuildPlayer 因 DOTS 场景烘焙失败(退出码 2),脚本仍会复制已编译产物并继续验证------因为场景烘焙失败不代表脚本编译失败。

.gitignore

复制代码
**/bin/
**/obj/
/Log/
*.user
*.suo

常见问题

外部编译报 CS0246/CS0436

报错 修法
CS0246: type 'X' not found 缺 DLL 引用 → 从 Unity Editor 或 Library/ScriptAssemblies 同步
CS0436: 'X' conflicts with imported type DLL 重复 → 移除冗余 Reference
CS0012: type 'X' in un-referenced assembly 类型定义在未引用的 DLL(如 Sirenix.Serialization 的 SerializedScriptableObject)→ 在 Directory.Build.props 添加该 DLL 引用

外部编译通过但 Unity 内运行报错

外部工程只做静态编译检查,如果涉及 UnityEngine 的代码,大概率是无法正常运行的。因此,运行时行为(MonoBehaviour 生命周期、GameObject 实例化等)必须在 Unity Test Runner(轨道 B)中验证。

缺少 DLL 引用

在 Unity 内使用编辑器辅助工具 Tools/TDD Sync → 同步 Unity 真 DLL 一键同步。手动方式:从 Unity Editor 安装目录 Editor/Data/Managed/UnityEngine/ 复制引擎 DLL,从项目 Library/ScriptAssemblies/ 复制包依赖 DLL。

关于这部分,我写了一个示例框架工程,大家可以参考使用;

https://gitee.com/AsterForUnity/aster-start-game.githttps://gitee.com/AsterForUnity/aster-start-game.git

相关推荐
小弥儿1 小时前
GitHub今日热榜 | 2026-07-31:AI Agent工作流共享赛道升温
人工智能·学习·github
小蒋学算法10 小时前
算法-M个非重叠子数组最大和II-WQS二分学习
数据结构·学习·算法
我要见SA姐111 小时前
AI提示词遇见精密算法:TimeGuessr如何用数学魔法打造文化游戏新体验
人工智能·算法·游戏
湿滑路面12 小时前
Rouyan:使用WPF/C#构建的基于LLM的快捷翻译小工具
开发语言·c#·wpf
重庆小透明13 小时前
带你从不同视角了解三大MQ
java·学习·spring·kafka·rabbitmq·rocketmq
冰心孤城14 小时前
C++ 与 C#混合编程 示例 (基于VS)
java·c++·c#
~光~~14 小时前
【xv6学习】L0_环境配置
单片机·嵌入式硬件·学习
软萌萌的114 小时前
C#中的多级缓存架构设计与实现深度解析
缓存·c#·wpf
实验如有神祝女士15 小时前
不同参数规模大模型在医学翻译场景的适配差异
论文阅读·人工智能·深度学习·学习·算法·语言模型·论文笔记