Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SoundInstances page #174

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions .vitepress/sidebars/develop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ export default [
text: "develop.sounds.custom",
link: "/develop/sounds/custom",
},
{
text: "develop.sounds.dynamic-sounds",
link: "/develop/sounds/dynamic-sounds",
},
],
},
{
Expand Down
376 changes: 376 additions & 0 deletions develop/sounds/dynamic-sounds.md

Large diffs are not rendered by default.

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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,35 @@
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.network.ReceiveS2C;
import com.example.docs.sound.CustomSounds;
import com.example.docs.sound.instance.CustomSoundInstance;

public class FabricDocsDynamicSound implements ClientModInitializer {

@Override
public void onInitializeClient() {
ReceiveS2C.initialize();
}

private void playSimpleSoundInstance() {
// :::1
MinecraftClient client = MinecraftClient.getInstance();
client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
// :::1
// :::2
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);
// :::2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.docs.network;

import net.minecraft.client.world.ClientWorld;
import net.minecraft.sound.SoundCategory;

import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import com.example.docs.block.entity.custom.EngineBlockEntity;

Check failure on line 7 in reference/latest/src/client/java/com/example/docs/network/ReceiveS2C.java

View workflow job for this annotation

GitHub Actions / mod

'com.example.docs.block.entity.custom.EngineBlockEntity' should be separated from previous imports.
import com.example.docs.networking.payload.EngineSoundInstancePacket;
import com.example.docs.sound.AbstractDynamicSoundInstance;
import com.example.docs.sound.CustomSounds;
import com.example.docs.sound.DynamicSoundManager;
import com.example.docs.sound.instance.EngineSoundInstance;

public class ReceiveS2C {
static {
ClientPlayNetworking.registerGlobalReceiver(EngineSoundInstancePacket.IDENTIFIER, ReceiveS2C::handleS2CEngineSoundPacket);
}

// :::1
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;
}
}

Check failure on line 35 in reference/latest/src/client/java/com/example/docs/network/ReceiveS2C.java

View workflow job for this annotation

GitHub Actions / mod

missing blank line after block at same indentation level

Check failure on line 35 in reference/latest/src/client/java/com/example/docs/network/ReceiveS2C.java

View workflow job for this annotation

GitHub Actions / mod

missing blank line before block at same indentation level
if (!packet.shouldStart()) {
soundManager.getPlayingSoundInstance(CustomSounds.ENGINE_LOOP).ifPresent(AbstractDynamicSoundInstance::end);
}
}

Check failure on line 39 in reference/latest/src/client/java/com/example/docs/network/ReceiveS2C.java

View workflow job for this annotation

GitHub Actions / mod

missing blank line after block at same indentation level
// :::1

public static void initialize() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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;
import net.minecraft.util.math.MathHelper;

import com.example.docs.sound.instance.SoundInstanceCallback;

// :::1
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

// ...
// :::1

// :::2
// ...

// 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();
}

// ...
// :::2

// :::3
@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;
}
// :::3

// :::4
@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
}
// :::4

// :::5
// 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);
}
// :::5

// :::6
// 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;
}
// :::6
// :::1
}
// :::1

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

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import net.minecraft.client.MinecraftClient;
import net.minecraft.sound.SoundEvent;

import com.example.docs.sound.instance.SoundInstanceCallback;

// :::1
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;
}
// :::1

// :::2
// 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.getId())) {
return Optional.of(activeSound);
}
}
return Optional.empty();
}
// :::2

// :::1

// 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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;

import com.example.docs.sound.AbstractDynamicSoundInstance;
// :::1
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();
}
}
// :::1
Loading
Loading