具体设置选项的设置

创建预制体可以减少重复的选项设置,常用的设置选项通常分为以下几种

  • 左标签+右滑动槽
  • 左标签+右输入框
  • 左标签+右开关按钮
  • 左标签+右下拉列表
  • 左标签+右按钮

预制体设置

需要先在Day3中中完成的ContentPanel预制体进行一些修改

  • ContentVertical Layout Group组件中,找到Padding填充,为其添加适当的间距,这样不会紧贴画布边缘

左标签+右滑动槽

  1. 创建空对象Label_Slider
    1. 添加Text子物体,重命名为Label,配置自动切换字体
    2. 添加Slider子物体
    3. 添加Horizontal Layout Group组件,勾选Child Controls Size的 Width 和 Height,取消勾选Child Force Expand的 Width
  2. Slider组件中
    1. 添加Layout Element组件,勾选Preferred Width,设置为合适值
  3. Label组件中
    1. 添加Layout Element组件,勾选Flexible Width,设置为1(会占据剩余的所有空间)
  4. 完成预制体

左标签+右输入框

  1. 创建空对象Label_InputField
    1. 添加Text子物体,重命名为Label,配置自动切换字体
    2. 添加空对象子物体并重命名为Wrapper子物体
    3. 添加Horizontal Layout Group组件,勾选Child Controls Size的 Width 和 Height,取消勾选Child Force Expand的 Width
  2. Wrapper子物体中
    1. 添加Input Field子物体
    2. 添加Layout Element组件,勾选Preferred Width,设置为与左标签+右滑动槽相同的值,后续为了UI统一建议都调整为相同值
  3. Input Field
    1. 控制Rect Transform组件,调整输入框大小
    2. InputField->Text Area->Placeholder中可以设置默认显示内容,可以考虑在这里设置为居中
    3. InputField->Text Area->Text中可以设置用户输入的值,在此也要设置自动切换字体
  4. Label组件中
    1. 添加Layout Element组件,勾选Flexible Width,设置为1(会占据剩余的所有空间)
  5. 完成预制体

左标签+开关按钮

  1. 创建空对象Label_Toggle
    1. 添加Text子物体,重命名为Label,配置自动切换字体
    2. 添加Toggle子物体
    3. 添加Horizontal Layout Group组件,勾选Child Controls Size的 Width 和 Height,取消勾选Child Force Expand的 Width
  2. Toggle组件中
    1. 添加Layout Element组件,勾选Preferred Width,设置为合适值
    2. 删除Toggle自带的Label子物体
  3. Label组件中
    1. 添加Layout Element组件,勾选Flexible Width,设置为1(会占据剩余的所有空间)
  4. 完成预制体

左标签+下拉列表

  1. 创建空对象Label_Dropdown
    1. 添加Text子物体,设置为Label,配置自动切换字体
    2. 添加Wrapper子物体
    3. 添加Horizontal Layout Group组件,勾选Child Controls Size的 Width 和 Height,取消勾选Child Force Expand的 Width
  2. Wrapper子物体中
    1. 添加Dropdown子物体
    2. 添加Layout Element组件,勾选Preferred Width,设置为合适值
  3. Label组件中
    1. 添加Layout Element组件,勾选Flexible Width,设置为1(会占据剩余的所有空间)
  4. 完成预制体

左标签+右按钮

  1. 创建空对象Label_Button
    1. 添加Text子物体,重命名为Label,配置自动切换字体
    2. 添加空对象并重命名为Wrapper
    3. 添加Horizontal Layout Group组件,勾选Child Controls Size的 Width 和 Height,取消勾选Child Force Expand的 Width
  2. Wrapper子物体中
    1. 添加Button子物体
    2. 添加Layout Element组件,勾选Preferred Width,设置为合适值
  3. Label组件中
    1. 添加Layout Element组件,勾选Flexible Width,设置为1(会占据剩余的所有空间)
  4. 完成预制体

为具体设置编写脚本

切换语言

  1. 拖入Label_Dropdown 预制体到LanguagePanel->Content
  2. Dropdown物体的Dropdown-TextMeshPro组件中,添加Options,注意这里的顺序要与项目设置里的语言顺序一致
  3. Day3中中添加的SettingsManager脚本中添加以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Localization.Settings;
using TMPro;

public class SettingsManager : MonoBehaviour
{
private TMP_Dropdown languageDropdown;

private void Awake()
{
languageDropdown = LanguagePanel.transform.Find("Content/Language/Wrapper/Dropdown").GetComponent<TMP_Dropdown>();
languageDropdown.onValueChanged.AddListener(OnLanguageChanged);
}

private void OnLanguageChanged(int index)
{
if (LocalizationSettings.InitializationOperation.IsDone)
{
LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.Locales[index];
}
}
}

按键绑定

默认的控制方式可能不是用户习惯的,所以需要添加按键绑定功能

  1. 左标签+右按钮的预制体作为按键绑定的对象
  2. 准备Excel表格规划按键,记录按键的id名称默认按键
  3. 新建KeybindManager脚本挂在在Canvas_Settings_Menu上,并参考下述代码实现按键的记录,先简单阐述这一段逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*
此文件专门用于处理用户自定义按键绑定:其逻辑为
1. 在游戏加载时尝试用 SafeLoadKeybind 去加载每一个键位,在 Awake 中传入固定的默认键位
2. 在每一个键位的设置中,尝试读取用户曾经的自定义键位,若出现了错误,则使用 Awake 中传入的默认键位,否则使用用户自定义键位,这样保证了游戏的高容错
3. 对于组合键,通过 CustomKeyBind 结构体的 RequireCtrl, RequireShift, RequireAlt 字段来判断是否需要同时按下这些键,从而实现组合键的功能
4. 对于UI中所需要呈现的内容,通过 CustomKeyBind 结构体的 ToString 方法来将其转化为字符串,例如 "Ctrl + Shift + W"
5. 对于用户输入,通过 GetActionDown 严格判断是否按下了正确的键位
*/
using System.Collections.Generic;
using UnityEngine;

// 组合键数据结构
[System.Serializable]
public struct CustomKeyBind
{
public KeyCode MainKey; // 主键 (例如 W, Mouse0, Escape)
public bool RequireCtrl; // 是否需要同时按下 Ctrl 键
public bool RequireShift; // 是否需要同时按下 Shift 键
public bool RequireAlt; // 是否需要同时按下 Alt 键

// 将结构体转化为 UI 显示的字符串 (用于在设置面板中展示给玩家看)
public override string ToString()
{
string s = "";
if (RequireCtrl) s += "Ctrl + ";
if (RequireShift) s += "Shift + ";
if (RequireAlt) s += "Alt + ";
s += MainKey.ToString(); // 组合结果例如: "Ctrl + Shift + W"
return s;
}

// 存入硬盘时的序列化格式 (去掉空格以节省空间,例如: "Ctrl+W", "Mouse0")
public string Serialize()
{
string s = "";
if (RequireCtrl) s += "Ctrl+";
if (RequireShift) s += "Shift+";
if (RequireAlt) s += "Alt+";
s += MainKey.ToString();
return s;
}

// 从硬盘读取时的反序列化解析器 (将 "Ctrl+W" 还原成 CustomKeyBind 结构体)
public static CustomKeyBind Parse(string savedStr)
{
CustomKeyBind bind = new CustomKeyBind();

// 1. 判断字符串中是否包含特定修饰键的标识
bind.RequireCtrl = savedStr.Contains("Ctrl+");
bind.RequireShift = savedStr.Contains("Shift+");
bind.RequireAlt = savedStr.Contains("Alt+");

// 2. 提取主键:通过 '+' 分割字符串,最后一部分必然是主键
string[] parts = savedStr.Split('+');
string keyStr = parts[parts.Length - 1];

// 3. 安全地将字符串转换为 KeyCode 枚举类型
// 如果转换成功,赋值给 MainKey;如果失败(比如存档被篡改),则赋值为 None
/*
1. System.Enum.TryParse:是 C# 自带的一个尝试转换函数。假设 keyStr 里的文本是 "W",它会去 KeyCode 里找有没有叫 W 的按键
2. out KeyCode parsedKey:这是 C# 的 out 语法。如果转换成功,它会当场创建一个名叫 parsedKey 的变量,并把转换后的结果(KeyCode.W)塞进去
3. if 判断:TryParse 会返回一个布尔值。如果字符串是乱码(比如 "Hello"),找不到对应的按键,它就返回 false,这样就能防止游戏报错崩溃;如果找到了,返回 true,接着执行 bind.MainKey = parsedKey;
*/
if (System.Enum.TryParse(keyStr, out KeyCode parsedKey))
bind.MainKey = parsedKey;
else
bind.MainKey = KeyCode.None;

return bind;
}
}

public class KeybindManager : MonoBehaviour
{
// 单例模式,方便全局随时调用 (例如 KeybindManager.Instance.GetActionDown("Attack"))
public static KeybindManager Instance { get; private set; }

// 核心字典:使用动作名称 (如 "MoveUp") 作为 Key,对应的按键配置作为 Value,存在:游戏数据Excel->Keybind 中
public Dictionary<string, CustomKeyBind> Keybinds { get; private set; }

private void Awake()
{
// 标准单例初始化逻辑:确保场景中只有一个 KeybindManager
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
// 注:DontDestroyOnLoad 在 SettingsManager 里做过了,所以这里省略

Keybinds = new Dictionary<string, CustomKeyBind>();
LoadKeybinds(); // 游戏启动时加载所有按键配置
}

// 注册并加载游戏中所有的默认操作键位
private void LoadKeybinds()
{
SafeLoadKeybind("MoveUp", "W");
SafeLoadKeybind("MoveDown", "S");
SafeLoadKeybind("MoveLeft", "A");
SafeLoadKeybind("MoveRight", "D");
SafeLoadKeybind("Attack", "Mouse0"); // 鼠标左键
SafeLoadKeybind("Skill", "Ctrl+Mouse1"); // Ctrl + 鼠标右键
SafeLoadKeybind("Menu", "Escape"); // 绑定ESC
}

// 安全加载机制:负责从 PlayerPrefs 读取存档,并处理存档损坏的情况
private void SafeLoadKeybind(string actionName, string defaultKeyString)
{
// 使用Unity引擎自带的PlayerPrefs去查查是否有存过的数据,尝试获取玩家自定义的按键字符串,如果没有,则使用传入的默认值 (defaultKeyString)
string savedKeyString = PlayerPrefs.GetString(actionName, defaultKeyString);
CustomKeyBind bind = CustomKeyBind.Parse(savedKeyString);

// 容错处理:如果解析出来连主键都没有(例如玩家修改注册表填了个乱码),强行重置为默认键位
if (bind.MainKey == KeyCode.None)
{
Debug.LogWarning($"[{actionName}] 存档已损坏,重置为: {defaultKeyString}");
bind = CustomKeyBind.Parse(defaultKeyString);
}

// 将最终合法的按键存入字典
Keybinds[actionName] = bind;
}

// 供 UI 设置面板调用的方法:当玩家在设置里修改了按键后,调用此方法更新并保存
public void BindKey(string actionName, CustomKeyBind newBind)
{
Keybinds[actionName] = newBind; // 更新内存字典
PlayerPrefs.SetString(actionName, newBind.Serialize()); // 存入硬盘缓存
PlayerPrefs.Save(); // 强制写入硬盘
}

// 获取某个动作是否在【当前帧被按下】 (相当于 Input.GetKeyDown)
public bool GetActionDown(string actionName)
{
// 如果字典里找不到这个动作,直接返回 false
if (!Keybinds.TryGetValue(actionName, out CustomKeyBind bind)) return false;

// 1. 获取当前玩家实际上按下了哪些修饰键 (区分左右键)
bool ctrlHeld = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
bool shiftHeld = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
bool altHeld = Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);

// 2. 严格校验修饰键状态 (互斥校验)
// 例如:设置里要求按 Ctrl,但玩家没按,或者设置里没要求按 Ctrl,但玩家按了,都不算触发。
if (bind.RequireCtrl != ctrlHeld) return false;
if (bind.RequireShift != shiftHeld) return false;
if (bind.RequireAlt != altHeld) return false;

// 3. 最后检查主按键是否在当前帧被按下
return Input.GetKeyDown(bind.MainKey);
}

// 获取某个动作是否【处于持续被按住的状态】 (相当于 Input.GetKey)
public bool GetAction(string actionName)
{
if (!Keybinds.TryGetValue(actionName, out CustomKeyBind bind)) return false;

// 修饰键校验逻辑同上
bool ctrlHeld = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
bool shiftHeld = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
bool altHeld = Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);

if (bind.RequireCtrl != ctrlHeld) return false;
if (bind.RequireShift != shiftHeld) return false;
if (bind.RequireAlt != altHeld) return false;

// 检查主键是否被按住
return Input.GetKey(bind.MainKey);
}
}
  1. 新建KeybindUIItem脚本放在按钮预制体的根节点下,并参考下述代码实现预制体中的信息读取、硬盘中已存数据读取、接收用户输入功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/*
此脚本专门用于处理用户在设置菜单中点击绑定按键的操作
1. 使用全局锁确保同一时间只有一个按钮在监听玩家输入,在StartListening中处理此逻辑
2. 在OnGUI中按照此逻辑处理用户的绑定
1. 是否是鼠标按键KeyDown,按下时是否有ctrl、shift、alt,监听到第一个主键就完成绑定,不监听KeyUp
2. 是否是键盘按键KeyDown
- 如果按下的是ctrl、shift、alt,则单纯更新按钮里面的文字,暂时不结束
- 如果是其余键,则记录此时ctrl、shift、alt的状态,以及本次按下的键位,完成绑定
3. 如果监听到KeyUp,则说明玩家一直没有按下主键,用IsModifierKey判断是否是修饰键,是则绑定
- 但是这里有个问题,玩家无法绑定ctrl+shift这种没有主键的两个ModifierKey构成的组合键,但是考虑到一般不会有玩家这样使用
*/
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.Localization;
using UnityEngine.EventSystems; // 引入事件系统以处理焦点

public class KeybindUIItem : MonoBehaviour
{
[Header("绑定设置")]
public string actionName;
// 脚本需要挂在在按钮预制体的根节点,在每一个预制体的脚本处都要准确无误的填写这个按键的名称
// 动作名称,例如 "MoveUp",必须与 KeybindManager 里的名称对应,也可以查Excel表

[Header("UI 组件引用")]
public TextMeshProUGUI labelText; // 左侧显示的文字,比如 "向前移动"
public Button bindButton; // 右侧供玩家点击的按钮
public TextMeshProUGUI buttonText; // 按钮上显示的当前按键,比如 "W" 或 "等待输入..."

[Header("多语言文本配置")]
public LocalizedString waitingInputString; // 多语言支持的 "等待输入..." 文本

private bool isWaitingForInput = false; // 当前这个按钮是否正在监听玩家绑定新按键

// 【全局互斥锁】static 意味着所有 KeybindUIItem 实例共享这一个变量
// 用来确保同一时间绝对只有一个按钮处于 "等待输入" 状态
private static KeybindUIItem currentActiveItem = null;

private void Start()
{
// 游戏启动时,给按钮挂载点击事件
bindButton.onClick.AddListener(StartListening);
// 初始化按钮上显示的文字 (去 Manager 里查当前绑定的什么键)
UpdateButtonText();
}

// 玩家点击了按钮,开始监听输入
private void StartListening()
{
// 1. 互斥逻辑:如果有其他按钮正在等待输入,强行打断它,让它恢复原状
if (currentActiveItem != null && currentActiveItem != this)
{
currentActiveItem.CancelListening();
}

// 2. 将全局锁交给自己,向全世界宣布:“现在轮到我接管输入了!”
currentActiveItem = this;

// 3. 剥夺当前 UI 焦点 (极其重要)
// 防止玩家按下 Space(空格) 或 Enter(回车) 时,Unity 默认再次触发按钮点击事件
if (EventSystem.current != null)
{
EventSystem.current.SetSelectedGameObject(null);
}

// 4. 进入等待状态,并更新 UI 提示玩家
isWaitingForInput = true;
buttonText.text = waitingInputString.GetLocalizedString();
}

// 供其他按钮抢夺锁时调用(被迫停止监听)
public void CancelListening()
{
isWaitingForInput = false;
UpdateButtonText(); // 取消监听,把文字恢复成原来的按键名
}

// OnGUI 每一帧可能会执行多次,专门用来捕获底层的系统事件 (Event)
private void OnGUI()
{
// 如果当前没在等待玩家输入,直接跳过,什么都不做
if (!isWaitingForInput) return;

Event e = Event.current; // 获取当前正在发生的事件(比如鼠标移动、键盘按下)

// 【处理鼠标按键】
// 如果事件是鼠标事件,且是按下(MouseDown)
if (e.isMouse && e.type == EventType.MouseDown)
{
CustomKeyBind newBind = new CustomKeyBind
{
RequireCtrl = e.control, // 判断鼠标按下的同时,有没有按住 Ctrl
RequireShift = e.shift,
RequireAlt = e.alt,
// 神奇的枚举加法:KeyCode.Mouse0 的底层数字是 323
// e.button 返回 0(左键), 1(右键), 2(中键)
// Mouse0 + 1 就变成了 KeyCode.Mouse1,完美映射!
MainKey = KeyCode.Mouse0 + e.button
};

FinishBinding(newBind); // 完成绑定
e.Use(); // e.Use() 告诉系统:“这个事件我吃掉了,不要再往后传给游戏里的开枪或UI逻辑了”
return;
}

// 【处理键盘按键】
// isKey 代表是键盘事件,keyCode != None 排除掉一些无效的幽灵输入
if (e.isKey && e.keyCode != KeyCode.None)
{
// 阶段 1:按键按下的瞬间 (KeyDown)
if (e.type == EventType.KeyDown)
{
// 如果玩家按下的是修饰键 (比如只按下了 Ctrl)
if (IsModifierKey(e.keyCode))
{
string tempStr = "";
if (e.control) tempStr += "Ctrl + ";
if (e.shift) tempStr += "Shift + ";
if (e.alt) tempStr += "Alt + ";

// 不结束绑定,而是实时更新 UI,让按钮显示 "Ctrl + ",提示玩家继续按下一个主键
buttonText.text = tempStr;
return;
}

// 如果按下的不是修饰键(也就是按下了普通主键,比如 W, 空格, 回车)
CustomKeyBind newBind = new CustomKeyBind
{
RequireCtrl = e.control,
RequireShift = e.shift,
RequireAlt = e.alt,
MainKey = e.keyCode
};

FinishBinding(newBind);
e.Use();
}
// 阶段 2:按键抬起的瞬间 (KeyUp)
// 专门为了处理玩家“仅仅想把动作绑定在单独的修饰键上”的情况 (比如我想按 Shift 翻滚,不按别的)
else if (e.type == EventType.KeyUp)
{
// 如果玩家松开了一个修饰键 (说明他没有按其他主键就松手了)
if (IsModifierKey(e.keyCode))
{
CustomKeyBind newBind = new CustomKeyBind
{
// 因为修饰键本身变成了主键,所以不再需要组合键属性,全部设为 false
RequireCtrl = false,
RequireShift = false,
RequireAlt = false,
MainKey = e.keyCode // 把 Shift 或 Ctrl 本身作为主键存进去
};

FinishBinding(newBind);
e.Use();
}
}
}
}

// 完成绑定并善后
private void FinishBinding(CustomKeyBind bind)
{
// 1. 将新的按键配置推送到 Manager 保存到硬盘
KeybindManager.Instance.BindKey(actionName, bind);

// 2. 退出监听状态
isWaitingForInput = false;

// 3. 释放全局锁 (如果锁还在自己手上的话)
if (currentActiveItem == this)
{
currentActiveItem = null;
}

// 4. 更新按钮显示的文字
UpdateButtonText();
}

// 更新按钮文字,向 Manager 索要当前动作对应的字符描述
private void UpdateButtonText()
{
if (KeybindManager.Instance.Keybinds.ContainsKey(actionName))
{
buttonText.text = KeybindManager.Instance.Keybinds[actionName].ToString();
}
}

// 工具方法:判断传入的按键是否是修饰键 (区分左右)
private bool IsModifierKey(KeyCode code)
{
return code == KeyCode.LeftControl || code == KeyCode.RightControl ||
code == KeyCode.LeftShift || code == KeyCode.RightShift ||
code == KeyCode.LeftAlt || code == KeyCode.RightAlt ||
code == KeyCode.LeftCommand || code == KeyCode.RightCommand; // Command 是 Mac 系统的按键
}
}
  1. 完善翻译问题

用Excel解决翻译问题

可以将Localization Tables导出为Excel表格,从而完成翻译工作

  1. Window->Asset Management Tools->Localization Tables
  2. 切换到String Table,点击右上角的小三点,选择**Export->CSV…**选择导出路径
  3. 打开导出的Excel表格,即可开始翻译,翻译完成后再重新导入即可