AudioLocalizationSystemV2
Namespace: Vortex.Sdk.AudioLocalizationSystemV2
Assembly: ru.vortex.sdk.localization.audio.v2
Преемник AudioLocalizationSystem (V1, помечен [Obsolete]). Отличие — клипы реплик выгружаемые: это собственные ассеты записи за AssetHandle<AudioClip>, а не записи Sound в БД, и держатся в памяти только на время проигрывания.
Назначение
Связывает текстовые ключи локализации с озвученными аудиоклипами. Для каждого ключа хранится карта «язык → звуковой ассет», при воспроизведении автоматически выбирается клип текущего языка.
Возможности:
- Маппинг ключей локализации на звуковые ассеты по языкам
- Клип грузится на время проигрывания и отпускается по его завершению (
AudioSampleWrapper.OnFinished) - Выбор хранения клипа в пресете: прямая ссылка (
DirectAssetHandle) или ленивая подгрузка через Addressables (AddressableAssetHandle, с группойAssetCache) - Воспроизведение через выделенный аудиоканал с автоматической остановкой предыдущего клипа
- Батчинг индексации при загрузке (
IProcess,UniTask.Yield)
Вне ответственности:
- Управление аудиоканалами (используется
AudioController) - Определение текущего языка (используется
Localization) - Механика загрузки/кэширования ассетов (используется
AssetHandle<T>и его реализации; для Addressables —AssetCache)
Выбор версии (V1 / V2)
Версия выбирается тогглом audioLocalizationV2 в SdkSettings (partial — DefineSettings/SdkSettings.AudioLocalization.cs). Тоггл управляет define-символом USING_VORTEX_AUDIO_LOCALIZATION_V2:
| Что | Выключен (V1) | Включён (V2) |
|---|---|---|
Сборка ru.vortex.sdk.localization.audio (V1) |
компилируется | вырезана (defineConstraints: !USING_VORTEX_AUDIO_LOCALIZATION_V2) |
Сборка ru.vortex.sdk.localization.audio.v2 |
вырезана (defineConstraints: USING_VORTEX_AUDIO_LOCALIZATION_V2) |
компилируется |
LocalizationExt (канальные расширения Localization) |
копия из V1 | копия из V2 |
LocalizationExt — LocaleChannels, IChanneledDriver, StringExtChannel и partial Localization — встраивается через asmref в сборку локализации, поэтому defineConstraints пакета на него не действует. Копии лежат в обоих пакетах, каждая экранирована #if по тому же символу — активна ровно одна. Потребитель (NaniExtensions/LocalizationSystem: LocalizationDriver : IChanneledDriver, NaniVortexLocaleConnector, NaniLocaleHandler) правок не требует — типы есть при любой версии. Копии держатся одинаковыми: правка канальных расширений вносится в обе.
Зависимости
Core
Vortex.Core.System.Abstractions—Singleton<T>Vortex.Core.System.ProcessInfo—IProcess,ProcessDataVortex.Core.DatabaseSystem—Database,RecordVortex.Core.AudioSystem—AudioController.PlaySoundWithControl,AudioSampleWrapperVortex.Core.AssetHandleSystem—AssetHandle<T>Vortex.Core.LocalizationSystem—Localization.GetCurrentChannelLanguage(LocaleChannels.Voice)Vortex.Core.LoaderSystem—Loader.RegisterVortex.Core.Extensions—ActionExt,ReactiveValues
Unity
Vortex.Unity.AudioSystem—SoundClipFixed,AudioChannelNameAttributeVortex.Unity.DatabaseSystem—RecordPreset<T>Vortex.Unity.LocalizationSystem—LanguageAttributeVortex.Unity.EditorTools—ClassFilter,AutoLink,ClassLabelCysharp.Threading.Tasks—UniTaskSirenix.Utilities— строковые расширения (Odin Inspector)
Реализации AssetHandle<T> (DirectAssetHandle в Vortex.Unity.AssetHandleSystem, AddressableAssetHandle в Vortex.Unity.AssetCacheSystem) пакет напрямую не использует — их выбирает дизайнер в пресете.
Использование
Создание пресета
Create → Database → AudioLocaleDataV2- Указать
textGuid— ключ строки локализации - Указать
channel— аудиоканал реплики - Заполнить
voices— пары «язык → хэндл клипа»: выбрать реализацию хэндла (DirectAssetHandle/AddressableAssetHandle) и клип; для addressable — группу кэша - Проверить блок ошибок над
voices: инспектор сообщает о пустом ключе, пустом канале, пустом или повторном языке, незаданном хэндле и хэндле без клипа (дляDirectAssetHandle/AddressableAssetHandle). Проверка пересчитывается не чаще раза в секунду
Воспроизведение из кода
// По текстовому ключу
AudioLocalizationController.PlayForText("quest_intro_text_guid");
Воспроизведение через Handler
- Добавить
AudioLocaleHandlerна GameObject - Указать
source— компонент сIDataStorage, предоставляющийStringData - При изменении
StringData.Valueозвучка воспроизведётся автоматически. Уже озвученное значение повторно не играет, пока не сменится: повторное включение компонента (скрытие/показ UI) или перепривязка хранилища реплику не перезапускают. Смена на пустое значение тоже считается сменой — после неё та же реплика озвучится снова
Доступ к данным напрямую
var data = Database.GetRecord<AudioLocaleData>(guid);
// Хэндл клипа для текущего языка. Load/Release — забота держателя: хэндл записи общий,
// и его же использует AudioLocalizationController.
AssetHandle<AudioClip> handle = data.GetLocale();
Editor-инструмент «Грубый инструментарий массовой замены»
В инспекторе AudioLocaleDataPreset есть свёртываемая группа Грубый инструментарий массовой замены. Это Editor-only хелперы для частых сценариев пересборки озвучки реплик. Все три действия пишут EditorUtility.SetDirty — изменения попадут в коммит.
Клипы ищутся по точному имени файла AudioClip в проекте (AssetDatabase, без учёта регистра). Хэндл перенацеливается с сохранением своей реализации: у DirectAssetHandle меняется ссылка на клип, у AddressableAssetHandle — GUID ассета в AssetReference (группа кэша сохраняется). Если клип ещё не addressable — его нужно пометить addressable вручную.
| Кнопка | Что делает | Когда полезно |
|---|---|---|
Update Voices Array |
Для каждого voice берёт текущий клип хэндла, проверяет наличие точки в его имени. Затем ищет клип с именем "{TextGuid}.{voice.language}" и перенацеливает на него хэндл. |
После переименования textGuid пресета — массово пересобрать ссылки на звук под новый ключ |
Замена первой части ключа |
Принимает поле charName и для каждого voice ищет клип с именем "{charName}.{oldSuffix}", где oldSuffix — остаток имени старого клипа после первой точки. Перенацеливает хэндл на найденный. Дополнительно перевешивает textGuid пресета на ключ локализации "{charName}.{Name}" (через Localization.GetLocalizationKeys(), case-insensitive). |
Скопировать пресет под другого персонажа: меняешь префикс — реплика автоматически перевешивается на его озвучку |
Скопировать первую запись под другой язык |
Принимает поле _newLanguage. Берёт первый voice как шаблон, заменяет в имени его клипа подстроку .{templateLang} на .{newLanguage}, находит соответствующий клип и добавляет новую запись voice для _newLanguage с хэндлом той же реализации. Если для языка уже есть запись — действие пропускается; если клип не найден — InvalidOperationException. |
Добавить новый язык: первая локализация уже сделана, под остальные нужны заглушки в одно нажатие |
Конвенция именования, которую инструмент эксплуатирует:
- Имя файла клипа имеет вид
Character.{anything}.{lang}.{index}или похожий — с языком в одной из позиций между точками. textGuidимеет видCharacter.LineKey.voice.languageсоответствует коду языка изLocalization.
При нарушении конвенции инструмент молча пропускает запись (проверяет наличие точки в имени, существование клипа с нужным именем, etc.) — пакет не упадёт, но и эффекта не даст. Исключение — копирование под язык: ненайденный клип бросает исключение. Это сделано осознанно: «грубый инструментарий» по контракту — best-effort, ручной просмотр результата обязателен.
Архитектура
AudioLocalizationController (Singleton, IProcess)
├── Index: Dictionary<string, AudioLocaleData> ← TextGuid → данные
├── PlayForText(string) ← загрузка + воспроизведение по ключу
└── RunAsync() ← индексация при загрузке
AudioLocaleData (Record)
├── TextGuid: string ← ключ локализации
├── Voices: Dictionary<string, AssetHandle<AudioClip>> ← язык → хэндл клипа
├── Channel: string ← аудиоканал реплики
└── GetLocale() → AssetHandle<AudioClip> ← хэндл для текущего языка
AudioLocaleDataPreset (RecordPreset<AudioLocaleData>)
├── textGuid [LocalizationKey] ← привязка к строке локализации
├── channel [AudioChannelName] ← аудиоканал
└── voices: List<LangGroup> ← пары язык + [SerializeReference] хэндл клипа
AudioLocaleHandler (MonoBehaviour)
└── IDataStorage → StringData → PlayForText() ← автовоспроизведение при изменении текста
Поток PlayForText(text)
1. Index[text] → data; нет — выход
2. data.GetLocale() → handle; нет озвучки языка — выход
3. handle уже грузится (повторный запрос той же реплики) — выход
4. канал data.Channel не найден — LogError, выход
5. AudioController.StopAllSounds(channel) ← звучащая реплика завершается → её OnFinished отпускает клип
6. отмена загрузки предыдущей реплики (CancellationToken)
7. await handle.LoadAsync(ct)
├── отмена → выход (владение снято кэшем)
├── ошибка загрузки → LogError, выход
└── пустой клип → LogError, handle.Release(), выход
8. PlaySoundWithControl(new SoundClipFixed(clip, false, channel)) → wrapper
└── null → LogError, handle.Release()
9. wrapper.OnFinished += handle.Release ← естественный конец / вытеснение / стоп канала
Компоненты
| Класс | Тип | Назначение |
|---|---|---|
AudioLocalizationController |
Singleton<T>, IProcess |
Индексация, загрузка и воспроизведение озвучки по ключу, отпуск клипа по завершению |
AudioLocaleData |
Record |
Данные маппинга: ключ → языки → хэндлы клипов, канал |
AudioLocaleDataPreset |
RecordPreset<AudioLocaleData> |
ScriptableObject-пресет для настройки в редакторе |
AudioLocaleHandler |
MonoBehaviour |
Автовоспроизведение озвучки при изменении StringData из IDataStorage |
Контракт
Вход
Database— записиAudioLocaleDataзагружены до вызоваRunAsyncLocalization.GetCurrentChannelLanguage(LocaleChannels.Voice)— текущий язык озвучкиIDataStorage+StringData— источник текстового ключа дляAudioLocaleHandler
Выход
AudioLocalizationController.PlayForText(string)— воспроизвести озвучку для текстового ключаAudioLocaleData.GetLocale()— получить хэндл клипа для текущего языка
Гарантии
PlayForTextостанавливает предыдущий клип на канале перед воспроизведением нового- Клип отпускается (
AssetHandle.Release) при любом завершении звука: естественный конец, вытеснение следующей репликой, остановка канала; а также если воспроизведение не стартовало - Реплика, вытесненная во время загрузки, не заиграет
RunAsyncбатчит индексацию по 50 записей за кадрWaitingFor() → Database— загрузка начнётся только после готовности БД
Ограничения
- Один голосовой клип одновременно (канал очищается целиком)
- Задержка старта: для
AddressableAssetHandleголос стартует после асинхронной загрузки клипа (для HIT из survivorsAssetCache— сразу) - Odin Inspector обязателен (
Sirenix.Utilities) AudioLocaleDataне сохраняется (GetDataForSave() → null)
Выгрузка и группа кэша
Пакет только зовёт LoadAsync/Release хэндла. Что значит «выгрузить» — решает реализация:
DirectAssetHandle— no-op: клип живёт вместе с пресетом (поведение как в V1);AddressableAssetHandle—AssetCache.Releaseв группе, выбранной в полеgroupхэндла. Реальная выгрузка — по ёмкости survivors этой группы (0 — сразу послеRelease). Для реплик стоит завести отдельную группу вAssetCacheSettings, чтобы поток реплик не вытеснял survivors остальных ассетов.
Граничные случаи
| Ситуация | Поведение |
|---|---|
PlayForText с незарегистрированным ключом |
Игнорируется (early return) |
| Нет озвучки для текущего языка | GetLocale() → null, звук не воспроизводится |
| Канал реплики не задан / не найден | Debug.LogError, звук не воспроизводится |
Повторный PlayForText той же реплики во время загрузки |
Игнорируется — реплика заиграет по окончании загрузки |
Повторный PlayForText той же реплики во время звучания |
Звучащая останавливается (клип отпущен), клип грузится заново (для addressable — HIT, если ещё в survivors) |
| Новая реплика во время загрузки предыдущей | Загрузка предыдущей отменяется, она не заиграет |
| Реплика A запрошена снова, пока её вытесненная загрузка ещё в полёте (A → B → A) | Новый цикл загрузки. Отменённая загрузка снимает в AssetCache только своё удержание, клип держит звучащая реплика |
| Ошибка загрузки клипа | Debug.LogError, звук не воспроизводится |
| Смена языка между вызовами | Следующий вызов берёт хэндл нового языка |
Дублирование language в пресете |
Ошибка валидации в инспекторе; при загрузке ToDictionary кинет ArgumentException |
| Хэндл озвучки для языка не задан | Ошибка валидации в инспекторе; Debug.LogError при индексации; для этого языка звука нет |
Хэндл без клипа (DirectAssetHandle с пустым asset) |
Ошибка валидации в инспекторе; Debug.LogError при воспроизведении, хэндл отпускается |
Воспроизведение не запустилось (PlaySoundWithControl → null) |
Debug.LogError, хэндл отпускается |
StringData.Value == null/empty |
AudioLocaleHandler игнорирует (проверка IsNullOrWhitespace) |
Повторное включение AudioLocaleHandler / перепривязка хранилища с тем же значением |
Повторно не воспроизводится |
AudioLocaleHandler получил значение до окончания индексации |
PlayForText его игнорирует, но значение считается озвученным — заиграет только после следующей смены |
Отмена загрузки (CancellationToken) |
Индексация прерывается, Index может быть неполным |