Files
g-world/scripts/autoload/game_settings.gd
T
2026-08-10 20:39:06 +03:00

286 lines
9.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name GameSettingsService
extends Node
## Глобальные настройки игры: хранятся в user://settings.cfg и применяются к движку.
##
## Зарегистрирован автозагрузкой, поэтому доступен отовсюду как GameSettings.
## UI (scenes/ui/settings_menu.tscn) только читает и вызывает сеттеры —
## вся работа с DisplayServer, AudioServer и InputMap живёт здесь.
const CONFIG_PATH := "user://settings.cfg"
enum WindowMode { WINDOWED, BORDERLESS, FULLSCREEN }
## Режимы окна в порядке, в котором они показываются в выпадающем списке.
const WINDOW_MODE_LABELS: Array[String] = ["Оконный", "Без рамки", "Полноэкранный"]
## Действия, доступные для переназначения, и их подписи в UI.
const ACTION_LABELS := {
"move_forward": "Вперёд",
"move_back": "Назад",
"move_left": "Влево",
"move_right": "Вправо",
"jump": "Прыжок",
"sprint": "Бег",
"interact": "Взаимодействие",
"pause": "Пауза",
}
## Шины микшера, громкость которых регулируется в настройках.
const AUDIO_BUSES := {
"Master": "Общая",
"Music": "Музыка",
"SFX": "Эффекты",
}
const RESOLUTIONS: Array[Vector2i] = [
Vector2i(1280, 720),
Vector2i(1600, 900),
Vector2i(1920, 1080),
Vector2i(2560, 1440),
Vector2i(3840, 2160),
]
## 0 — без ограничения.
const FPS_LIMITS: Array[int] = [0, 30, 60, 90, 120, 144, 240]
## Любое изменение настроек — чтобы открытые экраны могли обновить себя.
signal changed
var window_mode: int = WindowMode.WINDOWED
var resolution := Vector2i(1920, 1080)
var vsync := true
var fps_limit := 0
var volumes := {"Master": 1.0, "Music": 0.8, "SFX": 0.8}
## action: String -> keycode: int
var bindings := {}
var _default_bindings := {}
func _ready() -> void:
_capture_default_bindings()
_read_current_state()
if load_settings():
apply_all()
else:
# Первый запуск: ничего не навязываем окну, просто фиксируем как есть.
save_settings()
# --- Чтение / запись --------------------------------------------------------
func load_settings() -> bool:
var config := ConfigFile.new()
if config.load(CONFIG_PATH) != OK:
return false
window_mode = config.get_value("video", "window_mode", window_mode)
resolution = config.get_value("video", "resolution", resolution)
vsync = config.get_value("video", "vsync", vsync)
fps_limit = config.get_value("video", "fps_limit", fps_limit)
for bus_name: String in AUDIO_BUSES:
var value: float = config.get_value("audio", bus_name, volumes[bus_name])
volumes[bus_name] = clampf(value, 0.0, 1.0)
for action: String in ACTION_LABELS:
bindings[action] = config.get_value("input", action, _default_bindings.get(action, KEY_NONE))
return true
func save_settings() -> void:
var config := ConfigFile.new()
config.set_value("video", "window_mode", window_mode)
config.set_value("video", "resolution", resolution)
config.set_value("video", "vsync", vsync)
config.set_value("video", "fps_limit", fps_limit)
for bus_name: String in AUDIO_BUSES:
config.set_value("audio", bus_name, volumes[bus_name])
for action: String in bindings:
config.set_value("input", action, bindings[action])
config.save(CONFIG_PATH)
func reset_to_defaults() -> void:
window_mode = WindowMode.WINDOWED
resolution = Vector2i(
ProjectSettings.get_setting("display/window/size/viewport_width", 1920),
ProjectSettings.get_setting("display/window/size/viewport_height", 1080),
)
vsync = true
fps_limit = 0
volumes = {"Master": 1.0, "Music": 0.8, "SFX": 0.8}
bindings = _default_bindings.duplicate()
apply_all()
save_settings()
changed.emit()
# --- Применение к движку ----------------------------------------------------
func apply_all() -> void:
apply_video()
apply_audio()
apply_input()
func apply_video() -> void:
match window_mode:
WindowMode.WINDOWED:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
DisplayServer.window_set_size(resolution)
_center_window()
WindowMode.BORDERLESS:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
var screen := DisplayServer.window_get_current_screen()
DisplayServer.window_set_size(DisplayServer.screen_get_size(screen))
DisplayServer.window_set_position(DisplayServer.screen_get_position(screen))
WindowMode.FULLSCREEN:
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
DisplayServer.window_set_vsync_mode(
DisplayServer.VSYNC_ENABLED if vsync else DisplayServer.VSYNC_DISABLED
)
Engine.max_fps = fps_limit
func apply_audio() -> void:
for bus_name: String in volumes:
var index := AudioServer.get_bus_index(bus_name)
if index < 0:
continue
var value: float = volumes[bus_name]
AudioServer.set_bus_mute(index, is_zero_approx(value))
AudioServer.set_bus_volume_db(index, linear_to_db(maxf(value, 0.0001)))
func apply_input() -> void:
for action: String in bindings:
_apply_action(action)
# --- Сеттеры (UI дёргает только их) -----------------------------------------
func set_window_mode(mode: int) -> void:
window_mode = mode
apply_video()
_commit()
func set_resolution(value: Vector2i) -> void:
resolution = value
apply_video()
_commit()
func set_vsync(enabled: bool) -> void:
vsync = enabled
DisplayServer.window_set_vsync_mode(
DisplayServer.VSYNC_ENABLED if enabled else DisplayServer.VSYNC_DISABLED
)
_commit()
func set_fps_limit(value: int) -> void:
fps_limit = value
Engine.max_fps = value
_commit()
func set_volume(bus_name: String, value: float) -> void:
volumes[bus_name] = clampf(value, 0.0, 1.0)
apply_audio()
_commit()
## Вешает клавишу на действие. Если клавиша уже занята, снимает её с прежнего
## действия и возвращает его имя — вызывающий может показать предупреждение.
func set_binding(action: String, keycode: int) -> String:
var conflict := ""
for other: String in bindings:
if other != action and int(bindings[other]) == keycode:
conflict = other
bindings[other] = KEY_NONE
_apply_action(other)
bindings[action] = keycode
_apply_action(action)
_commit()
return conflict
## Подпись клавиши для UI.
static func key_name(keycode: int) -> String:
if keycode == KEY_NONE:
return "—"
return OS.get_keycode_string(keycode)
# --- Внутреннее -------------------------------------------------------------
func _commit() -> void:
save_settings()
changed.emit()
func _apply_action(action: String) -> void:
if not InputMap.has_action(action):
return
for event: InputEvent in InputMap.action_get_events(action):
if event is InputEventKey:
InputMap.action_erase_event(action, event)
var keycode := int(bindings.get(action, KEY_NONE))
if keycode == KEY_NONE:
return
var event := InputEventKey.new()
event.keycode = keycode as Key
InputMap.action_add_event(action, event)
func _capture_default_bindings() -> void:
for action: String in ACTION_LABELS:
var keycode := KEY_NONE
if InputMap.has_action(action):
for event: InputEvent in InputMap.action_get_events(action):
if event is InputEventKey:
var key_event := event as InputEventKey
keycode = key_event.keycode if key_event.keycode != KEY_NONE else key_event.physical_keycode
break
_default_bindings[action] = keycode
bindings[action] = keycode
func _read_current_state() -> void:
resolution = DisplayServer.window_get_size()
vsync = DisplayServer.window_get_vsync_mode() != DisplayServer.VSYNC_DISABLED
fps_limit = Engine.max_fps
# Безрамочность намеренно не считываем: запуск из редактора отдаёт окно без
# рамки (встроенное окно игры), и это попало бы в конфиг как выбор игрока.
if DisplayServer.window_get_mode() in [
DisplayServer.WINDOW_MODE_FULLSCREEN,
DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN,
]:
window_mode = WindowMode.FULLSCREEN
else:
window_mode = WindowMode.WINDOWED
for bus_name: String in AUDIO_BUSES:
var index := AudioServer.get_bus_index(bus_name)
if index >= 0:
volumes[bus_name] = db_to_linear(AudioServer.get_bus_volume_db(index))
func _center_window() -> void:
var screen := DisplayServer.window_get_current_screen()
var screen_rect := Rect2i(
DisplayServer.screen_get_position(screen), DisplayServer.screen_get_size(screen)
)
var window_size := DisplayServer.window_get_size()
@warning_ignore("integer_division")
var offset := (screen_rect.size - window_size) / 2
DisplayServer.window_set_position(screen_rect.position + offset)