286 lines
9.1 KiB
GDScript
286 lines
9.1 KiB
GDScript
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)
|