Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>net.jafama</groupId>
<artifactId>jafama</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
Expand Down
61 changes: 48 additions & 13 deletions src/main/java/project_16x16/Audio.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,51 @@ public final class Audio {

private static final String BGM_PATH = "Audio/BGM/";
private static final String SFX_PATH = "Audio/SFX/";
private static float gainBGM = 0;
private static float gainMenuBGM = 0;
private static float gainGameBGM = 0;
private static float gainSFX = 0;

private static Minim minim;

/**
* Background music, which are referenced as enums.
* Background music, which are referenced as enums. Each track is either menu
* or game music; the two have separate volumes.
*/
public enum BGM {
TEST0("first_theme.mp3"), // TODO
TEST1("cyberSynthwave.mp3"), // TODO
TEST2("First_level_take_1.mp3"), TEST3("Intro_Title_Pause_Screen_Take1_Loopable.mp3");
TEST0("first_theme.mp3", false), // TODO
TEST1("cyberSynthwave.mp3", false), // TODO
TEST2("First_level_take_1.mp3", false),
TEST3("Intro_Title_Pause_Screen_Take1_Loopable.mp3", true), //
TEST4("Intro_Sax.mp3", true) //
;

private String filename;
private final boolean menu;

private BGM(String filename) {
private BGM(String filename, boolean menu) {
this.filename = filename;
this.menu = menu;
}

public String getPath() {
return BGM_PATH + filename;
}

/** @return true for menu music, false for in-game music */
public boolean isMenu() {
return menu;
}

private float gain() {
return menu ? gainMenuBGM : gainGameBGM;
}
}

/**
* Sound effects, which are referenced as enums.
*/
public enum SFX {
JUMP("jump.wav"), STEP("walk_single.wav"), ATTACK("melee_attack.wav");
JUMP("jump.wav"), STEP("walk_single.wav"), ATTACK("melee_attack.wav"), UI_HOVER("ui_hover.wav"), UI_CLICK("ui_click.wav");

private String filename;

Expand Down Expand Up @@ -84,7 +100,8 @@ public static void assignApplet(SideScroller sideScroller) {
System.err.println(bgm.getPath() + " not found.");
}
}
setGainBGM(Options.gainBGM);
setGainMenuBGM(Options.gainMenuBGM);
setGainGameBGM(Options.gainGameBGM);
setGainSFX(Options.gainSFX);
}

Expand Down Expand Up @@ -141,7 +158,7 @@ public static void play(BGM sound) {
}
}
if (BGM_MAP.containsKey(sound)) {
BGM_MAP.get(sound).setGain(gainBGM);
BGM_MAP.get(sound).setGain(sound.gain());
BGM_MAP.get(sound).loop();
} else {
System.err.println(sound.getPath() + " not found.");
Expand All @@ -167,13 +184,31 @@ public static void play(BGM sound, float gain) {
}

/**
* Sets gain (volume) for background music.
* Sets gain (volume) for menu background music.
*
* @param gain gain, in decibels (where negative is quieter). Default = 0.
*/
public static void setGainMenuBGM(float gain) {
gainMenuBGM = gain;
BGM_MAP.forEach((bgm, sound) -> {
if (bgm.isMenu()) {
sound.setGain(gain);
}
});
}

/**
* Sets gain (volume) for in-game background music.
*
* @param gain gain, in decibels (where negative is quieter). Default = 0.
*/
public static void setGainBGM(float gain) {
gainBGM = gain;
BGM_MAP.values().forEach(sound -> sound.setGain(gainBGM));
public static void setGainGameBGM(float gain) {
gainGameBGM = gain;
BGM_MAP.forEach((bgm, sound) -> {
if (!bgm.isMenu()) {
sound.setGain(gain);
}
});
}

/**
Expand Down
96 changes: 60 additions & 36 deletions src/main/java/project_16x16/Camera.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
/**
* Camera class. Extends {@link org.gicentre.utils.move.ZoomPan ZoomPan},
* offering some bespoke methods relating to Project-16x16. At the moment, the
* camera uses {@link PApplet#lerp(float, float, float) lerp()} to follow
* objects or go to target position.
* camera uses frame-rate independent exponential smoothing
* ({@link Time#smoothing(float)}) to follow objects or go to target position.
*
* @todo deadzone mode can be choppy when tracking; zoom-to-fit (multiple
* entities); setting position to mouse when camera is rotated [bugged]
Expand All @@ -26,10 +26,10 @@ public final class Camera extends ZoomPan {

private SideScroller applet;
/**
* Lerp constant for motion. Used for all motion easing (zoom, position and
* rotation).
* Smoothing rate (per second) for motion. Used for all motion easing (zoom,
* position and rotation).
*/
private float lerpSpeed = Constants.CAMERA_LERP;
private float smoothing = Constants.CAMERA_SMOOTHING;
private float zoom = 1.0f;
/**
* Target variables are used as the target for lerping.
Expand All @@ -54,10 +54,11 @@ public final class Camera extends ZoomPan {
private PVector offset;
private PVector followObjectOffset = new PVector(0, 0);
/**
* Specifies a camera translation offset when shaking (where it is randomised
* each frame). Used internally.
* Specifies a camera translation offset when shaking. It's applied on top of
* the (smoothed) pan offset, and removed again before the next frame's
* smoothing, so shaking never drifts the camera. Used internally.
*/
private PVector shakeOffset = new PVector(0, 0);
private final PVector shakeOffset = new PVector(0, 0);
/**
* Deadzone coordinate points. Can correspond to either screen or world
* coordinates, depending on which method was used to set them
Expand All @@ -76,9 +77,14 @@ public final class Camera extends ZoomPan {
private EditableObject followObject;
private float zoomMax = 100, zoomMin = 0;
/**
* Trauma is used internally to inform the magnitude of camera shake.
* Trauma is used internally to inform the magnitude of camera shake. Decays at
* traumaDecay per second.
*/
private float trauma = 0, traumaDecay = 0.015f;
private float trauma = 0, traumaDecay = 0.9f;
/** Shake at full trauma: max translation (px) and rotation (radians). */
private static final float SHAKE_MAX_OFFSET = 150, SHAKE_MAX_ROTATION = 0.35f;
/** How quickly the shake moves (noise units per second). */
private static final float SHAKE_FREQUENCY = 20;

/**
* The most basic constructor. Initialises the camera at position (0, 0).
Expand Down Expand Up @@ -201,45 +207,61 @@ public void release() {
*/
private void update() {
offset = new PVector(applet.width / 2, applet.height / 2);
final float lerpAmount = Time.smoothing(smoothing);

if (zoom != zoomTarget) {
zoom = PApplet.lerp(zoom, zoomTarget, lerpSpeed);
zoom = PApplet.lerp(zoom, zoomTarget, lerpAmount);
if (PApplet.abs(zoom - zoomTarget) < 0.0025) { //
zoom = zoomTarget;
}
}

rotation = PApplet.lerp(rotation, rotationTarget, lerpSpeed);
rotation = PApplet.lerp(rotation, rotationTarget, lerpAmount);
applet.translate(offset.x, offset.y);
applet.rotate(rotation + shakeRotationOffset);
applet.translate(-offset.x, -offset.y);
transform();

float scale = PApplet.lerp((float) getZoomScaleX(), zoom, lerpSpeed);
float scale = PApplet.lerp((float) getZoomScaleX(), zoom, lerpAmount);
setZoomScaleX(scale);
setZoomScaleY(scale);

// smooth the un-shaken pan offset (last frame's shake is removed first)
float panX = getPanOffset().x + shakeOffset.x;
float panY = getPanOffset().y + shakeOffset.y;
if (following && ((deadZoneScreen && !withinScreenDeadZone()) || ((deadZoneWorld && !withinWorldDeadZone())) || (!deadZoneScreen && !deadZoneWorld))) {
setPanOffset(PApplet.lerp(getPanOffset().x, ((-followObject.position.x - followObjectOffset.x + offset.x) * zoom), lerpSpeed) - shakeOffset.x,
PApplet.lerp(getPanOffset().y, ((-followObject.position.y - followObjectOffset.y + offset.y) * zoom), lerpSpeed) - shakeOffset.y);
panX = PApplet.lerp(panX, ((-followObject.position.x - followObjectOffset.x + offset.x) * zoom), lerpAmount);
panY = PApplet.lerp(panY, ((-followObject.position.y - followObjectOffset.y + offset.y) * zoom), lerpAmount);
} else if (!following) {
setPanOffset(PApplet.lerp(getPanOffset().x, ((targetPosition.x + offset.x) * zoom), lerpSpeed) - shakeOffset.x,
PApplet.lerp(getPanOffset().y, ((targetPosition.y + offset.y) * zoom), lerpSpeed) - shakeOffset.y);
panX = PApplet.lerp(panX, ((targetPosition.x + offset.x) * zoom), lerpAmount);
panY = PApplet.lerp(panY, ((targetPosition.y + offset.y) * zoom), lerpAmount);
}
updateShake();
setPanOffset(panX - shakeOffset.x, panY - shakeOffset.y);
logicalPosition = PVector.mult(PVector.sub(getPanOffset(), new PVector(applet.width / 2 * zoom, applet.height / 2 * zoom)), -1 / zoom);
}

if (trauma > 0) { // 50 and 0.35 seem suitable values
trauma -= traumaDecay;
/**
* Decays trauma and computes this frame's shake offsets. Shake follows smooth
* noise sampled over (game) time, so it looks the same at any frame rate.
*/
private void updateShake() {
trauma = Math.max(0, trauma - traumaDecay * Time.delta());
final float magnitude = trauma * trauma;
final float t = Time.millis() / 1000f * SHAKE_FREQUENCY;
shakeOffset.set(magnitude * shakeNoise(0, t) * SHAKE_MAX_OFFSET, magnitude * shakeNoise(1, t) * SHAKE_MAX_OFFSET);
shakeRotationOffset = magnitude * shakeNoise(2, t) * SHAKE_MAX_ROTATION;
}

float x = (trauma * trauma) * applet.random(-1, 1) * 50;
float y = (trauma * trauma) * applet.random(-1, 1) * 50;
shakeOffset = new PVector(x, y);
shakeRotationOffset = (trauma * trauma) * applet.random(-1, 1) * 0.35f;
if (trauma == 0) {
shakeOffset = new PVector(0, 0);
shakeRotationOffset = 0;
}
}
/**
* Smooth noise centred on 0. Scaled so its spread roughly matches a uniform
* random value in [-1, 1].
*
* @param channel independent noise stream (one per shake axis)
* @param t noise time
*/
private float shakeNoise(int channel, float t) {
return (applet.noise(channel * 100, t) - 0.5f) * 4;
}

/**
Expand Down Expand Up @@ -392,8 +414,9 @@ public void setCameraPosition(PVector position) {
*/
public void setCameraPositionNoLerp(PVector position) {
following = false;
PVector temp = PVector.sub(position, offset);
PVector temp = PVector.sub(position, new PVector(applet.width / 2, applet.height / 2)); // offset may not be set yet
this.setPanOffset(-temp.x, -temp.y);
shakeOffset.set(0, 0); // pan offset is now un-shaken
this.targetPosition = new PVector(-position.x, -position.y);
}

Expand Down Expand Up @@ -440,16 +463,17 @@ public void zoomOut(float amount) {
}

/**
* Specify lerp (linear interpolation) speed for camera motion. Default = 0.05.
* Since the lerp is calculated per-frame (after prior motion), the camera
* motion is effectively non-linear. Smaller values provide a smoother, less
* Specify the smoothing rate for camera motion (per second). Default =
* {@link Constants#CAMERA_SMOOTHING}. The camera closes the fraction
* {@code 1 - e^(-rate * t)} of the distance to its target after {@code t}
* seconds, regardless of frame rate. Smaller values provide a smoother, less
* snappy, slower camera.
*
* @param lerpSpeed Range = [0-1.0]
* @see {@link PApplet#lerp(float, float, float) lerp()}
* @param smoothing smoothing rate (per second)
* @see Time#smoothing(float)
*/
public void setLerpSpeed(float lerpSpeed) {
this.lerpSpeed = lerpSpeed;
public void setSmoothing(float smoothing) {
this.smoothing = smoothing;
}

/**
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/project_16x16/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@
*/
public final class Constants {

public static final float CAMERA_LERP = 0.05f;
/** Camera motion smoothing rate (per second); see {@link Time#smoothing(float)}. */
public static final float CAMERA_SMOOTHING = 3.08f;
public static final float CAMERA_ZOOM_MAX = 3.0f;
public static final float CAMERA_ZOOM_MIN = 0.3f;
public static final float GAME_GRAVITY = 1;
/** Downwards acceleration of entities (px/s²). */
public static final float GAME_GRAVITY = 3600;
/**
* Longest physics step (seconds). Longer frames are integrated in several
* sub-steps so fast-moving entities can't pass through collisions.
*/
public static final float PHYSICS_MAX_STEP = 1 / 120f;

public static final String GAME_FONT = "Font/font-pixel-48.vlw";

Expand All @@ -35,6 +42,7 @@ public final class Constants {
public static class Colors {

public static final int MENU_GREY = colorToRGB(29, 33, 45);
public static final int BLACK = colorToRGB(0, 0, 0);

}

Expand Down
17 changes: 15 additions & 2 deletions src/main/java/project_16x16/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,15 @@ public static enum Option {
// Graphics
TARGET_FPS,
UI_SCALE,
FULLSCREEN,
WINDOW_WIDTH,
WINDOW_HEIGHT,
MENU_PARTICLES,

// Audio
GAIN_BGM,
GAIN_BGM, // legacy (single music volume); seeds the menu/game music volumes
GAIN_MENU_BGM,
GAIN_GAME_BGM,
GAIN_SFX,
MUTE_BGM,
MUTE_SFX,
Expand Down Expand Up @@ -139,13 +145,20 @@ public static class DefaultKeys {
// Graphics settings
public static float uiScale = options.getInt(Option.UI_SCALE.toString(), 1);
public static int targetFrameRate = options.getInt(Option.TARGET_FPS.toString(), 60);
public static boolean fullscreen = options.getBoolean(Option.FULLSCREEN.toString(), false);
/** Windowed-mode window size (px); 0 = sized automatically from the display scale. */
public static int windowWidth = options.getInt(Option.WINDOW_WIDTH.toString(), 0);
public static int windowHeight = options.getInt(Option.WINDOW_HEIGHT.toString(), 0);
/** Density of the menu background particles: 0 = low, 1 = medium, 2 = high. */
public static int menuParticles = options.getInt(Option.MENU_PARTICLES.toString(), 1);

// Game settings
public static int snapSize = options.getInt(Option.SNAP_SIZE.toString(), 32);
public static int debugMode = options.getInt(Option.DEBUG_MODE.toString(), 2);

// Audio settings
public static float gainBGM = options.getFloat(Option.GAIN_BGM.toString(), 0);
public static float gainMenuBGM = options.getFloat(Option.GAIN_MENU_BGM.toString(), options.getFloat(Option.GAIN_BGM.toString(), 0));
public static float gainGameBGM = options.getFloat(Option.GAIN_GAME_BGM.toString(), options.getFloat(Option.GAIN_BGM.toString(), 0));
public static float gainSFX = options.getFloat(Option.GAIN_SFX.toString(), 0);
public static boolean muteBGM = options.getBoolean(Option.MUTE_BGM.toString(), false);
public static boolean muteSFX = options.getBoolean(Option.MUTE_SFX.toString(), false);
Expand Down
Loading
Loading