第 26 课
实现博物馆风格切换功能
场景风格切换的核心不是重新加载场景,也不是更换模型,而是根据下拉列表返回的选项序号,为地面和墙面的 Renderer(渲染器)指定同一套 Material(材质)。完成后,设置页面、场景表面和初始显示会沿着同一条调用链保持一致。
让一个下拉选项同步切换地面与墙面风格
把设置页面的风格下拉列表连接到场景材质。你会从网格、渲染器和材质的关系开始,逐步理解选项序号、对象数组、安全判断与共享材质赋值,最后完成两种博物馆风格的双向切换。
学完你将能够
学习目标
- 说明材质如何决定模型表面的颜色、纹理和质感。
- 区分 UIManager、GameManager 与 AudioManager 的职责。
- 理解 Dropdown 的 onValueChanged 为什么会传入一个从0开始的整数序号。
- 说明 Renderer[] 与 Material[] 分别保存要修改的对象和可选择的材质。
- 解释 Awake、Start 与 OnDestroy 在下拉列表监听中的不同职责。
- 读懂 ApplyVisualStyle 如何用同一个序号同时控制地面和墙面。
- 逐项解释 ApplyMaterial 的空引用、越界和缺失材质保护。
- 理解 foreach 循环和 sharedMaterial 赋值实际改变了什么。
- 正确导入风格材质包并保持两组材质数组的索引一致。
- 完成完整脚本、场景引用和两种风格的双向测试。
开始操作前
准备工作
- 打开第25课完成的主场景,确认系统菜单、声音开关和导航按钮可以正常使用。
- 在设置页面找到 VisualStyleDropdown,并确认其中有“现代简约”和“复古怀旧”两个选项。
- 确认场景中能够找到 Floor、Wall 和 Wall2,三者都带有 Renderer 类型的渲染组件。
- 下载本课提供的 StyleMat.unitypackage,并先保存项目与当前场景。
- 打开 UIManager.cs、层级面板、项目面板、属性面板和控制台。
- 退出运行状态后再修改脚本、导入材质和连接引用。
本课学习路线
按照学习模块逐步完成
每个模块都包含理解、跟随操作和独立实践。同一模型可以穿上不同的表面外观
先理解风格切换究竟改变了什么
墙体和地面的形状没有变化,改变的是渲染器使用的材质。先分清模型、渲染器和材质,后面的代码就会变成一条很清楚的“选择—查找—赋值”链。三个对象各自负责什么
模型保存物体的几何形状,例如墙面有多高、地面有多大。Renderer(渲染器)是 Unity 负责把这个形状绘制到画面上的组件,MeshRenderer(网格渲染器)是场景模型最常见的一种 Renderer。
Material(材质)保存表面如何显示,例如基础颜色、贴图、金属感和光滑度。把同一个 Renderer 的材质从现代风格换成复古风格,模型与碰撞范围不会移动,但表面外观会立刻改变。
因此,本课不需要重新生成墙体,也不需要切换整套场景。下拉列表只提供一个风格序号,UIManager 再用这个序号找到地面材质与墙面材质。
- 输入:VisualStyleDropdown 当前选择的序号。
- 处理:UIManager 根据序号选择两组材质。
- 输出:Floor、Wall 和 Wall2 的共享材质引用被替换。
- 不受影响:模型位置、碰撞体、菜单导航和声音状态。
网格、渲染器与材质的关系
三个概念互相配合,但只有材质会在本课被替换。
左右滑动表格,可查看完整内容
| 概念 | 负责什么 | 本课中的表现 |
|---|---|---|
| Mesh(网格) | 保存物体的几何形状 | 墙面高度、地面大小保持不变 |
| Renderer(渲染器) | 把网格和材质绘制到画面 | Floor、Wall、Wall2 上的 Mesh Renderer |
| Material(材质) | 决定颜色、贴图和表面质感 | 现代简约与复古怀旧两套外观 |
辨认场景中真正要改变的组件
在层级面板中依次选择 Floor、Wall 和 Wall2,找到它们的渲染组件,并观察材质槽。
- 确认三个对象都存在 Renderer 类型的组件。
- 记录当前材质名称,但不要手动改动材质。
- 说出风格切换后哪些内容会变化、哪些内容不会变化。
把界面输入、场景规则和声音状态放到正确位置
明确三个管理脚本各自负责什么
风格选项来自设置页面,最终改变的也是画面外观,因此这条功能链由 UIManager 接收并执行。先分清脚本职责,可以避免把材质切换写进不相关的管理脚本。为什么由 UIManager 完成风格切换
UIManager 已经连接菜单中的按钮、开关和下拉列表。它最先收到学生在设置页面做出的选择,再把选项序号转换为地面与墙面的材质。
GameManager 继续保存玩家引用并处理菜单按键,它不需要知道具体使用哪一种地面材质。AudioManager 继续管理背景音乐和语音解说,也不参与画面风格选择。
完整调用链是:VisualStyleDropdown 产生整数序号 → UIManager 接收序号 → UIManager 选择两组材质 → Renderer 显示新外观。
三个管理脚本的职责
每个脚本只承担与自己直接相关的任务,脚本之间通过清楚的方法调用协作。
左右滑动表格,可查看完整内容
| 脚本 | 主要职责 | 与风格切换的关系 |
|---|---|---|
| UIManager.cs | 接收菜单输入并更新界面或场景表现 | 接收下拉序号并应用地面、墙面材质 |
| GameManager.cs | 保存玩家引用并处理菜单按键 | 提供玩家与菜单功能,不负责材质选择 |
| AudioManager.cs | 统一管理背景音乐和语音解说 | 保持声音状态,不负责画面风格 |
把功能放回正确的脚本
根据职责表,为每项功能选择最合适的脚本,并说出理由。
- 把“接收风格下拉列表”归入 UIManager。
- 把“应用地面与墙面材质”归入 UIManager。
- 把“读取菜单按键”归入 GameManager。
- 把“暂停背景音乐”归入 AudioManager。
代码要先知道输入来自哪里、修改谁、换成什么
新增五个字段,准备界面、表面和材质引用
材质切换需要三类信息:下拉列表、目标渲染器和候选材质。五个字段把这些信息保存到 UIManager,并允许在 Unity 属性面板中完成连接;Header 则只负责整理属性面板中的分组标题。 [Header("风格设置")]
[SerializeField] private Dropdown visualStyleDropdown;
[SerializeField] private Renderer[] floorRenderers;
[SerializeField] private Renderer[] wallRenderers;
[SerializeField] private Material[] floorMaterials;
[SerializeField] private Material[] wallMaterials;这6行位于两个声音 Toggle 字段之后。字段仍是 private,但 SerializeField 让它们能够显示并赋值。
逐行理解字段类型
Header(分组标题)只负责把属性面板中的字段整理到“风格设置”区域,不参与程序运行。SerializeField 表示字段仍由本类保护,但可以在 Unity 属性面板中连接场景对象。
visualStyleDropdown 保存设置页面下拉列表的组件引用。Dropdown 的 value 是整数:第一项为0,第二项为1。
floorRenderers 与 wallRenderers 保存要改变外观的渲染器。使用数组是因为同一类表面可能有多个对象;当前场景有一个地面渲染器和两个墙面渲染器。
floorMaterials 与 wallMaterials 保存每种风格的候选材质。两个材质数组必须用完全相同的风格顺序,选项序号才能同时找到配套的地面材质和墙面材质。
五个字段的对应关系
数组名使用复数,因为一个字段可以保存多个成员。
左右滑动表格,可查看完整内容
| 字段 | 类型 | 应连接的内容 | 当前数量 |
|---|---|---|---|
| visualStyleDropdown | Dropdown | VisualStyleDropdown | 1个 |
| floorRenderers | Renderer[] | Floor 的渲染器 | 1个 |
| wallRenderers | Renderer[] | Wall、Wall2 的渲染器 | 2个 |
| floorMaterials | Material[] | FloorStyleA、FloorStyleB | 2个 |
| wallMaterials | Material[] | WallStyleA、WallStyleB | 2个 |
口头完成一次类型配对
先不拖拽对象,只根据字段名和类型说明每个字段为什么存在。
- 指出哪个字段负责接收用户输入。
- 指出哪两个数组回答“要修改谁”。
- 指出哪两个数组回答“要换成什么”。
- 解释为什么墙面使用 Renderer[] 而不是单个 Renderer。
你选择一项时,Unity 会主动发送新的整数值
监听下拉列表,把选项序号交给处理方法
字段引用只让 UIManager 找到下拉列表,还不会自动执行材质切换。需要把 ApplyVisualStyle 注册为 onValueChanged 的监听方法。 if (visualStyleDropdown != null)
{
visualStyleDropdown.onValueChanged.AddListener(ApplyVisualStyle);
}先确认引用不为空,再把 ApplyVisualStyle 交给 AddListener。这里传递的是方法本身,不要在方法名后写括号。
onValueChanged 会传递什么
onValueChanged 是“值发生变化时”的事件。当用户从第一项切换到第二项时,Dropdown 会调用所有已经注册的监听方法,并把新的 value 一起传过去。
ApplyVisualStyle 的参数类型是 int,正好能够接收这个 value。int 表示整数;这里的 styleIndex 就是数组索引,也就是成员所在的位置编号。
数组索引从0开始,所以两个选项依次对应0和1。页面上显示的中文文字帮助用户理解选项,代码并不通过文字判断风格。
- 选择现代简约:value=0,调用 ApplyVisualStyle(0)。
- 选择复古怀旧:value=1,调用 ApplyVisualStyle(1)。
- 切换时只传序号,不直接传递材质对象。
预测两次选择产生的参数
依次选择两个选项,用调用表达式写出 Unity 将执行的结果。
- 第一项写成 ApplyVisualStyle(0)。
- 第二项写成 ApplyVisualStyle(1)。
- 说明为什么不能把第一项误写成1。
能响应后续变化,也要正确处理第一次进入场景
补全初始同步与监听清理
AddListener 只登记以后要调用的方法,不会自动把下拉列表当前值发送一次。因此 Start 还要主动应用初始值;对象销毁时则应移除监听。 if (visualStyleDropdown != null)
{
ApplyVisualStyle(visualStyleDropdown.value);
}场景开始后读取 visualStyleDropdown.value,并立即走一遍与用户切换时相同的处理方法。
为什么不能只依靠事件
如果场景打开时下拉列表已经停在“复古怀旧”,但用户没有再次点击,onValueChanged 就不会触发。没有 Start 同步时,文字可能显示复古风格,场景却仍保留原来的材质。
Start 主动调用 ApplyVisualStyle,使“界面显示的选择”和“场景实际外观”在第一帧开始工作时保持一致。之后用户每次改变选项,再由事件继续更新。
if (visualStyleDropdown != null)
{
visualStyleDropdown.onValueChanged.RemoveListener(ApplyVisualStyle);
}RemoveListener 使用与 AddListener 完全相同的方法引用,解除 UIManager 与 Dropdown 的事件关系。
三个生命周期位置各自解决的问题
生命周期方法由 Unity 在特定阶段自动调用。
左右滑动表格,可查看完整内容
| 方法 | 发生阶段 | 本课职责 |
|---|---|---|
| Awake | 对象初始化时 | 添加下拉列表监听 |
| Start | 第一次更新前 | 应用下拉列表当前值 |
| OnDestroy | 对象被销毁时 | 移除已经添加的监听 |
按运行顺序复述三步
假设 Dropdown 初始 value 为1,然后用户切换到0,最后场景关闭。
- 说明 Awake 登记了什么。
- 说明 Start 会调用哪个表达式以及传入什么值。
- 说明用户选择0后由谁再次调用方法。
- 说明场景关闭时为什么要解除监听。
一项用户选择,分成两次材质应用
用同一个序号同时控制地面与墙面
地面与墙面使用不同材质,却必须呈现同一种风格。ApplyVisualStyle 不亲自遍历对象,而是把同一个 styleIndex 分别交给两个材质组。 private void ApplyVisualStyle(int styleIndex)
{
ApplyMaterial(floorRenderers, floorMaterials, styleIndex);
ApplyMaterial(wallRenderers, wallMaterials, styleIndex);
}第一次调用处理地面,第二次调用处理墙面;两次都传入同一个 styleIndex。
ApplyVisualStyle 只负责组织调用
方法名 ApplyVisualStyle 表示“应用视觉风格”。它接收下拉列表传来的整数,然后调用通用的 ApplyMaterial。这样地面和墙面不需要各写一套几乎相同的遍历代码。
第一组参数是 floorRenderers 与 floorMaterials,第二组是 wallRenderers 与 wallMaterials。参数成组传递,保证每次调用都明确知道要处理哪些渲染器,以及从哪一组材质中取值。
同一个 styleIndex 是同步的关键。如果地面使用0而墙面使用1,场景就会出现现代地面配复古墙面的混合结果。
一个索引在两组材质中的含义
数组中同一列必须属于同一种完整风格。
左右滑动表格,可查看完整内容
| styleIndex | 下拉选项 | 地面材质 | 墙面材质 |
|---|---|---|---|
| 0 | 现代简约 | FloorStyleA | WallStyleA |
| 1 | 复古怀旧 | FloorStyleB | WallStyleB |
观察重点:一个选项序号分成地面与墙面两条分支,两个分支仍然使用同一个序号,所以最终风格能够保持一致。
手机上可左右滑动;点击图片可以查看完整图
观察重点:列表显示两个文字选项,但代码收到的是整数序号;“现代简约”是0,“复古怀旧”是1。
点击图片可以查看完整截图跟踪 styleIndex=1 的两条调用
从用户选择“复古怀旧”开始,分别写出地面与墙面的参数。
- 写出 ApplyMaterial(floorRenderers, floorMaterials, 1)。
- 写出 ApplyMaterial(wallRenderers, wallMaterials, 1)。
- 从两组材质数组的第1号位置找出最终材质。
先确认输入可用,再逐个修改有效渲染器
逐步读懂安全判断、循环和材质赋值
ApplyMaterial 是本课真正改变场景外观的方法。它先排除无法安全处理的输入,再遍历目标渲染器,把指定序号的材质赋给每一个有效成员。 private static void ApplyMaterial(Renderer[] renderers, Material[] materials, int styleIndex)
{
if (renderers == null || materials == null ||
styleIndex < 0 || styleIndex >= materials.Length ||
materials[styleIndex] == null)
{
return;
}只要任一条件不满足就提前 return,后面的数组访问和循环都不会执行。这只是 ApplyMaterial 的前半段,请继续完成下一段,不要单独保存回 Unity 编译。
先读方法声明,再把长条件拆成五个问题
private 表示这个辅助方法只供 UIManager 类本身调用;static 表示它不依赖某一个 UIManager 对象的其他字段,全部信息都由参数传入;void 表示方法只执行材质替换,不返回结果。三个参数依次是目标渲染器数组、候选材质数组和本次选择的序号。
renderers == null:目标渲染器数组没有连接。materials == null:候选材质数组没有连接。这两种情况都无法继续。
styleIndex < 0:序号位于数组起点之前。styleIndex >= materials.Length:序号已经到达或超过数组长度。数组最后一个合法索引始终是 Length - 1。
materials[styleIndex] == null:索引虽然合法,但该位置没有放入材质。此时继续赋值只会把目标材质清空,所以方法直接结束。
多行条件中的 || 表示“或者”,并且会从左向右短路判断:前面的条件已经为真时,后面的条件就不再读取。正因为先判断序号范围,越界时才不会继续访问 materials[styleIndex]。只要任意一项为真,return 就立即结束本次方法调用。
五个保护条件分别防止什么问题
从左到右逐项判断,只要有一项成立,本次材质应用就会安全结束。
左右滑动表格,可查看完整内容
| 保护条件 | 表示的情况 | 避免的问题 |
|---|---|---|
| renderers == null | 没有连接目标渲染器数组 | 无法遍历目标对象 |
| materials == null | 没有连接候选材质数组 | 无法选择目标材质 |
| styleIndex < 0 | 序号小于数组起点 | 访问不存在的负数位置 |
| styleIndex >= materials.Length | 序号到达或超过数组长度 | 访问数组范围之外的位置 |
| materials[styleIndex] == null | 该位置没有材质 | 把已有表面材质意外清空 |
foreach (Renderer targetRenderer in renderers)
{
if (targetRenderer != null)
{
targetRenderer.sharedMaterial = materials[styleIndex];
}
}
}foreach 逐个取出数组成员;遇到空成员就跳过赋值,其余成员都使用同一风格材质。与上一段组合后,ApplyMaterial 方法才完整;完整类仍要在下一模块组合后再编译。
为什么使用 sharedMaterial
foreach 的意思是“对集合中的每个成员执行一次”。targetRenderer 先代表第一个渲染器,再代表第二个,直到数组遍历完毕。成员为空时,if 条件不成立,循环自然继续处理下一个成员。
sharedMaterial 表示渲染器当前引用的共享材质资源。本课只是在多个已经准备好的材质之间切换引用,不需要为每个对象生成独立材质副本,因此使用 sharedMaterial 更符合目的。
sharedMaterial 只替换 Renderer 的主材质槽。本课的 Floor、Wall 与 Wall2 都只使用一个需要切换的材质槽;如果模型包含多个材质槽,就要改用 sharedMaterials 数组逐项处理。
如果以后要让每面墙拥有互不影响的运行时颜色参数,才需要考虑独立材质实例。本课不改变材质自身的参数,只替换引用,不要额外创建副本。
sharedMaterial 与 material 的区别
本课只在现成材质之间切换引用,因此选择 sharedMaterial。
左右滑动表格,可查看完整内容
| 写法 | 运行时行为 | 适用情况 |
|---|---|---|
| sharedMaterial | 直接引用已有材质资源,不额外生成副本 | 多个表面切换到预先准备的完整风格 |
| material | 访问时可能为当前渲染器生成独立材质副本 | 单独改变某个对象的颜色或参数 |
观察重点:无效输入会直接结束;有效输入进入循环,空渲染器被跳过,有效渲染器才执行 sharedMaterial 赋值。
手机上可左右滑动;点击图片可以查看完整图手动推演一次有效调用和一次无效调用
分别使用 styleIndex=1 和 styleIndex=2,假设材质数组长度为2。
- 说明 styleIndex=1 为什么能够进入循环。
- 说明 styleIndex=2 会命中哪个越界条件。
- 假设墙面数组第二个成员为空,说明第一个成员是否仍会完成赋值。
- 指出真正改变场景外观的唯一一行代码。
方法与依赖全部就绪后,再统一保存和编译
组合完整脚本并准备两套风格材质
现在把前面学习的字段、事件和材质方法放回完整 UIManager.cs,再导入两套地面与墙面材质。完整文件保存后,Unity 才能编译出可以连接到场景的风格字段。using UnityEngine;
using UnityEngine.UI;
public class UIManager : Singleton<UIManager>
{
[SerializeField] private GameObject menuUI;
[SerializeField] private float distanceFromPlayer = 1.5f;
[Header("声音设置")]
[SerializeField] private Toggle backgroundMusicToggle;
[SerializeField] private Toggle narrationToggle;
[Header("风格设置")]
[SerializeField] private Dropdown visualStyleDropdown;
[SerializeField] private Renderer[] floorRenderers;
[SerializeField] private Renderer[] wallRenderers;
[SerializeField] private Material[] floorMaterials;
[SerializeField] private Material[] wallMaterials;
protected override void Awake()
{
base.Awake();
if (Instance != this)
{
return;
}
if (menuUI == null)
{
menuUI = GameObject.Find("MenuUI");
}
if (menuUI != null)
{
menuUI.SetActive(false);
}
else
{
Debug.LogError("UIManager:没有找到 MenuUI。", this);
}
if (backgroundMusicToggle != null)
{
backgroundMusicToggle.onValueChanged.AddListener(SetBackgroundMusicEnabled);
}
if (narrationToggle != null)
{
narrationToggle.onValueChanged.AddListener(SetNarrationEnabled);
}
if (visualStyleDropdown != null)
{
visualStyleDropdown.onValueChanged.AddListener(ApplyVisualStyle);
}
}
private void Start()
{
if (backgroundMusicToggle != null)
{
SetBackgroundMusicEnabled(backgroundMusicToggle.isOn);
}
if (narrationToggle != null)
{
SetNarrationEnabled(narrationToggle.isOn);
}
if (visualStyleDropdown != null)
{
ApplyVisualStyle(visualStyleDropdown.value);
}
}
private void OnDestroy()
{
if (backgroundMusicToggle != null)
{
backgroundMusicToggle.onValueChanged.RemoveListener(SetBackgroundMusicEnabled);
}
if (narrationToggle != null)
{
narrationToggle.onValueChanged.RemoveListener(SetNarrationEnabled);
}
if (visualStyleDropdown != null)
{
visualStyleDropdown.onValueChanged.RemoveListener(ApplyVisualStyle);
}
}
private void LateUpdate()
{
Transform player = GameManager.Instance.Player;
if (menuUI == null || menuUI.activeSelf || player == null)
{
return;
}
PlaceMenuInFrontOfPlayer(player);
}
public void ToggleMenu()
{
if (menuUI == null)
{
return;
}
bool shouldShow = !menuUI.activeSelf;
if (shouldShow && GameManager.Instance.Player != null)
{
PlaceMenuInFrontOfPlayer(GameManager.Instance.Player);
}
menuUI.SetActive(shouldShow);
}
private void SetBackgroundMusicEnabled(bool isEnabled)
{
AudioManager.Instance.SetBackgroundMusicEnabled(isEnabled);
}
private void SetNarrationEnabled(bool isEnabled)
{
AudioManager.Instance.SetNarrationEnabled(isEnabled);
}
private void ApplyVisualStyle(int styleIndex)
{
ApplyMaterial(floorRenderers, floorMaterials, styleIndex);
ApplyMaterial(wallRenderers, wallMaterials, styleIndex);
}
private static void ApplyMaterial(Renderer[] renderers, Material[] materials, int styleIndex)
{
if (renderers == null || materials == null ||
styleIndex < 0 || styleIndex >= materials.Length ||
materials[styleIndex] == null)
{
return;
}
foreach (Renderer targetRenderer in renderers)
{
if (targetRenderer != null)
{
targetRenderer.sharedMaterial = materials[styleIndex];
}
}
}
private void PlaceMenuInFrontOfPlayer(Transform player)
{
menuUI.transform.position =
player.position + player.forward * distanceFromPlayer;
menuUI.transform.forward = player.forward;
}
}下面是可以直接放入项目的完整类。它同时包含菜单、声音设置和风格切换功能;核对文件名、类名与大括号后,再统一保存并返回 Unity 编译。
下载完整 UIManager.cs
也可以下载完整文件放入 Assets/_Scripts,并替换项目中的同名脚本。
- 文件同时包含菜单、声音设置与风格切换功能。
- 文件名与类名都必须保持为 UIManager.cs 和 UIManager。
- 直接覆盖现有 UIManager.cs 的内容,不要先删除脚本资源;无需下载或替换 .meta 文件。
- 保存后等待 Unity 编译结束,再继续连接场景引用。
下载风格材质包
下载后保留文件名 StyleMat.unitypackage,在 Unity 项目窗口中通过 Import Package → Custom Package 导入。
- 导入前保存项目和场景。
- 导入列表中的黄色提示表示项目里存在同路径资源,导入会更新对应资源。
- 确认项目中没有需要保留的自定义同名修改后,再执行导入。
把材质包放入当前项目
- 01
在 Project 面板空白处右键,选择 Import Package → Custom Package。
- 02
选择下载的 StyleMat.unitypackage,等待 Unity 显示导入列表。
- 03
阅读同名资源提示,确认后点击 Import。
- 04
打开 Assets/_Materials/MuseumStyleMats。
- 05
确认 FloorStyleA、FloorStyleB、WallStyleA、WallStyleB 四个目录及其 .mat 材质存在。
四个材质的配套关系
A 与 A 配套,B 与 B 配套;地面材质不能拖进墙面数组。
左右滑动表格,可查看完整内容
| 风格 | 下拉序号 | 地面 | 墙面 |
|---|---|---|---|
| 现代简约 | 0 | FloorStyleA | WallStyleA |
| 复古怀旧 | 1 | FloorStyleB | WallStyleB |
关于 Substance 生成资源
部分贴图与材质可能由 Substance 工具生成。项目已经包含可直接使用的 .mat 材质时,即使不编辑生成参数,也能把这些材质拖入数组并正常显示。
只有在需要打开并调整 Substance 生成参数时,才需要为当前 Unity 版本安装相应的官方支持包。普通风格切换练习不依赖这一步,不要因为可选工具中断核心功能。
确认材质完整且配对清楚
在项目面板中分别选中四个 .mat 文件,观察预览并按风格成对记录。
- 找到两个 FloorStyle 材质。
- 找到两个 WallStyle 材质。
- 确认 A 对应现代简约、B 对应复古怀旧。
- 不要在这一阶段直接覆盖场景对象的材质槽。
数组数量和元素顺序共同决定最终效果
连接下拉列表、渲染器和四个材质
脚本编译完成后,UIManager 的风格字段会出现在属性面板。接下来把一个下拉列表、三个渲染器和四个材质连接到准确位置。把脚本引用连接到真实对象
- 01
确认控制台没有红色编译错误,再选择挂载 UIManager 的 Scripts 对象。
- 02
把 SettingsPage 下的 VisualStyleDropdown 拖到 Visual Style Dropdown。
- 03
把 Floor Renderers 的数量设为1,将 Floor 的 Mesh Renderer 放入 Element 0。
- 04
把 Wall Renderers 的数量设为2,依次放入 Wall 与 Wall2 的 Mesh Renderer。
- 05
把 Floor Materials 的数量设为2,Element 0 放 FloorStyleA,Element 1 放 FloorStyleB。
- 06
把 Wall Materials 的数量设为2,Element 0 放 WallStyleA,Element 1 放 WallStyleB。
- 07
确认 Floor、Wall 与 Wall2 都只使用一个需要切换的主材质槽。
- 08
保存场景,重新选择 Scripts 对象确认所有引用仍然存在。

观察重点:渲染器数组保存1个地面和2面墙;两组材质数组都按 A、B 顺序排列,元素编号与下拉选项编号一致。
点击图片可以查看完整截图绑定完成后的数量与顺序
运行前逐项核对;两个材质数组中相同的 Element 编号必须属于同一种风格。
左右滑动表格,可查看完整内容
| 字段 | 数量 | Element 0 | Element 1 |
|---|---|---|---|
| Visual Style Dropdown | 1个引用 | VisualStyleDropdown | — |
| Floor Renderers | 1 | Floor | — |
| Wall Renderers | 2 | Wall | Wall2 |
| Floor Materials | 2 | FloorStyleA | FloorStyleB |
| Wall Materials | 2 | WallStyleA | WallStyleB |
完成引用并说明每个 Element 的意义
连接后不要只看字段不为空,还要核对数量、对象类型和顺序。
- 确认 Visual Style Dropdown 指向 Dropdown 组件,而不是它的文字子对象。
- 确认 Renderer 数组显示的是 Mesh Renderer 组件。
- 确认两个材质数组的 Element 0 都是A,Element 1 都是B。
- 保存场景并确认控制台没有红色错误。
不能只看到一次变化,要确认初始、切换和恢复都正确
完成双向测试并定位常见问题
最终测试要覆盖两个选项、三个目标表面和首次进入场景的状态。这样才能确认监听、索引、数组顺序和材质赋值完整协作。先切换到复古怀旧
- 01
运行场景并打开设置页面,先观察下拉列表当前文字与场景外观是否一致。
- 02
选择“复古怀旧”,观察 Floor、Wall 和 Wall2 是否同时切换到B组材质。
- 03
在场景中转动视角,确认不同方向的两面墙都已经变化。

观察重点:设置菜单显示“复古怀旧”,墙面呈暖棕色,地面换成配套的浅色材质;三处表面属于同一套风格。
点击图片可以查看完整截图再切回现代并验证初始选择
- 01
选择“现代简约”,观察三个表面是否同时恢复A组材质。
- 02
停止运行,把 Dropdown 初始 value 改为1,再次运行,确认不点击列表也会直接应用复古风格。
- 03
恢复项目需要的默认选项并保存场景。
完整运行验证表
每一步都要同时观察下拉文字、地面、两面墙和原有功能,不能只看一个表面。
左右滑动表格,可查看完整内容
| 操作 | 应看到的结果 | 说明了什么 |
|---|---|---|
| 以 value=0 启动场景 | 文字与三个表面均为现代简约 | Start 已应用当前选项 |
| 选择复古怀旧 | Floor、Wall、Wall2 均使用B组材质 | 索引1正确分发到两组表面 |
| 从不同方向观察墙面 | Wall 与 Wall2 都已变化 | 墙面渲染器数组成员完整 |
| 切回现代简约 | 三个表面均恢复A组材质 | 索引0与数组顺序正确 |
| 把初始 value 改为1后重新运行 | 无需再次选择即可显示复古风格 | 初始同步链能够工作 |
| 连续往返切换两次 | 每次都同步变化且无报错 | 监听与材质赋值可以重复执行 |
| 操作菜单、音乐和解说 | 原有功能继续正常响应 | 各脚本职责没有互相干扰 |
常见现象与处理方法
从调用链由前向后定位,先看输入,再看数组,最后看材质。
左右滑动表格,可查看完整内容
| 现象 | 常见原因 | 处理方法 |
|---|---|---|
| 选择后完全没有变化 | Dropdown 引用为空,或监听没有成功建立 | 核对 Visual Style Dropdown、Awake 监听和控制台状态 |
| 属性面板看不到风格字段 | 脚本尚未编译成功,或完整字段没有放入类中 | 先处理控制台红色错误,再打开 Scripts 对象 |
| 只有地面变化 | 墙面 Renderer 或墙面材质数组为空 | 核对 Wall、Wall2 与 WallStyleA/B |
| 地面与墙面风格不一致 | 两个材质数组的 A、B 顺序不同 | 让两个 Element 0 都是A,两个 Element 1 都是B |
| 第二项没有变化 | 材质数组长度不足2,或 Element 1 为空 | 把两个材质数组长度设为2并补齐第二项 |
| 首次进入时文字与场景不一致 | Start 中没有应用 Dropdown 当前 value | 确认 ApplyVisualStyle(visualStyleDropdown.value) 存在 |
| 一个墙面仍保留旧材质 | Wall Renderers 少放了一个成员 | 确认数组长度为2且包含 Wall 与 Wall2 |
| 找不到 Dropdown 类型 | 文件顶部缺少 using UnityEngine.UI | 补上命名空间引用并重新保存脚本 |
| 运行后出现额外材质副本 | 赋值时使用了 material 而不是 sharedMaterial | 本课使用 sharedMaterial 切换现有材质引用 |
| 重新打开场景后引用消失 | 连接完成后没有保存场景 | 重新连接字段并保存当前场景 |
| 代码提示不存在 GameManager.ChangeStyle | 把风格逻辑写到了错误的脚本调用链 | 由 UIManager 直接接收选项并应用材质 |
| 增加第三种风格后出现混搭 | 下拉选项或某一个材质数组没有同步增加第3项 | 同时补齐选项、Floor Materials 与 Wall Materials 的 Element 2 |
完整执行顺序与状态变化
场景启动时,Awake 添加监听,随后 Start 读取 Dropdown 当前 value。ApplyVisualStyle 接到序号后连续调用两次 ApplyMaterial,第一遍更新地面,第二遍更新墙面。
这里的“同步”表示两组表面响应同一次选择,代码实际按先地面、后墙面的顺序依次执行。如果墙面引用无效,地面可能已经换好而墙面没有变化,因此运行时仍要观察全部三个表面。
每次 ApplyMaterial 先验证输入,再遍历 Renderer 数组。有效成员的 sharedMaterial 被替换为 materials[styleIndex]。方法结束后,场景已经呈现新风格,不需要在下一帧继续重复赋值。
用户以后改变选项时,onValueChanged 再次启动同一调用链。因为输入是新的序号,两个材质组会同步切换到另一个索引。
完成一次可说明原因的成果演示
演示时不只点击选项,还要指出输入、方法和三个最终目标。
- 说明网格、渲染器与材质分别负责什么。
- 说出 value=0 与 value=1 对应的两组完整材质。
- 说明 UIManager、GameManager 与 AudioManager 的职责区别。
- 解释五个风格字段分别保存什么。
- 按顺序复述 Awake、Start、用户选择与 OnDestroy 的职责。
- 展示现代简约与复古怀旧能够双向切换。
- 分别指出 Floor、Wall、Wall2 都发生变化。
- 解释空引用、越界、空材质和空渲染器分别如何处理。
- 解释 sharedMaterial 赋值为何不会改变模型位置和碰撞体。
- 确认控制台没有红色错误,菜单导航和两个声音开关仍然正常。
把知识变成作品
本课练习
请按顺序完成以下任务,并保存自己的学习成果。- 01
不查看代码,画出 VisualStyleDropdown 到 Floor、Wall 和 Wall2 的调用顺序。
- 02
说明 AddListener、Start 初始同步和 RemoveListener 分别解决什么问题。
- 03
把 wallMaterials 的两个成员临时交换,观察混搭结果后恢复,并解释索引对齐的重要性。
- 04
在 wallRenderers 中临时留空一个成员,观察其他成员是否仍能切换,然后恢复正确引用。
- 05
为第三种风格设计数组扩展方案:新增下拉选项,并在两组材质数组的 Element 2 放入配套材质。
- 06
完成一次现代简约 → 复古怀旧 → 现代简约的连续演示,并确认声音设置和菜单导航没有受到影响。
把本课进度保存下来
登录后可以保存浏览内容、有效学习时间、完成状态和答题参与记录。