🇺🇦 Українська (Ukrainian - Ukraine)
🇺🇦 Українська (Ukrainian - Ukraine)
Зовнішній вигляд
🇺🇦 Українська (Ukrainian - Ukraine)
🇺🇦 Українська (Ukrainian - Ukraine)
Зовнішній вигляд
Ця сторінка написана для версії:
1.21.4
Ця сторінка написана для версії:
1.21.4
INFO
Ця сторінка доповнює сторінки відтворення звуків і створення власних звуків!
SoundEvents
Як ми дізналися на сторінці використання звуків, бажано використовувати SoundEvent
на стороні логічного сервера, навіть якщо це дещо суперечить інтуїції. Адже клієнту потрібно обробляти звук, який передається у ваші навушники, чи не так?
Такий спосіб мислення правильний. Технічно клієнтська сторона повинна обробляти аудіо. Однак для простого відтворення SoundEvent
сторона сервера підготувала великий проміжний крок, який може бути неочевидним на перший погляд. Які клієнти повинні чути цей звук?
Використання звуку на стороні логічного сервера вирішить проблему передачі SoundEvent
. Простіше кажучи, кожному клієнту (ClientPlayerEntity
) у діапазоні відстеження надсилається мережевий пакет для відтворення цього конкретного звуку. Звукова подія в основному транслюється зі сторони логічного сервера кожному клієнту, що бере участь, без необхідності про це взагалі думати. Звук відтворюється один раз із заданою гучністю та висотою.
Але що робити, якщо цього недостатньо? Що робити, якщо під час відтворення звуку потрібно динамічно змінювати гучність і висоту, і все це на основі значень, які надходять із таких речей, як Entities
або BlockEntities
?
Простого способу використання SoundEvent
на стороні логічного сервера недостатньо для цього випадку використання.
Ми збираємося створити новий зациклений звук для іншого SoundEvent
. Якщо ви можете знайти аудіофайл, який уже безперешкодно відтворюється, просто виконайте дії, описані в розділі створення власних звуків. Якщо звук ще не зациклюється ідеально, нам доведеться підготувати його до цього.
Знову ж таки, більшість сучасних DAW (Digital Audio Workstation Software) мають бути здатні на це, але я люблю використовувати Reaper, якщо редагування аудіо є дещо складнішим.
Наш початковий звук надходитиме від механізму.
Завантажмо файл у нашу DAW.
Ми можемо почути та побачити, що механізм запускається на початку та зупиняється в кінці, що не дуже добре для циклічних звуків. Виріжмо їх і налаштуємо маркери вибору часу відповідно до нової довжини. Також увімкніть режим Toggle Repeat
, щоб аудіо відтворювалося циклічно, поки ми його налаштовуємо.
Якщо ми уважно прислухаємося, на задньому фоні чути звуковий сигнал, який міг походити від машини. Я думаю, що в грі це не звучатиме чудово, тому спробуймо це видалити.
Це постійний звук, який зберігає свою частоту протягом усього аудіо. Тому простого фільтра еквалайзера має бути достатньо, щоб відфільтрувати його.
Reaper поставляється з уже обладнаним еквалайзером, який називається «ReaEQ». Це може бути розташовано в іншому місці та називатися по-іншому в інших DAW, але використання еквалайзера є стандартним у більшості DAW на сьогодні.
Якщо ви впевнені, що у вашій DAW немає доступного фільтра еквалайзера, перевірте наявність безплатних альтернатив VST в Інтернеті, які ви можете встановити у вибраній DAW.
У Reaper використовуйте вікно ефектів, щоб додати звуковий ефект «ReaEQ» або будь-який інший еквалайзер.
Якщо ми відтворимо аудіо зараз, утримуючи вікно фільтра еквалайзера відкритим, фільтр еквалайзера покаже вхідний аудіо на своєму дисплеї. Ми бачимо там багато нерівностей.
Якщо ви не є навченим аудіоінженером, ця частина здебільшого стосується експериментів і методу проб і помилок. Між вузлом 2 і 3 є досить сильна нерівність. Перемістімо вузли так, щоб знизити частоту лише для цієї частини.
Крім того, за допомогою простого фільтра еквалайзера можна досягти інших ефектів. Наприклад, скорочення високих і/або низьких частот може створити враження звуків, що передаються по радіо.
Ви також можете накладати більше аудіофайлів, змінювати висоту звуку, додавати трохи реверберації або використовувати складніші звукові ефекти, як-от "bit-crusher". Звуковий дизайн може бути цікавим, особливо якщо ви випадково знайдете хороші варіанти звучання вашого аудіо. Експериментування є ключовим, і, можливо, ваш звук стане ще кращим, ніж раніше.
Ми продовжимо лише фільтр еквалайзера, який ми використовували для вирізання проблемної частоти.
Порівняймо вихідний файл з очищеною версією.
Ви можете почути чітке дзижчання та звуковий сигнал, можливо, від електричного елемента механізму, в оригінальному звуці.
За допомогою фільтра еквалайзера ми змогли майже повністю видалити його. Однозначно приємніше слухати.
Якщо ми дозволимо звуку відтворити до кінця і знову розпочнемо спочатку, ми чітко почуємо, як відбувається перехід. Мета полягає в тому, щоб позбутися цього, застосувавши плавний перехід.
Почніть з того, що виріжте шматок з кінця, розміром якого ви хочете, щоб був перехід, і розмістіть його на початку нової звукової доріжки. У Reaper ви можете розділити аудіо, просто перемістивши курсор до місця розрізу та натиснувши S.
Можливо, вам також доведеться скопіювати звуковий ефект еквалайзера першої звукової доріжки на другу.
Тепер нехай кінцева частина нової доріжки зникне, а початок першої аудіодоріжки з’явиться.
Експортуйте аудіо з двома аудіодоріжками, але лише з одним аудіоканалом (моно) і створіть новий SoundEvent
для цього .ogg
файлу у вашому моді. Якщо ви не впевнені, як це зробити, перегляньте сторінку створення власних звуків.
Тепер це закінчена аудіосистема циклічного відтворення для SoundEvent
під назвою ENGINE_LOOP
.
SoundInstance
Щоб відтворювати звуки на стороні клієнта, потрібен SoundInstance
. Проте вони все ще використовують SoundEvent
.
Якщо ви хочете лише відтворити щось на зразок натискання елемента інтерфейсу, уже існує клас PositionedSoundInstance.
Майте на увазі, що це буде відтворено лише на конкретному клієнті, який виконував цю частину коду.
MinecraftClient client = MinecraftClient.getInstance();
client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
WARNING
Зверніть увагу, що в класі AbstractSoundInstance
, який SoundInstance
успадковує, є анотація @Environment(EnvType.CLIENT)
.
Це означає, що цей клас (і всі його підкласи) буде доступний лише клієнтській стороні.
Якщо ви спробуєте використати це в логічному контексті на стороні сервера, ви можете спочатку не помітити проблему в грі наодинці, але сервер у мережевому середовищі вийде з ладу, оскільки він взагалі не зможе знайти цю частину коду.
Якщо ви маєте проблеми з цими проблемами, рекомендуємо створити свій мод за допомогою онлайн-генератора шаблонів з увімкненою опцією Розділити клієнт і загальні джерела
.
SoundInstance
може бути потужнішим, ніж просто відтворення звуків один раз.
Перевірте клас AbstractSoundInstance
і які значення він може відстежувати. Окрім звичайних змінних гучності та висоти звуку, він також містить координати XYZ і якщо він має повторюватися після завершення SoundEvent
.
Потім, поглянувши на його підклас, MovingSoundInstance
, ми також отримуємо представлений інтерфейс TickableSoundInstance
, який додає функціонал тикання до SoundInstance
.
Тож, щоб скористатися цими утилітами, просто створіть новий клас для свого спеціального SoundInstance
і розширте його з MovingSoundInstance
.
public class CustomSoundInstance extends MovingSoundInstance {
private final LivingEntity entity;
public CustomSoundInstance(LivingEntity entity, SoundEvent soundEvent, SoundCategory soundCategory) {
super(soundEvent, soundCategory, SoundInstance.createRandom());
// 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.repeat = 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.isDead()) {
this.setDone();
return;
}
// move sound position over to the new position for every tick
this.setPositionToEntity();
}
@Override
public boolean shouldAlwaysPlay() {
// 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();
}
}
Використання власного екземпляра Entity
або BlockEntity
замість основного екземпляра LivingEntity
може дати вам ще більше контролю, наприклад, на основі методу tick()
на методи доступу, але вам не обов’язково потрібно посилання на таке джерело звуку. Натомість ви також можете отримати доступ до BlockPos
з іншого місця або навіть встановити його вручну лише один раз у конструкторі.
Просто майте на увазі, що всі об’єкти, на які посилаються, у SoundInstance
є версіями з боку клієнта. У певних ситуаціях властивості логічної сутності на стороні сервера можуть відрізнятися від властивостей на стороні клієнта. Якщо ви помітили, що ваші значення не збігаються, переконайтеся, що ваші значення синхронізовано з пакетами S2C TrackedData
, BlockEntity
сутності або повними спеціальними мережевими пакетами S2C.
Після того, як ви закінчили створювати свій власний SoundInstance
, його можна використовувати будь-де, якщо його було виконано на стороні клієнта за допомогою менеджера звуку. Таким же чином ви також можете зупинити настроюваний SoundInstance
вручну, якщо необхідно.
CustomSoundInstance instance = new CustomSoundInstance(client.player, CustomSounds.ENGINE_LOOP, SoundCategory.NEUTRAL);
// play the sound instance
client.getSoundManager().play(instance);
// stop the sound instance
client.getSoundManager().stop(instance);
Звуковий цикл тепер відтворюватиметься лише для клієнта, який запустив цей SoundInstance. У цьому випадку звук буде слідувати за самим ClientPlayerEntity
.
На цьому завершується пояснення створення та використання простого спеціального SoundInstance
.
WARNING
Наступний вміст охоплює складну тему.
На цьому етапі ви повинні добре знати Java, об'єктноорієнтоване програмування, генерики та системи зворотного виклику.
Знання щодо Entities
, BlockEntities
і користувацьких мереж також дуже допоможуть у розумінні сценарію використання та застосування розширених звуків.
Щоб показати приклад того, як можна створити складніші системи SoundInstance
, ми додамо додаткові функції, абстракції й утиліти, щоб зробити роботу з такими звуками в більшому обсязі, легшою, динамічнішою та гнучкою.
Подумаймо про те, яка наша мета із SoundInstance
.
EngineBlockEntity
SoundInstance
має рухатися, дотримуючись позиції свого власного EngineBlockEntity
(BlockEntity
не переміщуватиметься, тому це може бути кориснішим для Entities
)Підсумовуючи, нам потрібно відстежувати екземпляр спеціального BlockEntity
, регулюйте значення гучності та висоти під час роботи SoundInstance
на основі значень із цього спеціального BlockEntity
та реалізовуйте "Стани переходу".
Якщо ви плануєте створити кілька різних SoundInstance
, які поводяться по-різному, краще створити новий абстрактний клас AbstractDynamicSoundInstance
, який реалізує поведінку за замовчуванням і дозволяє фактичним власним класам SoundInstance
поширюватися з нього.
Якщо ви плануєте використовувати лише один, ви можете пропустити абстрактний суперклас і натомість реалізувати цю функцію безпосередньо у своєму спеціальному класі SoundInstance
.
Крім того, було б гарною ідеєю мати централізоване місце, де SoundInstance
відстежуються, відтворюються та зупиняються. Це означає, що він повинен обробляти вхідні сигнали, напр. із власних мережевих пакетів S2C, перерахувати всі поточні запущені екземпляри та обробляти особливі випадки, наприклад, які звуки дозволено відтворювати одночасно та які звуки можуть потенційно вимкнути інші звуки після активації. Для цього можна створити новий клас DynamicSoundManager
, щоб легко взаємодіяти з цією звуковою системою.
Загалом наша звукова система може виглядати так, коли ми закінчимо.
INFO
Усі ці переліки, інтерфейси та класи будуть створені заново. Налаштуйте систему та утиліти відповідно до конкретного випадку використання, як вважаєте за потрібне. Це лише приклад того, як можна підходити до таких тем.
DynamicSoundSource
Якщо ви вирішите створити новий, більш модульний, власний клас AbstractDynamicSoundInstance
як надклас, можливо, ви захочете посилатися не лише на один тип Entity
, а й на інші, або навіть на BlockEntity
.
У цьому випадку використання абстракції є ключовим. Замість посилань напр. спеціальний BlockEntity
безпосередньо, лише відстеження інтерфейсу, який надає дані, вирішує цю проблему.
Надалі ми використовуватимемо спеціальний інтерфейс під назвою DynamicSoundSource
. Він реалізований у всіх класах, які хочуть використовувати цю функціональність динамічного звуку, як-от ваш власний BlockEntity
, Entities або навіть, за допомогою Mixins, у вже існуючих класах, як-от ZombieEntity
. В основному він представляє лише необхідні дані джерела звуку.
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
Vec3d 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();
}
Після створення цього інтерфейсу обов’язково реалізуйте його також у необхідних класах.
INFO
Це утиліта, яку можна використовувати як на стороні клієнта, так і на стороні логічного сервера.
Отже, цей інтерфейс слід зберігати в загальних пакетах, а не в клієнтських пакетах, якщо ви використовуєте опцію «розділити джерела».
TransitionState
Як згадувалося раніше, ви можете припинити запуск SoundInstance
за допомогою SoundManager
клієнта, але це призведе до того, що SoundInstance миттєво замовкне. Наша мета полягає в тому, щоб, коли надходить сигнал зупинки, не зупинити звук, а виконати завершальну фазу його «перехідного стану». Лише після завершення фази завершення власний SoundInstance
слід зупинити.
TransitionState
— це щойно створений перелік, який містить три значення. Вони будуть використовуватися для відстеження фази звуку.
STARTING
: звук починає тихо, але повільно збільшуєтьсяRUNNING
: звук працює нормальноТехнічно простого переліку з фазами може бути достатньо.
public enum TransitionState {
STARTING, RUNNING, ENDING
}
Але коли ці значення надсилаються через мережу, ви можете визначити для них Identifier
або навіть додати інші власні значення.
public enum TransitionState {
STARTING("starting_phase"),
RUNNING("idle_phase"),
ENDING("ending_phase");
private final Identifier identifier;
TransitionState(String name) {
this.identifier = Identifier.of(FabricDocsReference.MOD_ID, name);
}
public Identifier getIdentifier() {
return identifier;
}
}
INFO
Знову ж таки, якщо ви використовуєте «розділені джерела», вам потрібно подумати про те, де ви будете використовувати цей перелік. Технічно лише спеціальні SoundInstance
, які доступні лише на стороні клієнта, використовуватимуть ці значення enum.
Але якщо цей перелік використовується десь ще, наприклад. у власних мережевих пакетах вам, можливо, доведеться розмістити цей перелік також у загальних пакетах замість пакетів лише для клієнта.
SoundInstanceCallback
Цей інтерфейс використовується як зворотний виклик. Наразі нам потрібен лише метод onFinished
, але ви можете додати власні методи, якщо вам потрібно надіслати також інші сигнали від об’єкта SoundInstance
.
public interface SoundInstanceCallback {
// deliver the custom SoundInstance, from which this signal originates,
// using the method parameters
<T extends AbstractDynamicSoundInstance> void onFinished(T soundInstance);
}
Впровадьте цей інтерфейс у будь-якому класі, який повинен мати можливість обробляти вхідні сигнали, наприклад, AbstractDynamicSoundInstance
, який ми незабаром створимо, і створимо функціональні можливості в самому спеціальному SoundInstance
.
AbstractDynamicSoundInstance
Давайте нарешті почнемо роботу над ядром динамічної системи SoundInstance
. AbstractDynamicSoundInstance
— це щойно створений абстрактний
клас. Він реалізує функції визначення за замовчуванням і утиліти наших власних SoundInstances
, які будуть успадковані від нього.
Ми можемо взяти CustomSoundInstance
з раніше і покращити його. Замість LivingEntity
ми будемо посилатися на наше DynamicSoundSource
. Крім того, ми визначимо більше властивостей.
TransitionState
для відстеження поточної фазиDynamicSoundManager
для остаточного очищення, коли SoundInstance
фактично завершеноpublic abstract class AbstractDynamicSoundInstance extends MovingSoundInstance {
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
// ...
}
Потім установіть початкові значення за замовчуванням для спеціального SoundInstance
у конструкторі абстрактного класу.
// ...
// set up default settings of the SoundInstance in this constructor
protected AbstractDynamicSoundInstance(DynamicSoundSource soundSource, SoundEvent soundEvent, SoundCategory soundCategory,
int startTransitionTicks, int endTransitionTicks, float maxVolume, float minPitch, float maxPitch,
SoundInstanceCallback callback) {
super(soundEvent, soundCategory, SoundInstance.createRandom());
// 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.repeat = true;
this.transitionState = TransitionState.STARTING;
this.setPositionToEntity();
}
// ...
Після завершення роботи конструктора вам потрібно дозволити SoundInstance
відтворюватися.
@Override
public boolean shouldAlwaysPlay() {
// override to true, so that the SoundInstance can start
// or add your own condition to the SoundInstance, if necessary
return true;
}
Тепер настає важлива частина цього динамічного SoundInstance
. На основі поточного такту екземпляра він може застосовувати різні значення та поведінку.
@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
}
Як бачите, ми ще не застосували тут модуляцію гучності та висоти. Ми застосовуємо лише спільну поведінку. Таким чином, у цьому класі AbstractDynamicSoundInstance
ми надаємо лише базову структуру та інструменти для підкласів, які можуть самі вирішувати, який тип звукової модуляції вони насправді хочуть застосувати.
Отже, розгляньмо кілька прикладів таких методів модуляції звуку.
// increase or decrease volume and pitch based on the current phase of the sound
protected void modulateSoundForTransition() {
float normalizedTick = switch (transitionState) {
case STARTING -> (float) this.transitionTick / this.startTransitionTicks;
case ENDING -> 1.0f - ((float) this.transitionTick / this.endTransitionTicks);
default -> 1.0f;
};
this.volume = MathHelper.lerp(normalizedTick, 0.0f, this.maxVolume);
}
// increase or decrease pitch based on the sound source's stress value
protected void modulateSoundForStress() {
this.pitch = MathHelper.lerp(this.soundSource.getNormalizedStress(), this.minPitch, this.maxPitch);
}
Як бачите, нормалізовані значення в поєднанні з лінійною інтерполяцією (lerp) допомагають формувати значення відповідно до бажаних меж аудіо. Майте на увазі, що якщо ви додаєте кілька методів, які змінюють одне й те саме значення, вам потрібно буде спостерігати та коригувати, як вони працюють разом один з одним.
Тепер нам просто потрібно додати решту службових методів, і ми закінчили з класом AbstractDynamicSoundInstance
.
// moves the sound instance position to the sound source's position
protected void setPositionToEntity() {
this.x = soundSource.getPosition().getX();
this.y = soundSource.getPosition().getY();
this.z = soundSource.getPosition().getZ();
}
// Sets the SoundInstance into its ending phase.
// This is especially useful for external access to this SoundInstance
public void end() {
this.transitionState = TransitionState.ENDING;
}
SoundInstance
Якщо ми подивимося на фактичний спеціальний клас SoundInstance
, який походить від новоствореного AbstractDynamicSoundInstance
, нам потрібно лише подумати про умови, які призведуть до зупинки звуку та яку звукову модуляцію ми хочемо застосувати.
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, SoundCategory 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
}
DynamicSoundManager
Ми обговорювали раніше, як відтворити та зупинити SoundInstance
. Щоб очистити, централізувати та керувати цими взаємодіями, ви можете створити власний обробник SoundInstance
, який будується на цьому.
Цей новий клас DynamicSoundManager
керуватиме спеціальними SoundInstances
, тому він також буде доступний лише для клієнта. Крім того, клієнт повинен дозволити існування лише одному екземпляру цього класу. Кілька звукових менеджерів для одного клієнта не мають особливого сенсу та ще більше ускладнюють взаємодію. Отже, використаймо "шаблон проєктування Singleton".
public class DynamicSoundManager implements SoundInstanceCallback {
// An instance of the client to use Minecraft's default SoundManager
private static final MinecraftClient client = MinecraftClient.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);
}
}
Отримавши правильну базову структуру, ви можете додати методи, необхідні для взаємодії зі звуковою системою.
// 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.getId().equals(soundEvent.id())) {
return Optional.of(activeSound);
}
}
return Optional.empty();
}
Замість того, щоб мати лише список усіх SoundInstances
, які зараз відтворюються, ви також можете відстежувати, які звуки відтворюють джерела звуку. Наприклад, двигун, який має два звуки двигуна одночасно, не матиме сенсу, тоді як кілька двигунів, які відтворюють відповідні звуки двигуна, є дійсним крайнім випадком. Заради простоти ми щойно створили List<AbstractDynamicSoundInstance>
, але в багатьох випадках HashMap
DynamicSoundSource
і AbstractDynamicSoundInstance
може бути кращим вибором.
Щоб використовувати цю систему звуків, просто скористайтеся методами DynamicSoundManager
або SoundInstance
. Використання onStartedTrackingBy
і onStoppedTrackingBy
від об’єктів або просто спеціальної мережі S2C, тепер ви можете запускати та зупиняти власні динамічні SoundInstance
.
private static void handleS2CEngineSoundPacket(EngineSoundInstancePacket packet, ClientPlayNetworking.Context context) {
ClientWorld world = context.client().world;
if (world == null) return;
DynamicSoundManager soundManager = DynamicSoundManager.getInstance();
if (world.getBlockEntity(packet.blockEntityPos()) instanceof EngineBlockEntity engineBlockEntity) {
if (packet.shouldStart()) {
soundManager.play(new EngineSoundInstance(engineBlockEntity,
CustomSounds.ENGINE_LOOP, SoundCategory.BLOCKS,
60, 30, 1.2f, 0.8f, 1.4f,
soundManager)
);
return;
}
}
if (!packet.shouldStart()) {
soundManager.getPlayingSoundInstance(CustomSounds.ENGINE_LOOP).ifPresent(AbstractDynamicSoundInstance::end);
}
}
Кінцевий продукт може регулювати свою гучність на основі звукової фази, щоб згладити переходи та змінити висоту на основі значення стресу, яке надходить від джерела звуку.
Ви можете додати інше значення до свого джерела звуку, яке відстежує значення «перегріву» і, крім того, дозволити шиплячому SoundInstance
повільно зникати, якщо значення вище 0, або додати новий інтерфейс до вашого спеціального динамічного SoundInstance
`s, який призначає значення пріоритету типам звуків, що допомагає вибрати звук для відтворення, якщо вони стикаються один з одним.
За допомогою поточної системи ви можете легко обробляти кілька SoundInstance
одночасно та створювати звук відповідно до ваших потреб.