Unity实验框架UXF入门:从零构建行为实验与数据记录系统
1. 项目概述为什么你需要一个实验框架如果你正在用Unity做科研、心理学实验、人机交互研究或者任何需要严格控制流程、记录精确数据、重复运行多次试次的项目那你大概率经历过这样的痛苦每次新建一个实验都得从头写一堆代码来管理试次顺序、记录被试反应、保存数据到文件还得确保时间戳精确到毫秒。更头疼的是一旦实验流程需要调整或者要加入新的自变量代码结构就可能变得一团糟调试起来让人抓狂。Unity Experiment Framework简称UXF就是为了解决这些问题而生的。它不是一个游戏框架而是一个专门为在Unity引擎内创建严谨的行为实验而设计的开源工具包。你可以把它理解为你实验的“脚手架”和“自动化管家”。它帮你把实验中最通用、最繁琐的部分——比如试次循环、数据记录、UI流程控制——都封装好了你只需要专注于设计你的实验刺激和任务逻辑本身。我最初接触UXF是在做一个视觉搜索的认知实验当时手动管理数据和试次让我浪费了大量时间在调试和数据处理上。用了UXF之后整个项目的结构清晰了十倍数据输出格式统一复现实验也变得轻而易举。无论是简单的反应时任务还是复杂的多阶段认知实验UXF都能提供坚实的底层支持。接下来我就带你从零开始把这个强大的工具集成到你的项目中并分享一些实战中积累的经验。2. 环境准备与项目初始化在开始安装UXF之前我们需要确保有一个合适的工作环境。虽然UXF对Unity版本的要求相对宽松但为了最佳的兼容性和稳定性选择正确的起点能避免很多后续麻烦。2.1 Unity版本与项目设置UXF官方推荐使用Unity 2019.4 LTS或更高版本。LTS长期支持版本以稳定性著称非常适合科研这种对可复现性要求极高的场景。我个人目前使用的是Unity 2021.3 LTS与UXF 2.3.0版本配合良好。创建一个新的Unity项目时我建议使用3D核心模板。即使你的实验是2D的比如呈现图片或文字3D核心模板也完全适用而且不会引入不必要的2D专用包让项目结构更干净。将项目命名为一个有意义的名称例如“MyVisualSearchExperiment”。创建好项目后第一件事是调整项目的色彩空间Color Space。在Edit - Project Settings - Player中找到Other Settings下的Rendering部分将Color Space从默认的Gamma切换到Linear。线性色彩空间能提供更精确的颜色混合和光照计算这对于需要严格控制视觉刺激亮度和对比度的心理学或神经科学实验至关重要。很多早期教程可能忽略了这一点但在涉及颜色感知的实验里这个设置是基础。2.2 安装UXF三种方法详解UXF的安装主要有三种途径Unity Asset Store、Unity Package Manager (UPM) 和直接导入.unitypackage文件。每种方法各有优劣我会详细拆解。方法一通过Unity Asset Store安装最推荐给新手这是最直观、最不容易出错的方法。在Unity编辑器顶部菜单栏点击Window - Asset Store打开资源商店窗口。在搜索框中输入 “Unity Experiment Framework” 或 “UXF”。找到由“Jack Brookes”发布的官方资源包点击进入详情页。点击“添加到我的资源”然后“打开Unity”最后在Package Manager中点击“下载”和“导入”。Asset Store版本通常经过Unity官方审核依赖关系处理得比较好适合绝大多数用户。方法二通过Git URL使用Package Manager安装适合喜欢最新版或自定义分支的开发者如果你想使用GitHub上的最新开发版本或者未来方便更新可以使用这种方法。在Unity编辑器中打开Window - Package Manager。点击左上角的“”号选择“Add package from git URL...”。在弹出的输入框中粘贴UXF的Git仓库地址https://github.com/immersivecognition/unity-experiment-framework.git。点击“Add”。Unity会自动从GitHub克隆仓库并作为包导入。注意这种方法要求你的网络环境能够稳定访问GitHub。如果克隆失败可以尝试使用https://gitee.com/你的镜像/unity-experiment-framework.git这样的国内镜像地址如果有的话但需注意镜像的同步可能滞后。方法三手动下载并导入.unitypackage传统方法你可以直接从UXF的GitHub发布页面Releases下载最新的.unitypackage文件。访问https://github.com/immersivecognition/unity-experiment-framework/releases。下载最新版本的.unitypackage文件例如UXF_2.x.x.unitypackage。回到Unity选择Assets - Import Package - Custom Package...。找到并选中你下载的.unitypackage文件点击“打开”。在导入对话框中通常全选所有文件然后点击“Import”。安装后的验证无论采用哪种方法安装成功后你都会在Project窗口的Assets文件夹下看到一个名为“UXF”的文件夹。同时Unity的菜单栏会多出一个“UXF”选项。点击它如果能看到“Show Settings”、“Create Example Experiment”等子菜单就说明安装成功了。3. 核心概念与工作流解析安装好UXF只是第一步理解它的核心概念和工作流才能高效地利用它。UXF的设计哲学是“约定大于配置”它定义了一套清晰的实验结构我们只需要按照这个结构填充内容。3.1 理解UXF的核心组件Session, Trial, BlockUXF将实验抽象为三个层次这和我们设计实验时的思维逻辑是完全一致的Session会话代表一次完整的实验运行。通常对应一位被试完成整个实验的过程。它包含了实验的所有全局设置、最终的数据文件并管理着Trial和Block的生命周期。Block区组是Trial的集合。你可以把Block理解为实验的一个阶段或条件。例如在一个实验中你可能先有一个“练习Block”包含10个试次然后是“正式实验BlockA”和“正式实验BlockB”各包含50个试次。Block可以拥有独立的设置比如不同的指导语或参数。Trial试次实验的最小可重复单元。一次刺激呈现、一次被试反应、一次数据记录通常就发生在一个Trial内。例如在反应时任务中一次“屏幕中央出现红色圆点 - 被试按键 - 记录反应时”的过程就是一个Trial。这种层级关系是一个Session包含多个Block一个Block包含多个Trial。UXF会自动按照这个顺序来执行你的实验。3.2 数据记录系统自动化的强大后盾UXF最强大的功能之一是其自动化的数据记录系统。你几乎不需要手动写文件I/O代码。数据存储位置UXF默认会在系统特定路径如Windows的AppData/LocalLow/公司名/产品名下创建一个结构清晰的文件夹来存放数据。你可以在UXF - Show Settings中修改默认的存储路径。我通常会在实验开始前在代码中根据实验名称和被试ID动态创建一个更易查找的路径。记录什么UXF会自动为每个Session和每个Trial记录大量元数据例如开始结束时间、状态、在Block中的编号等。更重要的是你可以通过几行代码轻松记录任何自定义数据。数据格式UXF默认将数据保存为.csv逗号分隔值文件。这是一种通用格式可以直接用Excel、Python(pandas)、R或MATLAB打开和分析极大简化了后续的数据处理流程。每个Session会生成一个主文件每个Trial的数据也可能有独立的详细记录。3.3 典型UXF实验工作流一个标准的UXF实验流程在代码层面的典型交互如下实验开始你调用UXF.Session.Create()来创建一个新的Session。此时可以传入被试ID、实验条件等设置。创建Block通过session.CreateBlock()来定义实验的区组。你可以设置这个Block包含多少Trial以及Block级别的参数。运行TrialUXF会自动按照你定义的顺序开始运行每个Block中的Trial。在每个Trial开始(OnTrialBegin)和结束(OnTrialEnd)时UXF会触发相应的事件。注入实验逻辑你的核心任务就是编写代码来响应这些事件。例如在OnTrialBegin事件中生成并呈现本次Trial的刺激比如在屏幕上显示一个单词。在Trial进行中监听被试的输入键盘、鼠标、反应盒等。在获取反应后调用currentTrial.result记录结果如反应时、正误并调用currentTrial.End()来结束当前Trial。在OnTrialEnd事件中可以清理场景或者记录一些本次Trial的附加数据。实验结束当所有Block的所有Trial都完成后Session会自动结束所有数据会被写入文件。你也可以手动调用session.End()来提前结束。4. 从零构建你的第一个实验反应时任务理论说再多不如动手做一遍。让我们创建一个最简单的实验屏幕上随机呈现红色或绿色的圆点被试看到绿色圆点按“J”键看到红色圆点按“F”键记录每次的反应时和正误。4.1 场景与UXF管理器设置创建基础场景在Unity中创建一个新场景保存为“SimpleRT_Task”。添加UXF核心对象在Hierarchy窗口右键选择UXF - Session。这会自动创建一个名为“Session”的GameObject上面挂载了Session组件。它是整个实验的总控制器。创建刺激物在场景中创建一个UI - Image命名为“Stimulus”。将其锚点设置为居中调整大小例如100x100作为我们呈现红绿圆点的载体。暂时将其Image组件的颜色设为白色并禁用Deactivate这个GameObject因为我们不希望实验一开始就显示它。4.2 编写实验控制脚本创建一个新的C#脚本命名为SimpleRTExperiment并将其挂载到场景中一个空物体或Session物体上。using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; // 引用UI命名空间 using UXF; // 核心命名空间 public class SimpleRTExperiment : MonoBehaviour { public Session session; // 在Inspector中拖拽赋值 public Image stimulusImage; // 在Inspector中拖拽赋值你的Stimulus Image // 定义可能的颜色 private Color[] colors { Color.green, Color.red }; // 定义对应的正确按键 private KeyCode[] correctKeys { KeyCode.J, KeyCode.F }; private Trial currentTrial; private int currentColorIndex; private float stimulusStartTime; void Start() { // 将方法注册到UXF的事件中 session.onTrialBegin.AddListener(OnTrialBegin); session.onTrialEnd.AddListener(OnTrialEnd); } void Update() { // 如果当前有Trial正在运行且刺激已呈现则检测按键 if (currentTrial ! null currentTrial.InProgress) { // 检查是否为正确的按键 if (Input.GetKeyDown(correctKeys[currentColorIndex])) { float reactionTime Time.time - stimulusStartTime; RecordTrialResult(true, reactionTime); // 正确 } // 检查是否为另一个错误的按键 else if (Input.GetKeyDown(GetOppositeKey(currentColorIndex))) { float reactionTime Time.time - stimulusStartTime; RecordTrialResult(false, reactionTime); // 错误 } } } // 获取相反颜色的按键 private KeyCode GetOppositeKey(int index) { return index 0 ? KeyCode.F : KeyCode.J; } void OnTrialBegin(Trial trial) { currentTrial trial; // 1. 随机选择本次Trial的颜色 currentColorIndex Random.Range(0, colors.Length); // 2. 呈现刺激 stimulusImage.color colors[currentColorIndex]; stimulusImage.gameObject.SetActive(true); stimulusStartTime Time.time; // 记录刺激呈现的时刻 // 3. (可选) 将本次Trial的参数记录到UXF中方便后续分析 trial.settings.SetValue(stimulus_color, currentColorIndex 0 ? green : red); trial.settings.SetValue(correct_key, correctKeys[currentColorIndex].ToString()); } void RecordTrialResult(bool isCorrect, float rt) { // 1. 隐藏刺激 stimulusImage.gameObject.SetActive(false); // 2. 将结果保存到当前Trial中 currentTrial.result[correct] isCorrect; currentTrial.result[reaction_time] rt; // UXF会自动将C#类型转换为可存储的格式 // 3. 结束当前TrialUXF会自动进入下一个Trial或结束Block currentTrial.End(); } void OnTrialEnd(Trial trial) { // 确保刺激被隐藏 stimulusImage.gameObject.SetActive(false); currentTrial null; } // 提供一个按钮调用的方法用于在UI上开始实验 public void StartExperiment() { // 创建一个包含20个Trial的Block Block block session.CreateBlock(20); // 开始运行这个Block session.BeginNextTrial(); } }4.3 配置与运行在Unity编辑器中将Session物体拖拽到SimpleRTExperiment脚本的session字段。将Hierarchy中的StimulusImage物体拖拽到stimulusImage字段。创建一个UI Button将其文本改为“开始实验”在它的On Click()事件中拖拽挂载了SimpleRTExperiment脚本的物体然后选择方法SimpleRTExperiment - StartExperiment。运行游戏。点击“开始实验”按钮你应该会看到红绿圆点随机出现按下正确的键绿-J 红-F后刺激消失短暂间隔后下一个刺激出现。至此一个具备完整数据记录功能的反应时实验就完成了。所有试次的正确率、反应时都会自动保存到文件中。5. 高级功能与实战技巧掌握了基础之后我们可以利用UXF更强大的功能来构建复杂的实验。5.1 使用Settings系统管理实验参数硬编码参数比如上面的颜色数组不利于实验的灵活调整。UXF提供了强大的Settings系统允许你在不修改代码的情况下通过.json文件或Inspector界面来配置实验参数。在代码中使用Settingsvoid OnTrialBegin(Trial trial) { // 从Trial的settings中读取参数如果没有则使用默认值 float stimulusSize trial.settings.GetFloat(stimulus_size, 100f); float displayTime trial.settings.GetFloat(display_time_ms, 500f) / 1000f; // 转为秒 // 应用参数... }通过JSON文件提供Settings你可以创建一个JSON文件来定义整个Block或每个Trial的参数。UXF可以读取这个文件并自动应用到对应的Trial上。这对于实现完全随机的、条件平衡的实验设计至关重要。例如一个trial_settings.json文件可能包含一个字典列表每个字典代表一个Trial的设置。5.2 实现复杂的试次序列与条件平衡对于需要平衡多种条件如刺激类型、位置、SOA等的实验手动管理序列几乎是不可能的。UXF可以与外部工具如Python的random或psychopy库或算法结合生成一个预定义的Trial列表。一种常见的模式是用Python脚本生成一个包含所有Trial参数已随机化并平衡好条件的CSV或JSON文件。在Unity中在实验开始前读取这个文件。根据文件内容使用session.CreateBlock()并传入自定义的Trial列表来创建Block。这确保了实验逻辑Unity和实验设计Python的分离让修改实验设计变得非常容易也便于版本控制。5.3 自定义数据记录与输出除了UXF自动记录和通过trial.result字典记录的数据你还可以记录任何自定义对象。void OnTrialEnd(Trial trial) { // 记录一个自定义的类对象需要可序列化 MyCustomData data new MyCustomData(); data.someValue 123; data.anotherValue hello; // UXF会尝试将其转换为JSON格式存储 trial.result[custom_data] data; // 或者直接记录到Session的独立数据文件中 session.SaveDictToSessionData(extra_notes, 这是一个备注); }注意要记录自定义类该类必须标记为[System.Serializable]并且其所有字段也必须是可序列化的类型。5.4 与外部设备集成许多实验需要连接眼动仪、脑电设备、反应盒等。UXF本身不直接提供这些驱动但它提供了精确的时间戳和事件系统使得集成变得清晰。时间同步在OnTrialBegin时你可以获取Time.time作为刺激呈现的Unity时间。同时向外部设备发送一个触发信号Trigger。在数据分析时你可以用这个触发信号来对齐Unity的时间线和设备记录的时间线。数据流通常外部设备有自己的SDK会在另一个线程或进程中持续记录数据并保存到文件。你只需要在UXF记录的数据中标记出每个关键事件如Trial开始、刺激呈现、反应发生的精确时间戳最好是设备时间或同步后的时间用于后期与设备数据合并分析。6. 常见问题、调试技巧与性能优化即使框架再好实际开发中也会遇到各种坑。下面是我在多个项目中总结的一些典型问题和解决方法。6.1 安装与导入问题问题现象可能原因解决方案导入UXF后出现大量编译错误Unity版本与UXF版本不兼容或项目原有脚本与UXF的依赖如Newtonsoft Json.NET冲突。1. 检查UXF官方文档对Unity版本的要求。2. 尝试创建一个全新的、纯净的Unity项目导入UXF确认框架本身无问题。3. 如果使用Package Manager安装尝试指定一个更早的稳定版本号。菜单栏没有出现“UXF”菜单脚本编译失败导致编辑器脚本未加载。查看Console窗口中的错误信息优先解决编译错误。通常是因为缺少依赖或API变更。运行时报错“找不到命名空间UXF”脚本文件没有引用using UXF;或者UXF程序集未正确加载。1. 在脚本顶部添加using UXF;。2. 重启Unity编辑器。3. 检查Project窗口中的UXF文件夹是否完整。6.2 运行时逻辑问题Trial不自动结束/不进入下一个这是新手最常见的问题。UXF不会自动结束Trial你必须在你定义的实验逻辑中在合适的时机如被试反应后、或超时后手动调用currentTrial.End()。检查你的代码确保所有逻辑分支最终都调用了End()方法。数据没有保存首先检查Session是否正常结束。只有session.End()被调用通常是所有Trial完成自动调用或出错手动调用后数据才会写入磁盘。可以在OnSessionEnd事件中添加一个Debug.Log来确认。其次检查保存路径的写入权限。时间戳不准Unity的Time.time受游戏帧率影响虽然对于大多数行为实验精度要求通常在毫秒级足够了但对于超高精度需求如脑电同步需要使用System.Diagnostics.Stopwatch或从外部设备获取主时钟。在刺激呈现和反应捕获的代码路径中要尽可能减少不必要的计算避免帧率波动带来额外误差。6.3 性能优化要点避免在Update中做复杂计算尤其是在每个Trial中都要运行的检测逻辑。确保你的刺激呈现和反应检测代码是高效的。对象池化Object Pooling如果你的实验需要频繁生成和销毁大量GameObject如快速序列呈现的刺激使用对象池来复用对象可以极大减少GC垃圾回收带来的卡顿保证时间精度。预加载资源将所有实验需要用到的图片、声音等资源在实验开始前如指导语界面阶段就加载到内存中避免在Trial中间加载造成不可预测的延迟。关闭不必要的Unity服务在构建独立运行的程序时可以在Player Settings中关闭不需要的模块如物理引擎、网络等并确保图形设置适合你的刺激避免垂直同步Vsync引入的延迟。6.4 调试与日志UXF内置了详细的日志系统。在UXF - Show Settings中你可以调整日志级别。在开发阶段建议设置为“Info”或“Debug”这样可以在Unity的Console窗口中看到每个Session、Block、Trial的开始结束信息以及数据保存的路径对于追踪实验流程非常有帮助。在实验正式运行时可以调回“Warning”或“Error”以减少日志输出对性能的潜在影响。另外养成一个好习惯在关键节点如OnTrialBegin、反应捕获时、OnTrialEnd使用Debug.Log输出关键变量的值并附加上时间戳Time.time。这能帮你快速定位逻辑错误或时序问题。7. 项目构建与部署实验开发完成后你需要将其分发给被试运行。UXF程序与普通Unity程序的构建过程基本一致但有一些特殊注意事项。7.1 构建平台选择Windows/macOS独立应用这是最常用的部署方式。在File - Build Settings中选择PC平台进行构建。确保在Player Settings中设置了合适的公司名和产品名因为这会影响UXF默认的数据存储路径。WebGLUXF支持WebGL构建这允许被试直接在浏览器中运行实验无需安装非常适合在线数据收集。但是WebGL有严格的浏览器安全限制文件系统WebGL不能直接写入用户的本地文件系统。UXF在WebGL模式下会自动将数据转换为一个可供下载的文本文件如.json或.csv。你需要引导被试在实验结束后手动点击下载链接保存数据。性能复杂的图形或大量刺激可能在WebGL上性能较差务必在目标浏览器中进行充分测试。全屏与指针锁定反应时实验通常需要全屏和锁定鼠标指针以防止意外点击。Unity WebGL的全屏API需要用户手势触发需妥善设计指导语界面。7.2 构建前检查清单数据路径确认你的代码或UXF设置中的数据保存路径在目标操作系统上是有效的。对于独立应用使用Application.persistentDataPath是通用选择。输入处理确保所有输入键盘、鼠标在构建后正常工作。注意在打包后某些系统快捷键如AltTab, CmdQ可能会干扰实验需要在指导语中明确告知被试。分辨率与显示设置游戏的初始分辨率和全屏模式。对于视觉实验强制全屏并禁用显示器的省电模式、夜间模式等干扰功能非常重要。关闭开发者功能移除所有调试用的UI按钮、日志输出确保构建版本是干净的。创建实验说明文档准备一个简短的README.txt说明如何运行程序、实验流程、按键说明以及如何找到生成的数据文件。7.3 版本管理与可复现性科研实验要求极高的可复现性。这意味着一年后你或其他人必须能完全复现今天的实验。UXF在这方面提供了很好的基础但你需要建立自己的规范版本控制使用Git管理你的整个Unity项目。将Assets、ProjectSettings和Packages目录纳入版本控制。确保记录了UXF的具体版本号在Packages/manifest.json中。参数固化实验的所有参数刺激列表、试次顺序、时间参数等不应硬编码在脚本里而应保存在配置文件中如JSON。并将这些配置文件一并纳入版本控制。每次实验运行都应该记录下使用了哪个版本的代码和哪个版本的参数文件。数据标记在生成的数据文件中UXF会自动包含Session UUID、开始时间等信息。我强烈建议在Session开始时额外记录一个“实验版本号”或“Git提交哈希”到自定义的Session数据中。这样在分析数据时你能精确知道这份数据是由哪个版本的实验程序生成的。8. 从UXF出发扩展工作流与生态UXF解决了实验程序的核心架构问题但一个完整的研究工作流还涉及其他环节。了解如何将UXF融入这个工作流能极大提升你的效率。8.1 与PsychoPy等工具的对比与协作PsychoPy是心理学实验领域的另一个标杆工具基于Python有图形化界面Builder模式。它和UXF基于Unity各有优劣PsychoPy优势在于入门极快Builder模式无需编程即可创建标准实验刺激呈现和时间精度经过学术界长期验证与Python科学计算生态无缝衔接。劣势在于复杂交互、3D环境或需要高性能渲染的实验实现起来困难。UXF/Unity优势在于能实现任何你能想象到的视觉、听觉刺激和交互尤其是3D、VR/AR性能强大C#语言在游戏开发中久经考验资源丰富。劣势在于需要编程基础学习曲线更陡。它们完全可以协作我经常看到的研究模式是用PsychoPy的Builder快速生成刺激列表和试次序列JSON/CSV文件然后用UXF/Unity来加载和执行这个序列并利用Unity的渲染能力呈现复杂刺激。数据分析阶段又可以用Python与PsychoPy同生态来处理UXF生成的CSV数据。8.2 数据分析流水线UXF生成的是结构化的CSV文件这为自动化数据分析打开了大门。我个人的流水线通常是这样的数据整理写一个Python脚本使用pandas库读取UXF输出的所有Session文件夹将数据合并、清洗如剔除异常反应时并计算每个被试在每个条件下的平均反应时、正确率等汇总指标。统计分析使用Python的statsmodels、pingouin或R语言进行重复测量方差分析、t检验等统计。可视化使用matplotlib、seaborn或plotly绘制条件对比图、个体数据点图等。报告生成使用Jupyter Notebook或R Markdown将数据整理、分析和可视化的步骤整合成一个可交互、可复现的报告文档。这个流水线的核心思想是自动化和可复现。一旦脚本写好新的数据进来一键运行就能得到分析结果和图表彻底告别手动在Excel里复制粘贴、计算的日子。8.3 社区资源与深入学习UXF有一个活跃的GitHub仓库和社区论坛如Unity官方论坛的相关板块这是解决问题的宝贵资源。官方示例UXF包内自带多个示例场景Simple Reaction Time, 2D Positioning Task等这是最好的学习材料。务必逐个运行并仔细阅读其源代码。GitHub Issues在遇到问题时先去GitHub的Issues页面搜索很可能已经有人提出并解决了类似问题。阅读源码当你对某些机制感到疑惑时比如Settings是如何被应用到每个Trial的直接去阅读UXF的源代码是最直接的方式。它的代码结构相对清晰是学习如何设计一个优秀框架的绝佳案例。最后我想分享一点个人体会UXF最大的价值在于它迫使你以一种结构化、模块化的方式去思考和组织你的实验。一旦你习惯了这种工作流你会发现设计、实现、修改和复现一个实验的效率得到了质的飞跃。它可能不是最简单的起点但绝对是通往构建复杂、稳健、可维护的行为实验程序的捷径。刚开始接触时多花点时间理解Session、Trial、Block和数据记录这些核心概念后期会省下无数调试和重构的时间。