Skip to content

Commit

Permalink
added more page content and back end code
Browse files Browse the repository at this point in the history
  • Loading branch information
JR1811 committed Jul 23, 2024
1 parent b2dca21 commit c36362e
Show file tree
Hide file tree
Showing 20 changed files with 473 additions and 12 deletions.
115 changes: 113 additions & 2 deletions develop/sounds/dynamic-sounds.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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`.

<audio controls>
<source src="/assets/develop/sounds/dynamic-sounds/step_8.ogg" type="audio/ogg">
Your browser does not support the audio element.
</audio>

## 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)

###
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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() {

}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* If this class has been used once already, it keeps its instance stored
* in the static instance variable and return it.<br>
* 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) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.docs.sound;

public enum TransitionState {
STARTING, RUNNING, ENDING
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.docs.sound.instance;

Check failure on line 1 in reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

File does not end with a newline.

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;

Check failure on line 8 in reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

Unused import - net.minecraft.util.math.random.Random.

import com.example.docs.sound.AbstractDynamicSoundInstance;

Check failure on line 10 in reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

Unused import - com.example.docs.sound.AbstractDynamicSoundInstance.
// :::1
public class CustomSoundInstance extends MovingSoundInstance {

Check failure on line 12 in reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

blank line after {

Check failure on line 12 in reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

'CLASS_DEF' should be separated from previous line.

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.docs.sound.instance;

import com.example.docs.sound.AbstractDynamicSoundInstance;

import com.example.docs.sound.DynamicSoundSource;

Check failure on line 5 in reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

Extra separation in import group before 'com.example.docs.sound.DynamicSoundSource'

import net.minecraft.sound.SoundCategory;

Check failure on line 7 in reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

Wrong order for 'net.minecraft.sound.SoundCategory' import.
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);
}

Check failure on line 14 in reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

adjacent blank lines

Check failure on line 14 in reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

'}' has more than 1 empty lines after.

Check failure on line 15 in reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java

View workflow job for this annotation

GitHub Actions / mod

blank line before }

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -71,7 +75,9 @@ public static void initialize() {
itemGroup.add(ModBlocks.CONDENSED_OAK_LOG.asItem());
itemGroup.add(ModBlocks.PRISMARINE_LAMP.asItem());
});
};
}

;

// :::1
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<EngineBlock> CODEC = createCodec(EngineBlock::new);

public EngineBlock(Settings settings) {
super(settings);
}

@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {
return CODEC;
}

@Nullable
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new EngineBlockEntity(pos, state);
}

@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
return validateTicker(type, ModBlockEntities.ENGINE_BLOCK_ENTITY, EngineBlockEntity::tick);
}

@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {
if (!(world.getBlockEntity(pos) instanceof EngineBlockEntity blockEntity))
return super.onUse(state, world, pos, player, hit);

if (blockEntity.isTicking()) return super.onUse(state, world, pos, player, hit);
blockEntity.setTick(0); // starts ticking
return ActionResult.SUCCESS;
}
}
Loading

0 comments on commit c36362e

Please sign in to comment.