Динамические и интерактивные звуки 26.1.2
Создание динамических и интерактивных звуков
ТРЕБОВАНИЯ
Эта страница основана на материалах разделов «Воспроизведение звуков» и «Создание собственных звуков»!
Проблемы с 'SoundEvents'
Как мы узнали в разделе «Использование звуков», предпочтительно использовать SoundEvent'ы на логической стороне сервера, даже если это немного противоречит интуиции. В конце концов, клиент должен обрабатывать звук, который передаётся в ваши наушники, верно?
Да, верно. Технически клиентская сторона должна обрабатывать звук. Однако для простого воспроизведения SoundEvent на стороне сервера подготовлен большой промежуточный шаг, который может быть не очевиден с первого взгляда. Какие клиенты должны иметь возможность слышать этот звук?
Использование звука на логической стороне сервера решает проблему широковещательной передачи SoundEvent'ов. Проще говоря, каждый клиент (LocalPlayer) в диапазоне отслеживания получает сетевой пакет для воспроизведения этого конкретного звука. Событие звука по сути транслируется с логической стороны сервера всем участвующим клиентам, без необходимости вам задумываться об этом. Звук воспроизводится один раз с указанными значениями громкости и высоты тона.
Но что, если этого недостаточно? Что если звук должен повторяться, динамически изменять громкость и высоту тона во время воспроизведения, и всё это основано на значениях, которые приходят от таких объектов, как Entities (сущностей) или BlockEntities (блоков-сущностей)?
Для такого случая недостаточно использовать SoundEvent на логической стороне сервера как обычно.
Создание своих звуков
Мы хотим создать новый циклический аудиофайл для ещё одного SoundEvent. Если вы можете найти аудиофайл, который уже бесшовно зациклен, можете просто следовать шагам из раздела «Создание собственных звуков». Если звук ещё не идеально зациклен, придётся подготовить его.
Для этой цели может подойти большинство современных DAW (digital audio workstation — цифровая звуковая рабочая станция), но я предпочитаю использовать Reaper, если не хочу сильно вдаваться в редактирование аудио.
Настройка
Наш исходный звук будет от двигателя.
Давайте загрузим файл в выбранную нами DAW.

Мы можем слышать и видеть, что двигатель запускается в начале и останавливается в конце, что не очень походит на цикличный звук. Давайте вырежем эти части и настроим маркеры выбора времени, чтобы соответствовать новой длине. Также включим режим повторения Toggle Repeat, чтобы аудио циклировалось, пока мы его настраиваем.

Удаление мешающих аудиоэлементов
Если мы прислушаемся, на заднем плане есть звуковой сигнал, который, возможно, исходил от машины. В игре это будет плохо звучать, поэтому давайте попробуем его удалить.
Это постоянный звук, который сохраняет свою частоту на протяжении всего аудио. Поэтому для его отфильтровки должно быть достаточно простого EQ-фильтра.
Reaper уже оснащён EQ-фильтром под названием «ReaEQ». Он может находиться в другом месте и называться иначе в других DAW, но использование EQ — стандарт в большинстве DAW сегодня.
Если вы уверены, что в вашей DAW нет EQ-фильтра, поищите бесплатные VST-альтернативы в интернете, которые вы сможете установить в выбранную вами DAW.
В Reaper используйте окно эффектов, чтобы добавить аудиоэффект «ReaEQ» или любой другой EQ.

Если мы сейчас запустим аудио, удерживая окно EQ-фильтра открытым, фильтр покажет входящий звук на своём дисплее. Там мы можем увидеть много пиков.

Если вы не аудиоинженер, эта часть в основном сводится к экспериментам и методу проб и ошибок. Между узлами 2 и 3 есть довольно резкий пик. Давайте переместим узлы так, чтобы снизить частоту только в этой части.

Также с помощью простого EQ-фильтра можно добиться других эффектов. Например, обрезка высоких и/или низких частот может создать такое впечатление, будто звук передаётся по радио.
Также вы можете накладывать больше аудиофайлов, изменять высоту тона, добавлять реверберацию или использовать более сложные звуковые эффекты, такие как «bit-crusher». Звуковой дизайн может быть увлекательным делом, особенно если вы начнётся случайно замечать хорошо звучащие варианты вашего аудио. Эксперименты — ключ к успеху, и, возможно, ваш звук станет даже лучше, чем был.
Продолжим с EQ-фильтром, который мы использовали для удаления проблемной частоты.
Сравнение
Давайте сравним оригинальный файл с очищенной версией.
Вы можете слышать явное гудение и звуковой сигнал, возможно, от электрического элемента двигателя, в оригинальном звуке.
С помощью EQ-фильтра мы почти полностью смогли удалить его из аудиофайла. На слух стало определённо приятнее.
Создание цикла
Если мы позволим звуку проиграться до конца и снова начаться сначала, мы отчётливо услышим переход. Наша цель — избавиться от этого, сделав его плавным.
Начнём с того, что обрежем кусок с конца, который настолько большой как переход, и поместите этот кусок в начало нового аудиотрека. В Reaper, вы можете разделить аудио, надо просто поставить свой курсор в место обрезки и нажать S.

Возможно надо как первый, так и во второй аудиотрек добавить аудиоэффект EQ.
Пусть концовка нового трека потухнет и пусть начало первого аудиотрека появится.

Экспортирование
Сделайте экспорт аудио с двумя аудиотреками, но только с одним аудиоканалом (Моно) и создайте новый SoundEvent для того .ogg файла в вашем моде. Если вы не уверены или же не знаете как оно делается, то посмотрите на страничку создания своих звуков.
Это теперь законченный аудиодвижок для SoundEvent'а, называющийся ENGINE_LOOP.
Используем SoundInstance
Для воспроизведения звуков на стороне клиента необходим SoundInstance. Однако они по-прежнему используют SoundEvent.
Если вам нужно воспроизвести звук, например, при нажатии на элемент интерфейса, для этого уже существует класс SimpleSoundInstance.
Имейте в виду, что это будет воспроизводиться только на том клиенте, который выполнил эту часть кода.
java
Minecraft client = Minecraft.getInstance();
client.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));1
2
2
WARNING
Обратите внимание, что в классе AbstractSoundInstance, от которого наследуются SoundInstance, есть аннотация @Environment(EnvType.CLIENT).
Это означает, что данный класс (и все его подклассы) будет доступен только на стороне клиента.
Если вы попробуете использовать это в логическом контексте на стороне сервера, вы можете сначала не заметить проблему в одиночной игре, но сервер в многопользовательской среде упадет, так как он вообще не сможет найти эту часть кода.
Если вы столкнулись с этими проблемами, рекомендуется создать мод с помощью Онлайн-генератора шаблонов с включенной опцией Разделять клиент и общие источники.
Случай SoundInstance может быть более мощным, чем просто однократное воспроизведение звуков.
Посмотрите на класс AbstractSoundInstance и на то, какие значения он может отслеживать. Помимо обычных переменных громкости и высоты тона, в нем также хранятся координаты XYZ и информация о том, следует ли повторять звук после завершения SoundEvent.
Если же мы обратимся к его подклассу AbstractTickableSoundInstance, то увидим что там также представлен интерфейс TickableSoundInstance, который добавляет функцию тиканья к SoundInstance.
Чтобы воспользоваться этими утилитами, просто создайте новый класс для своего собственного SoundInstance и расширьте его от MovingSoundInstance.
java
public class CustomSoundInstance extends AbstractTickableSoundInstance {
private final LivingEntity entity;
public CustomSoundInstance(LivingEntity entity, SoundEvent soundEvent, SoundSource soundCategory) {
super(soundEvent, soundCategory, SoundInstance.createUnseededRandom());
// In this constructor we also add the sound source (LivingEntity) of
// the SoundInstance and store it in the current object
this.entity = entity;
// set up default values when the sound is about to start
this.volume = 1.0f;
this.pitch = 1.0f;
this.looping = true;
this.setPositionToEntity();
}
@Override
public void tick() {
// stop sound instantly if sound source does not exist anymore
if (this.entity == null || this.entity.isRemoved() || this.entity.isDeadOrDying()) {
this.stop();
return;
}
// move sound position over to the new position for every tick
this.setPositionToEntity();
}
@Override
public boolean canStartSilent() {
// override to true, so that the SoundInstance can start
// or add your own condition to the SoundInstance, if necessary
return true;
}
// small utility method to move the sound instance position
// to the sound source's position
private void setPositionToEntity() {
this.x = this.entity.getX();
this.y = this.entity.getY();
this.z = this.entity.getZ();
}
}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
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
Использование собственных Entity или BlockEntity вместо базового экземпляра LivingEntity может дать вам еще больше контроля, например, в методе tick() на основе на методах-акселераторах, но вам не обязательно нужна ссылка на такой источник звука. Вместо этого вы можете получить доступ к BlockPos из другого места или даже задать его вручную только в конструкторе.
Только имейте в виду, что все объекты, на которые ссылается SoundInstance, являются версиями со стороны клиента. В определенных ситуациях свойства логической сущности на стороне сервера могут отличаться от ее аналога на стороне клиента. Если вы заметили, что ваши значения не совпадают, убедитесь, что они синхронизированы либо с EntityDataAccessor сущности, либо с привязанными к клиенту пакетами BlockEntity, либо с полностью настраиваемыми сетевыми пакетами, привязанными к клиенту.
После создания пользовательского SoundInstance его можно использовать где угодно, если он был выполнен на стороне клиента с помощью менеджера звука. Таким же образом вы можете остановить пользовательский SoundInstance вручную, если это необходимо.
java
CustomSoundInstance instance = new CustomSoundInstance(client.player, CustomSounds.ENGINE_LOOP, SoundSource.NEUTRAL);
// play the sound instance
client.getSoundManager().play(instance);
// stop the sound instance
client.getSoundManager().stop(instance);1
2
3
4
5
6
7
2
3
4
5
6
7
Теперь звуковой цикл будет воспроизводиться только для клиента, который запустил этот SoundInstance. В этом случае звук будет следовать за самим LocalPlayer.
На этом мы закончим объяснение создания и использования простого пользовательского SoundInstance.
Усовершенствованные звуковые экземпляры
WARNING
Следующее содержание охватывает продвинутую тему.
К этому моменту вы должны хорошо знать Java, объектно-ориентированное программирование, дженерики и системы обратного вызова.
Знания о Entities, BlockEntities и пользовательских сетях также очень помогут в понимании сценариев использования и применения расширенных звуков.
Чтобы показать пример того, как можно создавать более сложные системы SoundInstance, мы добавим дополнительную функциональность, абстракции и утилиты, чтобы сделать работу с такими звуками в более широком масштабе, более простой, динамичной и гибкой.
Теория
Давайте подумаем, какую цель мы преследуем, используя SoundInstance.
- Звук должен зацикливаться до тех пор, пока работает связанный пользовательский
EngineBlockEntity - Экземпляр
SoundInstanceдолжен перемещаться, следуя за положением своего пользовательскогоEngineBlockEntity(BlockEntityне будет перемещаться, так что это может быть более полезно дляEntities)_ - Нам нужны плавные переходы. Включение и выключение практически никогда не должно быть мгновенным.
- Изменение громкости и высоты тона в зависимости от внешних факторов (например, от источника звука)
Вкратце, нам нужно отслеживать экземпляр пользовательского BlockEntity, регулировать значения громкости и высоты тона во время работы SoundInstance на основе значений из этого пользовательского BlockEntity и реализовать "Transition States".
Если вы планируете создать несколько различных SoundInstances, которые будут вести себя по-разному, я бы рекомендовал создать новый абстрактный класс AbstractDynamicSoundInstance, который реализует поведение по умолчанию, и позволить реальным пользовательским классам SoundInstance расширяться от него.
Если вы планируете использовать только один, вы можете обойтись без абстрактного суперкласса, а реализовать эту функциональность непосредственно в своем пользовательском классе SoundInstance.
Кроме того, неплохо было бы иметь централизованное место, где SoundInstance отслеживаются, воспроизводятся и останавливаются. Это означает, что он должен обрабатывать входящие сигналы, например, от пользовательских сетевых пакетов S2C, перечислять все текущие запущенные экземпляры и обрабатывать особые случаи, например, какие звуки могут воспроизводиться одновременно и какие звуки могут отключить другие звуки при активации. Для этого можно создать новый класс DynamicSoundManager, чтобы легко взаимодействовать с этой звуковой системой.
В целом наша звуковая система может выглядеть так, когда мы закончим.

INFO
Все эти перечисления, интерфейсы и классы будут созданы заново. Настройте систему и утилиты в соответствии с вашими потребностями. Это лишь пример того, как можно подходить к подобным темам.
DynamicSoundSource Интерфейс
Если вы решите создать новый, более модульный, пользовательский класс AbstractDynamicSoundInstance в качестве суперкласса, вы можете захотеть ссылаться не только на один тип Entity, но и на разные типы, или даже на BlockEntity.
В этом случае использование абстракции - ключевой момент. Вместо того чтобы напрямую ссылаться, например, на пользовательский BlockEntity, эту проблему решает только отслеживание интерфейса, который предоставляет данные.
В дальнейшем мы будем использовать пользовательский интерфейс под названием DynamicSoundSource. Он реализован во всех классах, которые хотят использовать эту функциональность динамического звука, например, в ваших классах BlockEntity, Entities или даже, с помощью миксинов, в уже существующих классах, таких как Zombie. В основном он представляет только необходимые данные источника звука.
java
public interface DynamicSoundSource {
// gets access to how many ticks have passed for e.g. a BlockEntity instance
int getTick();
// gets access to where currently this instance is placed in the world
Vec3 getPosition();
// holds a normalized (range of 0-1) value, showing how much stress this instance is currently experiencing
// It is more or less just an arbitrary value, which will cause the sound to change its pitch while playing.
float getNormalizedStress();
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
После создания интерфейса, обязательно реализуйте его в необходимых классах.
INFO
Это утилита, которая может использоваться как на стороне клиента, так и на стороне логического сервера.
Поэтому этот интерфейс должен храниться в общих пакетах, а не в пакетах для клиентов, если вы используете опцию "разделить источники".
TransitionState Enum
Как упоминалось ранее, вы можете остановить запуск SoundInstance с помощью клиентского SoundManager, но это приведет к тому, что SoundInstance мгновенно замолчит. Наша цель - при поступлении сигнала остановки не остановить звук, а выполнить завершающую фазу его "переходного состояния". Только после завершения фазы завершения пользовательская SoundInstance должна быть остановлена.
TransitionState - это вновь созданное перечисление, которое содержит три значения. Они будут использоваться для отслеживания того, в какой фазе должен находиться звук.
- Фаза
STARTING: звук начинается тихо, но постепенно увеличивается в громкости - Фаза
RUNNING: звук работает нормально - Фаза
ENDING: звук начинается с исходной громкости и медленно уменьшается, пока не затихнет
Технически простого перечисления с фазами может быть достаточно.
java
public enum TransitionState {
STARTING, RUNNING, ENDING
}1
2
3
2
3
Но когда эти значения передаются по сети, вы можете захотеть определить для них идентификатор или даже добавить другие пользовательские значения.
java
public enum TransitionState {
STARTING("starting_phase"),
RUNNING("idle_phase"),
ENDING("ending_phase");
private final Identifier identifier;
TransitionState(String name) {
this.identifier = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, name);
}
public Identifier getIdentifier() {
return this.identifier;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
INFO
Опять же, если вы используете "раздельные источники", вам нужно подумать о том, где вы будете использовать это перечисление. Технически, только пользовательские SoundInstance, которые доступны только на стороне клиента, будут использовать эти значения перечисления.
Но если это перечисление используется где-либо еще, например, в пользовательских сетевых пакетах, вам, возможно, придется поместить это перечисление в общие пакеты вместо пакетов, предназначенных только для клиентов.
SoundInstanceCallback Интерфейс
Этот интерфейс используется в качестве обратного вызова. Пока нам нужен только метод onFinished, но вы можете добавить свои собственные методы, если вам нужно посылать другие сигналы от объекта SoundInstance.
java
public interface SoundInstanceCallback {
// deliver the custom SoundInstance, from which this signal originates,
// using the method parameters
<T extends AbstractDynamicSoundInstance> void onFinished(T soundInstance);
}1
2
3
4
5
2
3
4
5
Реализуйте этот интерфейс в любом классе, который должен уметь обрабатывать входящие сигналы, например, в AbstractDynamicSoundInstance, который мы вскоре создадим и создайте функциональность в самом пользовательском SoundInstance.
AbstractDynamicSoundInstance Класс
Давайте, наконец, приступим к работе над ядром динамической системы SoundInstance. AbstractDynamicSoundInstance - это недавно созданный класс abstract. Он реализует стандартные определяющие функции и утилиты для наших пользовательских SoundInstances, которые будут наследоваться от него.
Мы можем взять CustomSoundInstance из ранее и усовершенствовать его. Вместо LivingEntity мы теперь будем ссылаться на наш DynamicSoundSource. Кроме того, мы определим дополнительные свойства.
TransitionStateдля отслеживания текущей фазы- продолжительность начальной и конечной фаз
- минимальные и максимальные значения громкости и высоты тона
- логическое значение, уведомляющее о том, что данный экземпляр завершен и может быть очищен
- держатели галочек, чтобы следить за ходом выполнения текущего звука.
- обратный вызов, который посылает сигнал обратно в
DynamicSoundManagerдля окончательной очистки, когдаSoundInstanceфактически завершена
java
public abstract class AbstractDynamicSoundInstance extends AbstractTickableSoundInstance {
protected final DynamicSoundSource soundSource; // Entities, BlockEntities, ...
protected TransitionState transitionState; // current TransitionState of the SoundInstance
protected final int startTransitionTicks, endTransitionTicks; // duration of starting and ending phases
// possible volume range when adjusting sound values
protected final float maxVolume; // only max value since the minimum is always 0
// possible pitch range when adjusting sound values
protected final float minPitch, maxPitch;
protected int currentTick = 0, transitionTick = 0; // current tick values for the instance
protected final SoundInstanceCallback callback; // callback for soundInstance states
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Затем установите начальные значения по умолчанию для пользовательского SoundInstance в конструкторе абстрактного класса.
java
// ...
// set up default settings of the SoundInstance in this constructor
protected AbstractDynamicSoundInstance(DynamicSoundSource soundSource, SoundEvent soundEvent, SoundSource soundCategory,
int startTransitionTicks, int endTransitionTicks, float maxVolume, float minPitch, float maxPitch,
SoundInstanceCallback callback) {
super(soundEvent, soundCategory, SoundInstance.createUnseededRandom());
// store important references to other objects
this.soundSource = soundSource;
this.callback = callback;
// store the limits for the SoundInstance
this.maxVolume = maxVolume;
this.minPitch = minPitch;
this.maxPitch = maxPitch;
this.startTransitionTicks = startTransitionTicks; // starting phase duration
this.endTransitionTicks = endTransitionTicks; // ending phase duration
// set start values
this.volume = 0.0f;
this.pitch = minPitch;
this.looping = true;
this.transitionState = TransitionState.STARTING;
this.setPositionToEntity();
}
// ...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
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
После завершения работы конструктора необходимо разрешить SoundInstance играть.
java
@Override
public boolean canStartSilent() {
// override to true, so that the SoundInstance can start
// or add your own condition to the SoundInstance, if necessary
return true;
}1
2
3
4
5
6
2
3
4
5
6
Теперь наступает важная часть для этого динамического SoundInstance. В зависимости от текущего тика экземпляра, он может применять различные значения и поведение.
java
@Override
public void tick() {
// handle states where sound might be actually stopped instantly
if (this.soundSource == null) {
this.callback.onFinished(this);
}
// basic tick behaviour
this.currentTick++;
this.setPositionToEntity();
// SoundInstance phase switching
switch (this.transitionState) {
case STARTING -> {
this.transitionTick++;
// go into next phase if starting phase finished its duration
if (this.transitionTick > this.startTransitionTicks) {
this.transitionTick = 0; // reset tick for future ending phase
this.transitionState = TransitionState.RUNNING;
}
}
case ENDING -> {
this.transitionTick++;
// set SoundInstance as finished if ending phase finished its duration
if (this.transitionTick > this.endTransitionTicks) {
this.callback.onFinished(this);
}
}
}
// apply volume and pitch modulation here,
// if you use a normal SoundInstance class
}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
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
Как видите, мы еще не применяли модуляцию громкости и высоты тона. Мы применяем только общее поведение. Таким образом, в этом классе AbstractDynamicSoundInstance мы предоставляем только базовую структуру и инструменты для подклассов, которые могут сами решать, какой вид модуляции звука они хотят применить.
Итак, давайте рассмотрим несколько примеров таких методов модуляции звука.
java
// increase or decrease volume and pitch based on the current phase of the sound
protected void modulateSoundForTransition() {
float normalizedTick = switch (this.transitionState) {
case STARTING -> (float) this.transitionTick / this.startTransitionTicks;
case ENDING -> 1.0f - ((float) this.transitionTick / this.endTransitionTicks);
default -> 1.0f;
};
this.volume = Mth.lerp(normalizedTick, 0.0f, this.maxVolume);
}
// increase or decrease pitch based on the sound source's stress value
protected void modulateSoundForStress() {
this.pitch = Mth.lerp(this.soundSource.getNormalizedStress(), this.minPitch, this.maxPitch);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Как видите, нормализованные значения в сочетании с линейной интерполяцией (lerp) помогают сформировать значения в желаемых пределах звука. Помните, что если вы добавите несколько методов, изменяющих одно и то же значение, вам придется наблюдать и настраивать их совместную работу друг с другом.
Осталось добавить оставшиеся вспомогательные методы, и класс AbstractDynamicSoundInstance готов.
java
// moves the sound instance position to the sound source's position
protected void setPositionToEntity() {
this.x = this.soundSource.getPosition().x();
this.y = this.soundSource.getPosition().y();
this.z = this.soundSource.getPosition().z();
}
// Sets the SoundInstance into its ending phase.
// This is especially useful for external access to this SoundInstance
public void end() {
this.transitionState = TransitionState.ENDING;
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Пример реализации SoundInstance
Если мы посмотрим на реальный пользовательский класс SoundInstance, который расширяется от недавно созданного AbstractDynamicSoundInstance, нам нужно будет только подумать о том. какие условия приведут к остановке звука и какую модуляцию звука мы хотим применить.
java
public class EngineSoundInstance extends AbstractDynamicSoundInstance {
// Here we just use the default constructor parameters.
// If you want to specifically set values here already,
// you can clean up the constructor parameters a bit
public EngineSoundInstance(DynamicSoundSource soundSource, SoundEvent soundEvent, SoundSource soundCategory,
int startTransitionTicks, int endTransitionTicks, float maxVolume, float minPitch, float maxPitch,
SoundInstanceCallback callback) {
super(soundSource, soundEvent, soundCategory, startTransitionTicks, endTransitionTicks, maxVolume, minPitch, maxPitch, callback);
}
@Override
public void tick() {
// check conditions which set this sound automatically into the ending phase
if (soundSource instanceof EngineBlockEntity blockEntity && blockEntity.isRemoved()) {
this.end();
}
// apply the default tick behaviour from the parent class
super.tick();
// modulate volume and pitch of the SoundInstance
this.modulateSoundForTransition();
this.modulateSoundForStress();
}
// you can also add sound modulation methods here,
// which should be only accessible to this
// specific SoundInstance
}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
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
Класс DynamicSoundManager
Ранее мы обсуждали (#using-a-soundinstance), как воспроизводить и останавливать SoundInstance. Чтобы очистить, централизовать и управлять этими взаимодействиями, вы можете создать свой собственный обработчик SoundInstance, который строится поверх этого.
Этот новый класс DynamicSoundManager будет управлять пользовательскими SoundInstances, поэтому он также будет доступен только на стороне клиента. Кроме того, клиент должен допускать существование только одного экземпляра этого класса. Несколько звукорежиссеров для одного клиента не имеют особого смысла и еще больше усложняют взаимодействие. Итак, давайте воспользуемся ["Шаблоном проектирования Singleton"] (https://refactoring.guru/design-patterns/singleton/java/example).
java
public class DynamicSoundManager implements SoundInstanceCallback {
// An instance of the client to use Minecraft's default SoundManager
private static final Minecraft client = Minecraft.getInstance();
// static field to store the current instance for the Singleton Design Pattern
private static DynamicSoundManager instance;
// The list which keeps track of all currently playing dynamic SoundInstances
private final List<AbstractDynamicSoundInstance> activeSounds = new ArrayList<>();
private DynamicSoundManager() {
// private constructor to make sure that the normal
// instantiation of that object is not used externally
}
// when accessing this class for the first time a new instance
// is created and stored. If this is called again only the already
// existing instance will be returned, instead of creating a new instance
public static DynamicSoundManager getInstance() {
if (instance == null) {
instance = new DynamicSoundManager();
}
return instance;
}
// This is where the callback signal of a finished custom SoundInstance will arrive.
// For now, we can just stop and remove the sound from the list, but you can add
// your own functionality too
@Override
public <T extends AbstractDynamicSoundInstance> void onFinished(T soundInstance) {
this.stop(soundInstance);
}
}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
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
После создания базовой структуры вы можете добавить методы, которые необходимы для взаимодействия со звуковой системой.
- воспроизведение звуков
- прекращение звуков
- проверка того, воспроизводится ли в данный момент звук
java
// Plays a sound instance, if it doesn't already exist in the list
public <T extends AbstractDynamicSoundInstance> void play(T soundInstance) {
if (this.activeSounds.contains(soundInstance)) return;
client.getSoundManager().play(soundInstance);
this.activeSounds.add(soundInstance);
}
// Stops a sound immediately. in most cases it is preferred to use
// the sound's ending phase, which will clean it up after completion
public <T extends AbstractDynamicSoundInstance> void stop(T soundInstance) {
client.getSoundManager().stop(soundInstance);
this.activeSounds.remove(soundInstance);
}
// Finds a SoundInstance from a SoundEvent, if it exists and is currently playing
public Optional<AbstractDynamicSoundInstance> getPlayingSoundInstance(SoundEvent soundEvent) {
for (var activeSound : this.activeSounds) {
// SoundInstances use their SoundEvent's id by default
if (activeSound.getIdentifier().equals(soundEvent.location())) {
return Optional.of(activeSound);
}
}
return Optional.empty();
}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
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
Вместо того чтобы иметь только список всех воспроизводимых в данный момент SoundInstances, можно также отслеживать, какие источники звука воспроизводят те или иные звуки. Например, двигатель с двумя звуками двигателя одновременно не имеет смысла, в то время как несколько двигателей, воспроизводящих соответствующие звуки двигателя является допустимым вариантом. Для простоты мы просто создали List<AbstractDynamicSoundInstance>, но во многих случаях лучше использовать HashMap из DynamicSoundSource и AbstractDynamicSoundInstance.
Использование расширенной звуковой системы
Чтобы использовать эту звуковую систему, просто воспользуйтесь методами DynamicSoundManager или SoundInstance. Используя методы onStartedTrackingBy и onStoppedTrackingBy из сущностей или в рамках пользовательской сетевой связи, привязанной к клиенту, теперь можно запускать и останавливать собственные динамические экземпляры SoundInstance.
java
private static void handleClientboundEngineSoundPacket(EngineSoundInstancePacket packet, ClientPlayNetworking.Context context) {
ClientLevel level = context.client().level;
if (level == null) return;
DynamicSoundManager soundManager = DynamicSoundManager.getInstance();
if (level.getBlockEntity(packet.blockEntityPos()) instanceof EngineBlockEntity engineBlockEntity) {
if (packet.shouldStart()) {
soundManager.play(new EngineSoundInstance(engineBlockEntity,
CustomSounds.ENGINE_LOOP, SoundSource.BLOCKS,
60, 30, 1.2f, 0.8f, 1.4f,
soundManager)
);
return;
}
}
if (!packet.shouldStart()) {
soundManager.getPlayingSoundInstance(CustomSounds.ENGINE_LOOP).ifPresent(AbstractDynamicSoundInstance::end);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Готовый продукт может регулировать громкость в зависимости от фазы звука, чтобы сгладить переходы, и изменять высоту тона в зависимости от величины напряжения, которое исходящего от источника звука.
Вы можете добавить еще одно значение к источнику звука, которое отслеживает значение "перегрева" и, кроме того, позволить шипящему SoundInstance медленно затухать, если значение больше 0 или добавить новый интерфейс для динамических SoundInstance, который присваивает приоритет типам звуков, что помогает выбрать, какой звук воспроизводить, если они сталкиваются друг с другом.
С помощью текущей системы вы можете легко работать с несколькими SoundInstance одновременно и создавать звук в соответствии с вашими потребностями.

