Unity UMA角色系统:从零构建动态可定制3D角色
1. 项目概述:为什么选择UMA来构建你的角色系统?
如果你在Unity里做过角色相关的项目,尤其是需要大量、可定制化角色的游戏或应用,那你一定经历过那种“复制粘贴改贴图”的枯燥,或者面对不同体型、装备时骨骼绑定错位的抓狂。Unity Multipurpose Avatar,简称UMA,就是为解决这类问题而生的一个强大框架。它不是Asset Store里一个简单的模型资源,而是一套完整的角色创建与管理系统。简单来说,UMA让你能像“捏人”一样,通过组合不同的“DNA”(如体型、肤色)和“覆盖层”(如衣服、发型、纹身),在运行时动态生成一个独一无二的、骨骼绑定正确的3D角色。
我最初接触UMA是在一个需要支持玩家高度自定义角色的MMO项目预研阶段。当时我们评估了手动制作多个基础模型、使用BlendShapes、甚至自己写一套骨骼融合系统等多种方案,最终UMA以其灵活性、社区支持度和相对成熟的工具链胜出。它的核心价值在于“动态”和“标准化”。所有通过UMA生成的角色,都基于一套统一的骨骼架构(UMA使用Humanoid Avatar),这意味着你为其中一个角色制作的动画,可以无缝应用到所有其他UMA角色上,无论他们高矮胖瘦、穿着什么装备。这对于需要大量NPC、支持角色换装的游戏来说,能节省海量的美术资源和开发时间。
网络上关于UMA的讨论很多,从“UMA2怎么导入”到“如何解决装备穿模”,再到“UMA与Final IK的结合使用”,都说明了它的实用性和一定的学习门槛。很多人卡在第一步:从Asset Store下载了UMA 2 Core,导入项目后却一脸茫然,不知道从何下手。这篇教程的目的,就是带你从零开始,绕开那些我踩过的坑,系统地掌握UMA的核心工作流,让你能真正把它用起来,而不是让它在你的项目里“积灰”。
2. UMA核心架构与资源准备
在动手之前,我们必须理解UMA的“乐高”式构建思想。一个UMA角色不是由一个完整的.FBX文件构成的,而是由多种资源在运行时“组装”起来的。理解这些核心组件,是后续一切操作的基础。
2.1 UMA的核心组件解析
一个完整的UMA角色,主要由以下几部分构成:
种族定义(Race Data):这是角色的“蓝图”。它定义了角色的基础骨骼结构(使用的是Unity的Humanoid骨骼)、可用的插槽(Slot,如头部、躯干、腿部等)以及一套基础的“覆盖层”(Overlay,通常是裸体皮肤)。常见的种族有HumanMale(人类男性)、HumanFemale(人类女性),UMA也支持自定义种族,比如精灵、兽人等。
插槽(Slot):你可以把它想象成角色身上的一个“挂载点”。每个插槽对应身体的一个部位,例如“Head”、“Torso”、“Legs”、“Hands”、“Feet”等。一个插槽上可以叠加多个覆盖层。
覆盖层(Overlay):这是UMA的“皮肤”或“衣服”。一个覆盖层本质上是一张纹理(Texture)及其对应的UV信息。它被分配到特定的插槽上,用来表现皮肤、衬衫、裤子、盔甲、毛发等。多个覆盖层可以在同一个插槽上叠加混合,比如先有一层基础皮肤,再叠加一层衣服纹理,最后再叠加一个盔甲破损的贴图。
DNA(DNA):这是控制角色形态参数的“基因”。DNA是一组可调节的浮点数值,通过UMA内置的变形器(DNA Converter)来驱动骨骼的缩放、位移或蒙皮网格的形变(通过BlendShapes),从而实现调整身高、臂长、肌肉强度、胸部大小等体型特征。这是实现“捏人”系统的关键。
配方(Recipe):这是一个配置文件,它保存了创建一个特定角色所需的所有“配方”:它属于哪个种族、包含了哪些DNA设置、在哪些插槽上使用了哪些覆盖层。运行时,UMA系统读取这个配方,就能动态生成对应的角色。配方可以保存为.asset文件,方便复用。
2.2 项目初始化与资源导入
首先,你需要一个Unity项目(建议使用较新的LTS版本,如2022.3.x)。UMA的获取方式主要有两种:
从Asset Store获取(推荐新手):在Unity Asset Store中搜索“UMA 2”。你会发现有“UMA 2 - Core”这个免费包,这是必须的基础框架。此外,还有“UMA 2 - Examples”示例包和大量付费的内容包(如服装、发型等)。请务必先导入“UMA 2 - Core”。
注意:导入UMA Core时,Unity可能会弹出“API更新”或“重编译”提示,这是正常的,点击确认即可。导入后,你的项目菜单栏会出现“UMA”和“UMA2”两个菜单项。
从GitHub获取(适用于进阶和定制):UMA的核心代码库在GitHub上是开源的。你可以克隆仓库,手动将必要的脚本和资源拖入项目。这种方式能让你紧跟最新修复,但需要一定的版本管理能力。
导入Core后,我强烈建议你同时导入“UMA 2 - Examples”。这个示例项目包含了预设的角色、服装、场景以及最重要的——演示脚本。它是你最好的学习资料。导入后,你可以在Assets/UMA/Examples目录下找到这些资源。
一个关键的准备工作:设置纹理导入格式。UMA的覆盖层纹理通常需要设置为“Advanced”模式,并启用“Read/Write Enabled”选项,因为UMA需要在运行时读写这些纹理进行混合。如果你使用示例资源,它们通常已经设置好了。但当你导入自己的纹理时,如果遇到角色显示为粉红色(Missing Shader),很大概率就是因为纹理导入设置不正确。选中你的纹理文件,在Inspector面板中,确保“Read/Write Enabled”被勾选,并根据情况选择合适的纹理类型(如Default)。
3. 从零开始创建你的第一个UMA角色
理论说得再多,不如动手做一遍。让我们抛开示例场景,自己从头创建一个能跑能跳的UMA角色。
3.1 创建基础角色预制体
- 在场景中创建一个空的GameObject,命名为“MyFirstUMA”。
- 选中这个GameObject,点击菜单栏
UMA2 -> Create -> UMA Avatar。你会发现它被自动添加了UMAAvatarBase脚本。 - 在
UMAAvatarBase组件的Race Data字段,你需要指定一个种族。点击右侧的小圆点,在弹出的选择窗口中,找到Assets/UMA/Content/UMA/Races文件夹,选择HumanMale或HumanFemale。这里我们选择HumanMale。 - 保持其他默认设置,现在运行游戏。你会看到一个……嗯,可能是粉红色的、或者姿势奇怪的基础人形。这是因为我们还没有给他任何“覆盖层”(衣服或皮肤)。
3.2 使用配方(Recipe)快速着装
手动在代码里添加覆盖层太麻烦,UMA提供了更直观的方式:使用预制的配方(Recipe)。
- 在Project窗口中,导航到
Assets/UMA/Examples/Assets/Recipes。这里有很多预设的配方文件(.asset格式)。 - 将一个配方,比如
CompleteAvatar_Example,直接拖拽到场景中你的“MyFirstUMA”游戏对象上。 - 你会立刻看到角色穿上了完整的装备。这就是配方的力量——它封装了所有DNA和覆盖层信息。
- 查看
UMAAvatarBase组件,你会发现Active Recipe字段现在显示了你拖入的配方名称。同时,Additional Recipes列表里也可能有内容。UMA允许一个基础配方叠加多个附加配方,实现模块化换装。
实操心得:直接拖拽配方到Avatar上是测试和原型设计阶段最快的方法。但在实际项目中,我们通常通过代码动态加载和更换配方。你可以通过UMAAvatarBase.LoadFromRecipe(umaRecipe)方法来实现。
3.3 动态换装与DNA调整
现在,让我们通过一个简单的脚本来实现动态换装和“捏人”。创建一个新的C#脚本,命名为SimpleUMAController,挂载到你的UMA角色对象上。
using UMA; using UMA.CharacterSystem; using UnityEngine; public class SimpleUMAController : MonoBehaviour { private DynamicCharacterAvatar avatar; // 使用更强大的DynamicCharacterAvatar public UMARecipeBase[] wardrobeRecipes; // 一个服装配方的数组 private int currentWardrobeIndex = 0; public float dnaValue = 0.5f; // 用于调整的DNA值 void Start() { // 获取DynamicCharacterAvatar组件,它比UMAAvatarBase功能更全 avatar = GetComponent<DynamicCharacterAvatar>(); if (avatar == null) { Debug.LogError("未找到DynamicCharacterAvatar组件!"); return; } // 监听角色创建完成的事件 avatar.CharacterCreated.AddListener(OnCharacterCreated); // 监听角色更新完成的事件(每次换装、调整DNA后触发) avatar.CharacterUpdated.AddListener(OnCharacterUpdated); } void Update() { // 按C键循环更换服装 if (Input.GetKeyDown(KeyCode.C) && wardrobeRecipes.Length > 0) { ChangeWardrobe(); } // 按上下箭头调整身高DNA if (Input.GetKey(KeyCode.UpArrow)) { AdjustDNA("height", 0.01f); } if (Input.GetKey(KeyCode.DownArrow)) { AdjustDNA("height", -0.01f); } } void ChangeWardrobe() { if (wardrobeRecipes == null || wardrobeRecipes.Length == 0) return; // 先清除当前所有的“Wardrobe”插槽的覆盖层(即衣服类) avatar.ClearSlot("Wardrobe"); // 加载新的服装配方到“Wardrobe”插槽 // 注意:这里假设你的服装配方只包含衣服的覆盖层,并且其插槽类型正确 avatar.LoadFromRecipe(wardrobeRecipes[currentWardrobeIndex]); // 更新索引 currentWardrobeIndex = (currentWardrobeIndex + 1) % wardrobeRecipes.Length; } void AdjustDNA(string dnaName, float delta) { // 通过SetDNA方法动态调整DNA值 avatar.SetDNA(dnaName, Mathf.Clamp01(dnaValue + delta)); dnaValue = Mathf.Clamp01(dnaValue + delta); // 应用DNA更改 avatar.BuildCharacter(); } void OnCharacterCreated(UMAData umaData) { Debug.Log("角色创建完成!"); } void OnCharacterUpdated(UMAData umaData) { Debug.Log("角色更新完成!"); } }将这个脚本挂载后,你需要做几件事:
- 将脚本中的
avatar类型从UMAAvatarBase改为DynamicCharacterAvatar。因此,你需要将场景中角色对象上的UMAAvatarBase组件移除(右键->Remove Component),然后通过菜单UMA2 -> Create -> Dynamic Character Avatar重新添加一个DynamicCharacterAvatar组件。这个组件功能更强大,是UMA2推荐使用的。 - 在Inspector中,将
DynamicCharacterAvatar的Race Data同样设置为HumanMale。 - 将你的
SimpleUMAController脚本拖到角色上。 - 在脚本的
Wardrobe Recipes数组里,从Assets/UMA/Examples/Assets/Recipes拖入几个不同的配方,比如CompleteAvatar_Example和Knight。 - 运行游戏。按C键,你应该能看到角色在不同的套装间切换。按上下箭头,角色的身高应该会发生变化。
重要提示:
DynamicCharacterAvatar的LoadFromRecipe方法会默认将配方中的覆盖层加载到名为“Wardrobe”的插槽集合中。而ClearSlot(“Wardrobe”)会清空这个集合。这是一种管理服装的常见模式,可以与基础皮肤(在“Base”插槽集合)分离。你需要根据你的配方实际设计来调整插槽集合的名称。
4. 深入UMA工作流:制作自定义内容
使用官方或商店资源固然方便,但项目的独特风格往往需要自定义内容。这部分是UMA学习的深水区,也是体现其价值的地方。
4.1 创建自定义覆盖层(衣服/贴图)
假设我们要为角色制作一件简单的T恤。
- 准备纹理:你需要一张漫反射贴图(Albedo)和一张法线贴图(Normal Map)。在Photoshop等软件中制作,确保UV与你目标种族的基础UV匹配。UMA人类种族通常使用一套标准的UV布局。
- 导入纹理:将T恤的漫反射贴图和法线贴图导入Unity。如前所述,务必在导入设置中勾选“Read/Write Enabled”。
- 创建覆盖层资源:在Project窗口右键 ->
Create -> UMA -> Overlay Data。将其命名为“MyTShirtOverlay”。 - 配置覆盖层:
Overlay Name: 填写“MyTShirt”。Slot:选择“UpperBody”。这决定了这件衣服穿在身体的哪个部位。Rect:这是一个关键设置。它定义了这张纹理在角色UV空间中的位置和大小。对于覆盖全身的服装,通常使用(0,0,1,1)。对于只覆盖部分部位(如头盔)的,需要精确指定矩形。你可以使用示例中的覆盖层作为参考,或者使用UMA提供的工具来生成。Texture:拖入你的T恤漫反射贴图。Normal Texture:拖入你的法线贴图。Material:通常使用UMA提供的标准材质,如“UMA Standard Shader”。你可以创建自己的材质球来获得更复杂的着色效果。
- 创建配方并使用:现在你需要一个配方来使用这个覆盖层。最简单的方法是复制一个现有的配方(如
CompleteAvatar_Example),在Inspector中编辑它,找到“UpperBody”相关的覆盖层列表,将你新建的“MyTShirtOverlay”添加进去。然后将这个新配方应用到你的角色上。
踩坑记录:Rect设置错误是导致贴图错位、拉伸或显示不全的最常见原因。对于新手,建议先从修改现有配方的纹理开始,而不是从头创建Rect。
4.2 定义自定义DNA(捏人参数)
UMA内置了一些基础的DNA,如身高(height)、肌肉(muscle)等。但你可以创建自己的DNA来控制更具体的部位,比如鼻梁高度、耳朵大小。
- 创建DNA转换器(DNA Converter):这是DNA系统的核心。它定义了DNA值如何影响角色网格。UMA内置了多种转换器,最常用的是
UMADnaHumanoid,它使用BlendShapes。你需要创建一个继承自UMADnaBase的脚本,但更简单的方式是复制并修改一个现有的DNA资产。 - 关联DNA与骨骼/形变:在DNA转换器中,你会看到一系列浮点字段(如
headWidth)。每个字段对应一个DNA名称。你需要通过编写代码或在编辑器中配置,将这些字段与骨骼的局部缩放、位置,或者SkinnedMeshRenderer上的BlendShape索引关联起来。这部分需要一定的编程和3D概念基础。 - 在种族数据中注册DNA:打开你的种族数据(如
HumanMale),在DNA Converter列表中添加你创建的自定义DNA转换器。 - 在配方或代码中调整:之后,你就可以像调整身高一样,通过
SetDNA(“yourCustomDNA”, value)来调整你自定义的参数了。
注意:自定义DNA是UMA的高级功能,涉及对角色骨骼和网格变形的深入理解。对于大多数项目,内置的DNA和通过覆盖层(如不同体型的法线贴图)来表现差异已经足够。
4.3 与动画系统的集成
这是UMA最大的优势之一。由于UMA角色在生成后,其SkinnedMeshRenderer使用的Avatar是标准的Unity Humanoid Avatar,因此它与Unity的Mecanim动画系统是完美兼容的。
- 确保骨骼匹配:在种族数据的导入设置中,确保其Rig类型为“Humanoid”。UMA内置种族已经设置好了。
- 应用动画控制器:为你的UMA角色对象添加一个
Animator组件。将你制作好的Animator Controller(动画控制器)拖入Controller字段。 - 动画重定向:因为所有UMA角色共享同一套Humanoid骨骼定义,所以你为一个瘦子制作的行走动画,无需任何调整,就可以直接应用到一个胖子UMA角色上,Unity会自动进行骨骼重定向。这是手动管理多个模型无法比拟的效率。
- 与Final IK等插件结合:由于是标准的Humanoid Avatar,你可以直接使用Final IK、Unity Animation Rigging等插件来为UMA角色添加逆向动力学、程序化动画等高级特性,只需像对待普通Humanoid模型一样操作即可。
实操心得:在角色生成完成(即收到CharacterCreated事件)后再去获取并配置Animator组件是最稳妥的。因为在此之前,角色的骨骼和SkinnedMeshRenderer可能还未就绪。
5. 性能优化与常见问题排查
UMA的动态生成特性是一把双刃剑,带来了灵活性的同时,也带来了性能开销。在移动端或需要大量同屏角色的项目中,优化至关重要。
5.1 性能优化策略
- 纹理图集(Texture Atlasing):这是最重要的优化。UMA在运行时默认会将角色所有覆盖层的纹理合并到一张或几张大的图集(Atlas)中。你可以通过
UMAAtlas组件来配置图集的最大尺寸(如1024x1024)、格式(压缩格式)等。确保图集尺寸不要过大,并针对目标平台(如Android用ETC2,iOS用ASTC)选择合适的压缩格式。 - LOD(Level of Detail):UMA支持为角色生成不同精度的LOD网格。你可以在种族数据中配置LOD级别,并设置距离切换阈值。对于远处的角色,使用面数更少的LOD可以极大提升渲染性能。
- 对象池(Object Pooling):频繁创建和销毁UMA角色(如NPC刷新)会产生GC(垃圾回收)压力。实现一个简单的UMA角色对象池,在需要时激活并重置一个已生成的角色,而不是每次都从头创建。
- 预生成(Pre-Baking):对于确定不会在运行时改变外观的NPC,可以考虑在编辑模式或加载时预生成(Bake)其网格和材质,将其转换为一个普通的静态SkinnedMeshRenderer,这样可以完全消除UMA的运行时开销。UMA提供了
UMAData.BakeMesh和相关的工具方法。 - 减少DNA复杂度:不必要的DNA计算会增加生成时间。只保留项目真正需要的DNA参数。
5.2 常见问题与解决方案速查表
以下是我在项目中遇到的一些典型问题及解决方法:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 角色显示为粉红色(洋红色) | 1. 覆盖层纹理的Shader丢失或错误。 2. 纹理未开启“Read/Write Enabled”。 3. 图集生成失败。 | 1. 检查覆盖层或最终材质使用的Shader是否正确(如UMA Standard Shader)。 2. 在纹理导入设置中勾选“Read/Write Enabled”。 3. 检查Console是否有图集生成错误日志。 |
| 装备严重穿模 | 1. 覆盖层对应的网格(Rect)不匹配当前体型。 2. 服装模型本身是为特定体型制作,与当前DNA设定的体型差异过大。 | 1. 检查覆盖层的Rect设置是否准确覆盖目标身体部位。 2. 为不同体型范围制作不同的服装覆盖层版本,或使用更宽松的模型。 3. 考虑使用“遮罩”覆盖层来隐藏穿模部位(治标不治本)。 |
| 角色生成后动画不播放或扭曲 | 1. Animator组件在UMA生成角色前就已配置并播放动画。 2. 骨骼映射(Avatar)不正确。 | 1. 在CharacterCreated事件回调中再配置Animator和播放动画。2. 确保种族数据的Rig类型为Humanoid,并且骨骼映射正确(可通过预览窗口检查)。 |
| 运行时换装卡顿 | 每次换装都触发完整的图集重建和网格更新。 | 1. 使用DynamicCharacterAvatar的LoadFromRecipe时,尽量一次性加载所有需要变化的覆盖层,而不是分多次加载。2. 对于频繁切换的服装,考虑预生成其对应的UMAData状态并缓存起来。 3. 使用 avatar.BuildCharacter(true)中的异步生成选项(如果版本支持)。 |
| 打包后角色显示异常 | 纹理图集在打包时未被正确包含在构建中,或者Shader变体丢失。 | 1. 检查UMA相关的Shader是否被添加到项目的Graphics Settings -> Shader Stripping中。 2. 确保所有自定义的覆盖层纹理、材质等资源都被场景或Resources引用,或者明确标记在AssetBundle中。 |
| DNA调整不生效 | 1. DNA名称拼写错误。 2. 对应的DNA转换器未正确关联到骨骼或BlendShape。 3. 调整DNA后未调用 BuildCharacter()。 | 1. 仔细核对DNA字符串名称,区分大小写。 2. 检查DNA转换器配置和种族数据中的注册。 3. 确保在设置DNA值后调用了 avatar.BuildCharacter()或avatar.ForceUpdate()。 |
6. 项目实践:构建一个简单的角色创建场景
让我们综合运用以上知识,搭建一个微型的角色创建场景,包含性别选择、体型滑动条和换装功能。
场景搭建:创建UI Canvas,添加以下UI元素:
- 两个Button:
Male,Female。 - 两个Slider:
Height Slider(范围0.0-1.0),Muscle Slider(范围0.0-1.0)。 - 一个Dropdown:
Wardrobe Dropdown, 用于选择服装。 - 一个RawImage:用于显示角色(需要渲染纹理Render Texture)。
- 两个Button:
脚本编写:创建
CharacterCreator脚本。
using UMA.CharacterSystem; using UnityEngine; using UnityEngine.UI; public class CharacterCreator : MonoBehaviour { public DynamicCharacterAvatar avatar; public RenderTexture renderTexture; // 用于在UI上显示角色的渲染纹理 public Camera renderCamera; // 专门渲染角色的相机 public UMARecipeBase maleRaceRecipe; public UMARecipeBase femaleRaceRecipe; public UMARecipeBase[] wardrobeOptions; // 所有可选的服装配方 private float currentHeight = 0.5f; private float currentMuscle = 0.5f; void Start() { if (renderCamera != null && renderTexture != null) { renderCamera.targetTexture = renderTexture; } // 初始化UI var wardrobeDropdown = FindObjectOfType<Dropdown>(); // 实际项目中应通过序列化字段引用 if (wardrobeDropdown != null && wardrobeOptions != null) { wardrobeDropdown.ClearOptions(); foreach (var recipe in wardrobeOptions) { wardrobeDropdown.options.Add(new Dropdown.OptionData(recipe.name)); } wardrobeDropdown.onValueChanged.AddListener(OnWardrobeChanged); wardrobeDropdown.value = 0; wardrobeDropdown.RefreshShownValue(); } // 初始化角色为男性 ChangeRace(true); } // 由Male/Female按钮调用 public void ChangeRace(bool isMale) { if (avatar == null) return; // 清空当前角色 avatar.ClearRace(); UMARecipeBase raceRecipe = isMale ? maleRaceRecipe : femaleRaceRecipe; if (raceRecipe != null) { avatar.LoadFromRecipe(raceRecipe); // 重新应用当前的DNA和服装 avatar.SetDNA("height", currentHeight); avatar.SetDNA("muscle", currentMuscle); ApplyCurrentWardrobe(); avatar.BuildCharacter(); } } // 由Height Slider的OnValueChanged事件调用 public void OnHeightChanged(float value) { currentHeight = value; if (avatar.isActiveAndEnabled) { avatar.SetDNA("height", value); avatar.BuildCharacter(); } } // 由Muscle Slider调用 public void OnMuscleChanged(float value) { currentMuscle = value; if (avatar.isActiveAndEnabled) { avatar.SetDNA("muscle", value); avatar.BuildCharacter(); } } // 由Wardrobe Dropdown调用 public void OnWardrobeChanged(int index) { if (wardrobeOptions == null || index < 0 || index >= wardrobeOptions.Length) return; ApplyCurrentWardrobe(); if (avatar.isActiveAndEnabled) { avatar.BuildCharacter(); } } private void ApplyCurrentWardrobe() { var dropdown = FindObjectOfType<Dropdown>(); if (dropdown == null || wardrobeOptions == null) return; int index = dropdown.value; if (index >= 0 && index < wardrobeOptions.Length) { avatar.ClearSlot("Wardrobe"); // 清空旧服装 avatar.LoadFromRecipe(wardrobeOptions[index]); // 加载新服装 } } }- 配置与运行:
- 将脚本挂载到场景中。
- 将你的UMA角色(带有
DynamicCharacterAvatar)拖入avatar字段。 - 分别创建男性和女性的基础配方(只包含皮肤和基础DNA),拖入对应的字段。
- 准备几套服装配方,填入
wardrobeOptions数组。 - 配置好UI事件绑定(如Slider的
OnValueChanged事件指向脚本的对应方法)。 - 运行场景,你应该可以通过UI控件实时改变角色的性别、体型和服装。
这个简单的例子涵盖了UMA最核心的运行时功能。在实际项目中,你会需要更完善的UI、数据保存(将配方的DNA和覆盖层信息序列化成JSON或二进制)、以及与后端服务器的角色数据同步。
UMA的学习曲线前期确实有些陡峭,尤其是资源制作和DNA系统。但一旦你掌握了它的工作模式,它所带来的灵活性和效率提升是巨大的。我的建议是,先从使用和修改官方示例开始,理解配方、覆盖层、DNA之间的关系,然后再尝试制作简单的自定义内容,最后再挑战复杂的系统集成和性能优化。记住,社区(如官方论坛和GitHub Issues)是你的好朋友,很多奇怪的问题都能在那里找到答案。