3. 配置管理与本地化
配置与本地化
假设你的游戏做完了,中国玩家玩得很开心。这时候一个外国朋友也想玩,打开一看全是中文——他看不懂。又或者一个玩家觉得背景音乐太吵了,想调小一点,却发现找不到设置按钮。
本地化就是让你的游戏支持多种语言。配置管理就是让玩家能自定义游戏体验(音量、画质、操作方式等)。这两个功能看起来不起眼,但对于一个"正规"的游戏来说,它们是标配。
为什么需要本地化
想象一下,你开发了一款超级好玩的游戏,想卖到全世界。全球有大约 78 亿人,其中母语是英语的大约 15 亿,中文大约 13 亿——剩下的 50 亿人,说的可能是西班牙语、阿拉伯语、日语、韩语、法语、德语……
如果你的游戏只支持中文,你就只能赚中文玩家的钱。如果你的游戏支持 10 种语言,你的潜在玩家就扩大了好几倍。
一组数据
- 支持 English + 中文的游戏,可以覆盖全球约 37% 的互联网用户
- 再加上 Spanish + Arabic,可以覆盖约 55%
- 如果覆盖 10 种主要语言,可以触达约 80% 的全球玩家
本地化不只是翻译文字那么简单,还包括:
| 方面 | 举例 |
|---|---|
| 文字翻译 | 菜单、对话框、物品描述、UI 提示 |
| 图片替换 | 含文字的图片(比如写着"START"的按钮) |
| 音频替换 | 语音对话、配音 |
| 文化适应 | 颜色含义、符号含义、节日活动 |
| 排版适配 | 阿拉伯语从右往左写、德语单词特别长 |
Godot 的 Translation 系统
Godot 内置了一套完整的翻译系统,使用起来非常简单。它的核心思路是:代码里只写一个"键名"(key),然后根据当前语言去翻译表里查找对应的文字。
打个比方:你在餐厅点菜,菜单上写的是菜名编号("第 42 号"),厨房拿到编号后去菜谱里查——如果是中文菜单就做宫保鸡丁,如果是英文菜单就做 Kung Pao Chicken。
TranslationServer —— Godot 的翻译大管家
Godot 提供了一个全局单例 TranslationServer,它负责管理所有翻译资源。你可以这样使用它:
# 获取当前语言
var current_lang = TranslationServer.get_locale() # 比如 "zh_CN"
# 设置语言
TranslationServer.set_locale("en_US")
# 翻译一段文字(tr 是 translate 的缩写)
var text = tr("hello_world") # 如果当前是中文,返回 "你好世界"tr() 函数是 Godot 里最常用的翻译方法。它会自动根据当前语言查找翻译表,返回对应的文字。如果找不到翻译,就原样返回键名。
注意
tr() 函数只能在继承自 Object 的类里使用(也就是几乎所有的 Godot 节点)。如果你在一个纯 C# 静态类里需要翻译,需要使用 TranslationServer.Translate("key")。
在场景中使用 tr()
你可以在 Godot 编辑器里直接给 Label、Button 等控件的文本设置翻译键:
- 选中一个 Label 节点
- 在 Inspector 面板里找到
Text属性 - 点击文本输入框旁边的"翻译"图标(或者勾选
Autowrap旁边的选项) - 在弹出的对话框中输入翻译键名
运行时,如果当前语言有对应的翻译,Label 就会显示翻译后的文字。
CSV 翻译文件——最简单的翻译管理方式
Godot 支持多种翻译文件格式,其中 CSV(逗号分隔值)是最直观的——你可以用 Excel 或者任何文本编辑器打开。
CSV 文件格式
创建一个翻译文件 text_zh_CN.translation,内容如下:
keys,zh_CN
hello_world,你好世界
start_game,开始游戏
settings,设置
quit_game,退出游戏
confirm,确认
cancel,取消
inventory,背包
player_hp,生命值
player_attack,攻击力
game_over,游戏结束
you_win,你赢了!再创建一个 text_en_US.translation:
keys,en_US
hello_world,Hello World
start_game,Start Game
settings,Settings
quit_game,Quit Game
confirm,Confirm
cancel,Cancel
inventory,Inventory
player_hp,HP
player_attack,Attack
game_over,Game Over
you_win,You Win!在项目设置中注册翻译文件
- 打开 Project -> Project Settings -> Localization
- 在
Translation列表中添加你的翻译文件 - 设置默认语言
或者通过代码动态加载:
using Godot;
public partial class LocalizationManager : Node
{
// 加载所有可用的翻译文件
public void LoadTranslations()
{
// 中文翻译
var zhTranslation = GD.Load<Translation>("res://translations/text_zh_CN.translation");
if (zhTranslation != null)
{
TranslationServer.AddTranslation(zhTranslation);
}
// 英文翻译
var enTranslation = GD.Load<Translation>("res://translations/text_en_US.translation");
if (enTranslation != null)
{
TranslationServer.AddTranslation(enTranslation);
}
}
// 切换语言
public void SetLanguage(string locale)
{
TranslationServer.SetLocale(locale);
GD.Print($"语言已切换为:{locale}");
// 保存语言设置
ConfigFile config = new ConfigFile();
config.SetValue("settings", "language", locale);
config.Save("user://settings.cfg");
}
// 获取所有可用语言
public string[] GetAvailableLanguages()
{
return TranslationServer.GetLoadedLocales();
}
}extends Node
# 加载所有可用的翻译文件
func load_translations() -> void:
# 中文翻译
var zh_translation = load("res://translations/text_zh_CN.translation")
if zh_translation:
TranslationServer.add_translation(zh_translation)
# 英文翻译
var en_translation = load("res://translations/text_en_US.translation")
if en_translation:
TranslationServer.add_translation(en_translation)
print("已加载 %d 种语言" % TranslationServer.get_loaded_locales().size())
# 切换语言
func set_language(locale: String) -> void:
TranslationServer.set_locale(locale)
print("语言已切换为:%s" % locale)
# 保存语言设置到配置文件
var config = ConfigFile.new()
config.set_value("settings", "language", locale)
config.save("user://settings.cfg")
# 获取所有可用语言
func get_available_languages() -> PackedStringArray:
return TranslationServer.get_loaded_locales()动态切换语言——带代码示例
当玩家在设置界面选择了新语言后,你需要做两件事:
- 调用
TranslationServer.SetLocale()切换语言 - 更新界面上所有已显示的文字
问题是:Godot 的 tr() 只在第一次调用时获取翻译。如果 Label 的文字已经设好了,你切换语言后它不会自动更新。你需要手动刷新。
using Godot;
using System;
public partial class LanguageOption : OptionButton
{
private struct LanguageInfo
{
public string Locale { get; set; }
public string DisplayName { get; set; }
}
private LanguageInfo[] _languages = new[]
{
new LanguageInfo { Locale = "zh_CN", DisplayName = "简体中文" },
new LanguageInfo { Locale = "en_US", DisplayName = "English" },
new LanguageInfo { Locale = "ja_JP", DisplayName = "日本語" },
new LanguageInfo { Locale = "ko_KR", DisplayName = "한국어" }
};
public override void _Ready()
{
// 填充下拉菜单选项
foreach (var lang in _languages)
{
AddItem(lang.DisplayName);
}
// 选中当前语言
string currentLocale = TranslationServer.GetLocale();
for (int i = 0; i < _languages.Length; i++)
{
if (_languages[i].Locale == currentLocale)
{
Selected = i;
break;
}
}
// 监听选择变化
ItemSelected += OnLanguageSelected;
}
private void OnLanguageSelected(long index)
{
string newLocale = _languages[(int)index].Locale;
TranslationServer.SetLocale(newLocale);
// 通知场景树中所有节点刷新翻译
GetTree().Root.PropagateNotification((int)Node.NotificationTranslationChanged);
GD.Print($"语言切换为:{_languages[(int)index].DisplayName}");
}
}extends OptionButton
var _languages := [
{"locale": "zh_CN", "display_name": "简体中文"},
{"locale": "en_US", "display_name": "English"},
{"locale": "ja_JP", "display_name": "日本語"},
{"locale": "ko_KR", "display_name": "한국어"}
]
func _ready() -> void:
# 填充下拉菜单选项
for lang in _languages:
add_item(lang["display_name"])
# 选中当前语言
var current_locale := TranslationServer.get_locale()
for i in range(_languages.size()):
if _languages[i]["locale"] == current_locale:
selected = i
break
# 监听选择变化
item_selected.connect(_on_language_selected)
func _on_language_selected(index: int) -> void:
var new_locale: String = _languages[index]["locale"]
TranslationServer.set_locale(new_locale)
# 通知场景树中所有节点刷新翻译
# 这会触发所有节点的 _notification(NOTIFICATION_TRANSLATION_CHANGED)
get_tree().root.propagate_notification(NOTIFICATION_TRANSLATION_CHANGED)
print("语言切换为:%s" % _languages[index]["display_name"])让 Label 自动响应语言切换
如果你有自定义的 Label 节点,可以重写 _Notification 方法来响应语言切换:
using Godot;
public partial class TranslatedLabel : Label
{
[Export] public string TranslationKey { get; set; } = "";
public override void _Ready()
{
if (!string.IsNullOrEmpty(TranslationKey))
{
Text = Tr(TranslationKey);
}
}
public override void _Notification(int what)
{
if (what == (int)Node.NotificationTranslationChanged)
{
// 语言切换时自动更新文字
if (!string.IsNullOrEmpty(TranslationKey))
{
Text = Tr(TranslationKey);
}
}
}
}extends Label
@export var translation_key: String = ""
func _ready() -> void:
if translation_key != "":
text = tr(translation_key)
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSLATION_CHANGED:
# 语言切换时自动更新文字
if translation_key != "":
text = tr(translation_key)字体处理——多语言字体加载
不同语言需要不同的字体文件。比如中文字体需要包含成千上万个汉字,而英文字体只需要几十个字母和符号。如果你用英文字体来显示中文,看到的将是一堆方块。
为每种语言准备字体
using Godot;
public partial class FontManager : Node
{
// 不同语言的字体映射
private readonly Dictionary<string, string> _fontMap = new()
{
{ "zh_CN", "res://fonts/NotoSansSC-Regular.ttf" },
{ "zh_Hant", "res://fonts/NotoSansTC-Regular.ttf" },
{ "ja_JP", "res://fonts/NotoSansJP-Regular.ttf" },
{ "ko_KR", "res://fonts/NotoSansKR-Regular.ttf" },
{ "en_US", "res://fonts/Roboto-Regular.ttf" },
{ "default", "res://fonts/Roboto-Regular.ttf" }
};
// 根据当前语言获取合适的字体
public Font GetFontForCurrentLanguage(int fontSize = 16)
{
string locale = TranslationServer.GetLocale();
string fontPath = _fontMap.GetValueOrDefault(locale, _fontMap["default"]);
var fontFile = GD.Load<FontFile>(fontPath);
if (fontFile == null)
{
GD.PrintErr($"字体文件加载失败:{fontPath}");
return ThemeDB.FallbackFont;
}
return fontFile;
}
// 应用字体到整个场景
public void ApplyFontToScene(Node rootNode, int fontSize = 16)
{
Font font = GetFontForCurrentLanguage(fontSize);
// 遍历所有子节点,找到 Label 和 Button
foreach (Node child in rootNode.GetChildren())
{
if (child is Label label)
{
label.AddThemeFontOverride("font", font);
label.AddThemeFontSizeOverride("font_size", fontSize);
}
else if (child is Button button)
{
button.AddThemeFontOverride("font", font);
button.AddThemeFontSizeOverride("font_size", fontSize);
}
// 递归处理子节点
ApplyFontToScene(child, fontSize);
}
}
}extends Node
# 不同语言的字体映射
var _font_map := {
"zh_CN": "res://fonts/NotoSansSC-Regular.ttf",
"zh_Hant": "res://fonts/NotoSansTC-Regular.ttf",
"ja_JP": "res://fonts/NotoSansJP-Regular.ttf",
"ko_KR": "res://fonts/NotoSansKR-Regular.ttf",
"en_US": "res://fonts/Roboto-Regular.ttf",
"default": "res://fonts/Roboto-Regular.ttf"
}
# 根据当前语言获取字体
func get_font_for_current_language(font_size: int = 16) -> Font:
var locale := TranslationServer.get_locale()
var font_path: String = _font_map.get(locale, _font_map["default"])
var font_file = load(font_path) as FontFile
if not font_file:
push_error("字体文件加载失败:%s" % font_path)
return ThemeDB.fallback_font
return font_file
# 应用字体到整个场景
func apply_font_to_scene(root_node: Node, font_size: int = 16) -> void:
var font := get_font_for_current_language(font_size)
for child in root_node.get_children():
if child is Label:
child.add_theme_font_override("font", font)
child.add_theme_font_size_override("font_size", font_size)
elif child is Button:
child.add_theme_font_override("font", font)
child.add_theme_font_size_override("font_size", font_size)
# 递归处理子节点
apply_font_to_scene(child, font_size)推荐的开源字体
- Noto Sans(Google 出品):覆盖几乎所有语言,中、日、韩、阿拉伯语都有
- 思源黑体 / 思源宋体:中文排版效果很好
- 字体文件可能比较大(中文字体通常 5-20MB),注意控制包体大小
游戏设置管理——音量、画质、键位
除了语言,游戏设置还包括音量、画面质量、按键映射等。Godot 提供了 ConfigFile 类来方便地管理这些设置。
ConfigFile 基础
ConfigFile 是一种 INI 格式的配置文件,结构很简单:
[settings]
language = zh_CN
master_volume = 0.8
music_volume = 0.5
sfx_volume = 0.7
[graphics]
fullscreen = true
resolution_x = 1920
resolution_y = 1080
quality = 2
vsync = true设置管理器完整实现
using Godot;
public partial class SettingsManager : Node
{
// ========== 设置数据 ==========
public class GameSettings
{
// 音频设置
public float MasterVolume { get; set; } = 0.8f;
public float MusicVolume { get; set; } = 0.5f;
public float SfxVolume { get; set; } = 0.7f;
// 画面设置
public bool Fullscreen { get; set; } = false;
public int ResolutionX { get; set; } = 1920;
public int ResolutionY { get; set; } = 1080;
public int Quality { get; set; } = 2; // 0=低, 1=中, 2=高
public bool Vsync { get; set; } = true;
// 游戏设置
public string Language { get; set; } = "zh_CN";
}
private const string SettingsPath = "user://settings.cfg";
public GameSettings CurrentSettings { get; private set; } = new();
public override void _Ready()
{
LoadSettings();
ApplySettings();
}
// 加载设置
public void LoadSettings()
{
var config = new ConfigFile();
Error err = config.Load(SettingsPath);
if (err != Error.Ok)
{
GD.Print("没有找到设置文件,使用默认设置");
return;
}
// 读取音频设置
CurrentSettings.MasterVolume = (float)config.GetValue("audio", "master_volume", 0.8f);
CurrentSettings.MusicVolume = (float)config.GetValue("audio", "music_volume", 0.5f);
CurrentSettings.SfxVolume = (float)config.GetValue("audio", "sfx_volume", 0.7f);
// 读取画面设置
CurrentSettings.Fullscreen = (bool)config.GetValue("graphics", "fullscreen", false);
CurrentSettings.ResolutionX = (int)config.GetValue("graphics", "resolution_x", 1920);
CurrentSettings.ResolutionY = (int)config.GetValue("graphics", "resolution_y", 1080);
CurrentSettings.Quality = (int)config.GetValue("graphics", "quality", 2);
CurrentSettings.Vsync = (bool)config.GetValue("graphics", "vsync", true);
// 读取语言设置
CurrentSettings.Language = (string)config.GetValue("game", "language", "zh_CN");
GD.Print("设置加载完成");
}
// 保存设置
public void SaveSettings()
{
var config = new ConfigFile();
config.SetValue("audio", "master_volume", CurrentSettings.MasterVolume);
config.SetValue("audio", "music_volume", CurrentSettings.MusicVolume);
config.SetValue("audio", "sfx_volume", CurrentSettings.SfxVolume);
config.SetValue("graphics", "fullscreen", CurrentSettings.Fullscreen);
config.SetValue("graphics", "resolution_x", CurrentSettings.ResolutionX);
config.SetValue("graphics", "resolution_y", CurrentSettings.ResolutionY);
config.SetValue("graphics", "quality", CurrentSettings.Quality);
config.SetValue("graphics", "vsync", CurrentSettings.Vsync);
config.SetValue("game", "language", CurrentSettings.Language);
config.Save(SettingsPath);
GD.Print("设置保存完成");
}
// 应用设置到游戏
public void ApplySettings()
{
// 应用音量设置
var busIndex = AudioServer.GetBusIndex("Master");
AudioServer.SetBusVolumeDb(busIndex, Mathf.LinearToDb(CurrentSettings.MasterVolume));
var musicIndex = AudioServer.GetBusIndex("Music");
AudioServer.SetBusVolumeDb(musicIndex, Mathf.LinearToDb(CurrentSettings.MusicVolume));
var sfxIndex = AudioServer.GetBusIndex("Sfx");
AudioServer.SetBusVolumeDb(sfxIndex, Mathf.LinearToDb(CurrentSettings.SfxVolume));
// 应用画面设置
if (CurrentSettings.Fullscreen)
{
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Fullscreen);
}
else
{
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
}
DisplayServer.WindowSetSize(new Vector2I(CurrentSettings.ResolutionX, CurrentSettings.ResolutionY));
if (CurrentSettings.Vsync)
{
DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Enabled);
}
else
{
DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled);
}
// 应用语言设置
TranslationServer.SetLocale(CurrentSettings.Language);
GD.Print("设置已应用到游戏");
}
// 设置主音量
public void SetMasterVolume(float volume)
{
CurrentSettings.MasterVolume = Mathf.Clamp(volume, 0f, 1f);
var busIndex = AudioServer.GetBusIndex("Master");
AudioServer.SetBusVolumeDb(busIndex, Mathf.LinearToDb(CurrentSettings.MasterVolume));
SaveSettings();
}
// 设置音乐音量
public void SetMusicVolume(float volume)
{
CurrentSettings.MusicVolume = Mathf.Clamp(volume, 0f, 1f);
var busIndex = AudioServer.GetBusIndex("Music");
AudioServer.SetBusVolumeDb(busIndex, Mathf.LinearToDb(CurrentSettings.MusicVolume));
SaveSettings();
}
// 设置画面质量
public void SetQuality(int quality)
{
CurrentSettings.Quality = Mathf.Clamp(quality, 0, 2);
// 根据画质等级调整渲染设置
int[] msaaLevels = { 0, 2, 4 }; // 关闭, 2x, 4x 抗锯齿
GetViewport().Msaa2D = (Viewport.Msaa)msaaLevels[CurrentSettings.Quality];
SaveSettings();
}
}extends Node
const SettingsPath := "user://settings.cfg"
# 设置数据(用字典存储)
var current_settings := {
# 音频设置
"master_volume": 0.8,
"music_volume": 0.5,
"sfx_volume": 0.7,
# 画面设置
"fullscreen": false,
"resolution_x": 1920,
"resolution_y": 1080,
"quality": 2, # 0=低, 1=中, 2=高
"vsync": true,
# 游戏设置
"language": "zh_CN"
}
func _ready() -> void:
load_settings()
apply_settings()
# 加载设置
func load_settings() -> void:
var config := ConfigFile.new()
var err := config.load(SettingsPath)
if err != OK:
print("没有找到设置文件,使用默认设置")
return
# 读取音频设置
current_settings["master_volume"] = config.get_value("audio", "master_volume", 0.8)
current_settings["music_volume"] = config.get_value("audio", "music_volume", 0.5)
current_settings["sfx_volume"] = config.get_value("audio", "sfx_volume", 0.7)
# 读取画面设置
current_settings["fullscreen"] = config.get_value("graphics", "fullscreen", false)
current_settings["resolution_x"] = config.get_value("graphics", "resolution_x", 1920)
current_settings["resolution_y"] = config.get_value("graphics", "resolution_y", 1080)
current_settings["quality"] = config.get_value("graphics", "quality", 2)
current_settings["vsync"] = config.get_value("graphics", "vsync", true)
# 读取语言设置
current_settings["language"] = config.get_value("game", "language", "zh_CN")
print("设置加载完成")
# 保存设置
func save_settings() -> void:
var config := ConfigFile.new()
config.set_value("audio", "master_volume", current_settings["master_volume"])
config.set_value("audio", "music_volume", current_settings["music_volume"])
config.set_value("audio", "sfx_volume", current_settings["sfx_volume"])
config.set_value("graphics", "fullscreen", current_settings["fullscreen"])
config.set_value("graphics", "resolution_x", current_settings["resolution_x"])
config.set_value("graphics", "resolution_y", current_settings["resolution_y"])
config.set_value("graphics", "quality", current_settings["quality"])
config.set_value("graphics", "vsync", current_settings["vsync"])
config.set_value("game", "language", current_settings["language"])
config.save(SettingsPath)
print("设置保存完成")
# 应用设置到游戏
func apply_settings() -> void:
# 应用音量设置
var master_bus := AudioServer.get_bus_index("Master")
AudioServer.set_bus_volume_db(master_bus, linear_to_db(current_settings["master_volume"]))
var music_bus := AudioServer.get_bus_index("Music")
AudioServer.set_bus_volume_db(music_bus, linear_to_db(current_settings["music_volume"]))
var sfx_bus := AudioServer.get_bus_index("Sfx")
AudioServer.set_bus_volume_db(sfx_bus, linear_to_db(current_settings["sfx_volume"]))
# 应用画面设置
if current_settings["fullscreen"]:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_size(Vector2i(
current_settings["resolution_x"],
current_settings["resolution_y"]
))
if current_settings["vsync"]:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
else:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
# 应用语言设置
TranslationServer.set_locale(current_settings["language"])
print("设置已应用到游戏")
# 设置主音量
func set_master_volume(volume: float) -> void:
current_settings["master_volume"] = clampf(volume, 0.0, 1.0)
var bus_index := AudioServer.get_bus_index("Master")
AudioServer.set_bus_volume_db(bus_index, linear_to_db(current_settings["master_volume"]))
save_settings()
# 设置画面质量
func set_quality(quality: int) -> void:
current_settings["quality"] = clampi(quality, 0, 2)
var msaa_levels := [0, 2, 4] # 关闭, 2x, 4x 抗锯齿
get_viewport().msaa_2d = msaa_levels[current_settings["quality"]]
save_settings()完整的设置界面思路
一个好的设置界面通常分几个标签页:
| 标签页 | 包含的设置项 |
|---|---|
| 音频 | 主音量、音乐音量、音效音量、语音音量 |
| 画面 | 分辨率、全屏/窗口、画质等级、抗锯齿、垂直同步 |
| 操作 | 键位映射、手柄映射、震动开关 |
| 语言 | 语言选择、字幕开关 |
| 其他 | 自动存档频率、通知设置 |
最终建议
- 配置文件用 ConfigFile,翻译文件用 Translation,各司其职
- 每次修改设置立即保存,不要等退出游戏才保存(防止崩溃丢失设置)
- 提供"恢复默认设置"按钮,万一玩家改乱了能一键恢复
- 字体文件做动态加载,不要一次性把所有语言的字体都加载到内存
- 翻译键名用英文小写加下划线,比如
msg_game_over而不是游戏结束
