diff --git a/develop/sounds/dynamic-sounds.md b/develop/sounds/dynamic-sounds.md index 6f3a9a196..8929e48bf 100644 --- a/develop/sounds/dynamic-sounds.md +++ b/develop/sounds/dynamic-sounds.md @@ -12,7 +12,7 @@ This page covers an advanced topic and builds on top of the [Playing Sounds](../ At this point you should have a solid grasp on Java basics and object oriented programming. -Having knowledge on Entities or BlockEntities and custom networking will also help a lot in understanding the use case and the applications of advanced sounds. +Having knowledge on Entities, BlockEntities and custom networking will also help a lot in understanding the use case and the applications of advanced sounds. ::: ## The Issue with Normal `SoundEvents` {#the-issue-with-normal-soundevents} @@ -60,7 +60,7 @@ Reaper comes with an EQ filter equipped already, called "ReaEQ". This might be l If you are sure that your DAW doesn't have an EQ filter available, check for free VST alternatives online which you may be able to install in your DAW of choice. -In Reaper use the Effects Window to add the "ReaEQ" audio effect, or any other EQ filter of your choice. +In Reaper use the Effects Window to add the "ReaEQ" audio effect, or any other EQ. ![Adding an EQ filter](/assets/develop/sounds/dynamic-sounds/step_2.png) @@ -106,3 +106,114 @@ You may have to copy the audio effect of the first audio track over to the secon Now let the end piece of the new track fade out and let the start of the first audio track fade in. ![Looping with fading audio tracks](/assets/develop/sounds/dynamic-sounds/step_7.png) + +### Exporting {#exporting} + +Now export the audio with the two audio tracks, but with only one audio channel (Mono) and create a new `SoundEvent` for that `.ogg` file in your mod. +If you are not sure how to do that, take a look at the [Creating Custom Sounds](../sounds/custom) page. + +This is now the finished looping engine audio for the `SoundEvent`. + + + +## Using the `SoundEvent` on the Client Side{#using-the-sound-event-on-the-client-side} + +Sounds on the client side use a `SoundInstance`. make sure to use the `SoundManager` for it only on the client side. + +If you only want to play something like a click on an UI element, you can make use of the already existing `PositionedSoundInstance`. + +This will only be played on the client, which executed this part of the code. + +@[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java) + +A `SoundInstance` can be more powerful then just playing Sounds once. + +Check out the `AbstractSoundInstance` class and what kind of values it can keep track of. +Besides the usual volume and pitch variables, it also holds XYZ coordinates and if it should repeat itself after finishing the `SoundEvent`. + +Then taking a look at its sub class, `MovingSoundInstance` we get the `TickableSoundInstance` interface too, which keeps track of the `SoundInstance` ticks. + +Simply create a new class for your custom `SoundInstance` and extend from `MovingSoundInstance`. + +@[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java) + +Then just call this custom `SoundInstance` from the client side place, where you wan't to call it from. + +@[code lang=java transcludeWith=:::2](@/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java) + +The sound loop will be played now only for the client, which ran that SoundInstance. In this case the sound will follow the `ClientPlayerEntity` itself. + +## Dynamic and Interactive Sounds{#dynamic-and-interactive-sounds} + +If you plan on making several different `SoundInstances`, which behave in different ways, I would recommend to create a new `AbstractDynamicSoundInstance` class, +which implements the default behavior and let the actual custom `SoundInstance` classes extend from it. + +If you just plan on using a single one, then you can skip the abstract super class, and instead implement that functionality in your custom `SoundInstance` class directly. + +### Theory{#theory} + +Let's think about what our goal with the `SoundInstance` is. + +1. The sound should loop as long as the linked custom "EngineBlockEntity" is running +2. The sound position should move around, following its custom "EngineBlockEntity" position (more useful on Entities) +3. The engine sound should have smooth transitions. Turning it on or off should never be abrupt. + +To summarize, we need to keep track of a (client sided) instance of a custom BlockEntity, +adjust volume and pitch values, while the `SoundInstance` is running based on values from that custom BlockEntity and implement "Transition States". + +Technically you could stop running `SoundInstance`s with the client's `SoundManager` directly too, but this will cause the SoundInstance to go silent instantly. +Our goal is, when a stopping signal comes in, to not stop the sound instantly but to execute an ending phase of its "Transition State". + +A "Transition State" is a newly created enum, which contains three values. They will be used to keep track on what phase the sound should be in. + +- `STARTING` phase: sound starts silent, but slowly increases in volume +- `RUNNING` phase: sound is running normally +- `ENDING` phase: sound starts at the original volume and slowly decreases until it is silent + +Only after `ENDING` phase finished, the custom `SoundInstance` is allowed to be stopped. + +Putting all of those building blocks together also raises the question, where we even start and stop sounds, +and where the signals e.g. from custom S2C Network Packets get processed. + +For that a new DynamicSoundManager class can be created, to easily interact with this sound system. + +Overall our sound system might look like this, when we are done. + +![Structure of the custom Sound System](/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg) + +### Creating the New `SoundInstance` {#creating-the-new-soundinstance} + +Start of by passing over the necessary sound source over the constructor. Your `SoundInstance` can keep track of client sided objects, so having e.g. your +custom Entity stored in there is not a bad idea. + +```java +public class EngineSoundInstance extends MovingSoundInstance { + private final EngineBlockEntity EngineBlockEntity; + + public EngineSoundInstance(EngineBlockEntity EngineBlockEntity, SoundEvent soundEvent, SoundCategory soundCategory) { + super(soundEvent, soundCategory, SoundInstance.createRandom()); + this.EngineBlockEntity = EngineBlockEntity; + } + //... +} +``` + +If you choose to make use of a new, more modular, custom `AbstractDynamicSoundInstance` class as a super class, +you may want to use that class not only on a single type of Entity but on different ones, or even on BlockEntities too. In that case making use of abstraction is the key. + +Instead of referencing a custom BlockEntity directly, only keeping track of an Interface, which provides the data, might be better in this case. +Going forward we will make use of a custom Interface called `DynamicSoundSource`. It is implemented in all classes which want to make use of that dynamic sound functionality. +It basically represents the Entity, BlockEntity or other objects which define the sound behavior. + +If you don't want to do that, just use your custom BlockEntity directly instead. + +@[code lang=java transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/sound/DynamicSoundSource.java) + +In the custom abstract `SoundInstance` parent class we will now use that Interface instead of the direct BlockEntity. + +@[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java) + +### \ No newline at end of file diff --git a/public/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg b/public/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg new file mode 100644 index 000000000..5e93de1ae Binary files /dev/null and b/public/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg differ diff --git a/public/assets/develop/sounds/dynamic-sounds/step_8.ogg b/public/assets/develop/sounds/dynamic-sounds/step_8.ogg new file mode 100644 index 000000000..ccf2bf931 Binary files /dev/null and b/public/assets/develop/sounds/dynamic-sounds/step_8.ogg differ diff --git a/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java b/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java new file mode 100644 index 000000000..2ece67855 --- /dev/null +++ b/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java @@ -0,0 +1,29 @@ +package com.example.docs; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; + +import net.fabricmc.api.ClientModInitializer; +import com.example.docs.sound.CustomSounds; +import com.example.docs.sound.DynamicSoundManager; +import com.example.docs.sound.instance.CustomSoundInstance; + +public class FabricDocsDynamicSound implements ClientModInitializer { + + public static final DynamicSoundManager SOUND_MANAGER = DynamicSoundManager.getInstance(); + + @Override + public void onInitializeClient() { + // :::1 + MinecraftClient client = MinecraftClient.getInstance(); + client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + // :::1 + // :::2 + client.getSoundManager().play( + new CustomSoundInstance(client.player, CustomSounds.ENGINE_LOOP, SoundCategory.NEUTRAL) + ); + // :::2 + } +} diff --git a/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java b/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java new file mode 100644 index 000000000..00e86bf61 --- /dev/null +++ b/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java @@ -0,0 +1,25 @@ +package com.example.docs.sound; + +import net.minecraft.client.sound.MovingSoundInstance; +import net.minecraft.client.sound.SoundInstance; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; + +// :::1 +public abstract class AbstractDynamicSoundInstance extends MovingSoundInstance { + private final DynamicSoundSource soundSource; + + protected AbstractDynamicSoundInstance(DynamicSoundSource soundSource, SoundEvent soundEvent, SoundCategory soundCategory) { + super(soundEvent, soundCategory, SoundInstance.createRandom()); + this.soundSource = soundSource; + + + } + // ... + // :::1 + + @Override + public void tick() { + + } +} diff --git a/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java b/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java new file mode 100644 index 000000000..db6283cf0 --- /dev/null +++ b/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java @@ -0,0 +1,42 @@ +package com.example.docs.sound; + +import net.minecraft.client.MinecraftClient; + +import java.util.ArrayList; +import java.util.List; + +public class DynamicSoundManager { + + private static DynamicSoundManager instance; + private static final MinecraftClient client = MinecraftClient.getInstance(); + + private final List extends AbstractDynamicSoundInstance> activeSounds = new ArrayList<>(); + + private DynamicSoundManager() { + // private constructor to make sure that the normal + // instantiation of that object is not used externally + } + + /** + * This "Singleton Design Pattern" makes sure that, at runtime, + * only one instance of this class can exist. + *
+ * If this class has been used once already, it keeps its instance stored
+ * in the static instance variable and return it.
+ * Otherwise, the instance variable is not initialized yet (null).
+ * It will create a new instance, use it
+ * and store it in the static variable for next uses.
+ */
+ public static DynamicSoundManager getInstance() {
+ if (instance == null) return new DynamicSoundManager();
+ return instance;
+ }
+
+ public void play(AbstractDynamicSoundInstance sound) {
+ client.getSoundManager().play(sound);
+ }
+
+ public void stop(AbstractDynamicSoundInstance sound) {
+
+ }
+}
diff --git a/reference/latest/src/client/java/com/example/docs/sound/TransitionState.java b/reference/latest/src/client/java/com/example/docs/sound/TransitionState.java
new file mode 100644
index 000000000..1a87ac688
--- /dev/null
+++ b/reference/latest/src/client/java/com/example/docs/sound/TransitionState.java
@@ -0,0 +1,5 @@
+package com.example.docs.sound;
+
+public enum TransitionState {
+ STARTING, RUNNING, ENDING
+}
diff --git a/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java b/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java
new file mode 100644
index 000000000..ae8c317d3
--- /dev/null
+++ b/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java
@@ -0,0 +1,46 @@
+package com.example.docs.sound.instance;
+
+import net.minecraft.client.sound.MovingSoundInstance;
+import net.minecraft.client.sound.SoundInstance;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.util.math.random.Random;
+
+import com.example.docs.sound.AbstractDynamicSoundInstance;
+// :::1
+public class CustomSoundInstance extends MovingSoundInstance {
+
+ private final LivingEntity entity;
+
+ // Here we pass over the sound source of the SoundInstance and store it in the instance.
+ public CustomSoundInstance(LivingEntity entity, SoundEvent soundEvent, SoundCategory soundCategory) {
+ super(soundEvent, soundCategory, SoundInstance.createRandom());
+
+ // here we can set up values when the sound is about to start.
+ this.repeat = true;
+ this.entity = entity;
+ 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
+ setPositionToEntity();
+ }
+
+ // small utility method to move the sound instance position
+ // to the sound source's position
+ private void setPositionToEntity() {
+ this.x = entity.getX();
+ this.y = entity.getY();
+ this.z = entity.getZ();
+ }
+}
+// :::1
\ No newline at end of file
diff --git a/reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java b/reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java
new file mode 100644
index 000000000..1c728856c
--- /dev/null
+++ b/reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java
@@ -0,0 +1,17 @@
+package com.example.docs.sound.instance;
+
+import com.example.docs.sound.AbstractDynamicSoundInstance;
+
+import com.example.docs.sound.DynamicSoundSource;
+
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.util.math.random.Random;
+
+public class EngineSoundInstance extends AbstractDynamicSoundInstance {
+ protected EngineSoundInstance(DynamicSoundSource source, SoundEvent soundEvent, SoundCategory soundCategory, Random random) {
+ super(source, soundEvent, soundCategory);
+ }
+
+
+}
diff --git a/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java b/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java
index c7ec6ea98..cae8ddc3a 100644
--- a/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java
+++ b/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java
@@ -11,8 +11,8 @@
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents;
-
import com.example.docs.FabricDocsReference;
+import com.example.docs.block.custom.EngineBlock;
import com.example.docs.block.custom.PrismarineLampBlock;
import com.example.docs.item.ModItems;
@@ -44,6 +44,10 @@ public class ModBlocks {
), "prismarine_lamp", true
);
// :::4
+ public static final Block ENGINE_BLOCK = register(
+ new EngineBlock(AbstractBlock.Settings.create()), "engine", true
+ );
+
// :::1
public static Block register(Block block, String name, boolean shouldRegisterItem) {
// Register the block and its item.
@@ -71,7 +75,9 @@ public static void initialize() {
itemGroup.add(ModBlocks.CONDENSED_OAK_LOG.asItem());
itemGroup.add(ModBlocks.PRISMARINE_LAMP.asItem());
});
- };
+ }
+
+ ;
// :::1
}
diff --git a/reference/latest/src/main/java/com/example/docs/block/custom/EngineBlock.java b/reference/latest/src/main/java/com/example/docs/block/custom/EngineBlock.java
new file mode 100644
index 000000000..62e3042b0
--- /dev/null
+++ b/reference/latest/src/main/java/com/example/docs/block/custom/EngineBlock.java
@@ -0,0 +1,52 @@
+package com.example.docs.block.custom;
+
+import com.mojang.serialization.MapCodec;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.BlockWithEntity;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityTicker;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import org.jetbrains.annotations.Nullable;
+
+import com.example.docs.block.entity.ModBlockEntities;
+import com.example.docs.block.entity.custom.EngineBlockEntity;
+
+public class EngineBlock extends BlockWithEntity {
+ public static final MapCodec