Skip to content

Commit

Permalink
finished first iteration of dynamic sound page
Browse files Browse the repository at this point in the history
  • Loading branch information
JR1811 committed Aug 26, 2024
1 parent 1fa69de commit 4be69c9
Show file tree
Hide file tree
Showing 21 changed files with 729 additions and 149 deletions.
274 changes: 208 additions & 66 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.
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
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.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() {
ReceiveS2C.initialize();
}

private void playSimpleSoundInstance() {
// :::1
MinecraftClient client = MinecraftClient.getInstance();
client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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;
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::handleEngineSound);
}

// ::: 1
private static void handleEngineSound(EngineSoundInstancePacket packet, ClientPlayNetworking.Context context) {
ClientWorld world = context.client().world;
if (world == null) return;
if (!(world.getBlockEntity(packet.blockEntityPos()) instanceof EngineBlockEntity engineBlockEntity)) return;

DynamicSoundManager soundManager = DynamicSoundManager.getInstance();

if (packet.shouldStart()) {
soundManager.play(new EngineSoundInstance(engineBlockEntity,
CustomSounds.ENGINE_LOOP, SoundCategory.BLOCKS,
60, 30, 1.2f, 0.8f, 1.4f,
soundManager)
);
} else {
soundManager.getPlayingSoundInstance(CustomSounds.ENGINE_LOOP).ifPresent(AbstractDynamicSoundInstance::end);
}
}
// ::: 1

public static void initialize() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,140 @@
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 {
private final DynamicSoundSource soundSource;
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) {

protected AbstractDynamicSoundInstance(DynamicSoundSource soundSource, SoundEvent soundEvent, SoundCategory soundCategory) {
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();
}

// ...
// :::1
// :::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.transitionTick = 0;
this.transitionState = TransitionState.ENDING;
}
// :::6
// :::1
}
// :::1

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

import net.minecraft.client.MinecraftClient;

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

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

public class DynamicSoundManager {
import com.example.docs.sound.instance.SoundInstanceCallback;

private static DynamicSoundManager instance;
// :::1
public class DynamicSoundManager implements SoundInstanceCallback {
// An instance of the client to use Minecraft's default SoundManager
private static final MinecraftClient client = MinecraftClient.getInstance();

private final List<? extends AbstractDynamicSoundInstance> activeSounds = new ArrayList<>();
// 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
}

/**
* 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.
*/
// 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) return new DynamicSoundManager();
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);
}

public void play(AbstractDynamicSoundInstance sound) {
client.getSoundManager().play(sound);
// 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

public void stop(AbstractDynamicSoundInstance sound) {
// :::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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ 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;
// In this constructor we also add the sound source (LivingEntity) of
// the SoundInstance and store it in the current object
this.entity = entity;
setPositionToEntity();
// set up default values when the sound is about to start
this.repeat = true;
this.setPositionToEntity();
}

@Override
Expand All @@ -31,15 +32,22 @@ public void tick() {
return;
}
// move sound position over to the new position for every tick
setPositionToEntity();
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 = entity.getX();
this.y = entity.getY();
this.z = entity.getZ();
this.x = this.entity.getX();
this.y = this.entity.getY();
this.z = this.entity.getZ();
}
}
// :::1
Loading

0 comments on commit 4be69c9

Please sign in to comment.