From f1b47b2bfe9d61a9cfbc624a83df61879b796f67 Mon Sep 17 00:00:00 2001 From: String Date: Mon, 12 Jan 2026 02:45:40 -0600 Subject: [PATCH 1/6] Completely rework screens and fix a bug with tweens --- assets/another_test.groovy | 5 +- assets/test.groovy | 10 +- .../java/me/stringfromjava/funkin/Funkin.java | 129 ++++++++++-------- .../me/stringfromjava/funkin/FunkinGame.java | 110 +++++++++++++-- .../funkin/audio/FunkinSound.java | 11 +- .../funkin/backend/FunkinReflect.java | 53 +++++++ .../funkin/backend/{system => }/Paths.java | 2 +- .../funkin/backend/Reflect.java | 18 --- .../funkin/backend/display/FunkinScreen.java | 100 -------------- .../funkin/game/InitScreen.java | 2 +- .../funkin/game/menus/TitleScreen.java | 18 +-- .../funkin/graphics/screen/FunkinScreen.java | 58 ++++++++ .../funkin/graphics/sprite/FunkinObject.java | 7 + .../funkin/graphics/sprite/FunkinSprite.java | 14 ++ .../text/FunkinText.java} | 6 +- .../funkin/polyverse/Polyverse.java | 6 +- .../funkin/tween/FunkinTween.java | 7 +- .../stringfromjava/funkin/util/Constants.java | 24 +++- .../system => util/signal}/FunkinSignal.java | 12 +- .../funkin/util/signal/FunkinSignalData.java | 21 +++ gradle.properties | 1 - 21 files changed, 395 insertions(+), 219 deletions(-) create mode 100644 core/src/main/java/me/stringfromjava/funkin/backend/FunkinReflect.java rename core/src/main/java/me/stringfromjava/funkin/backend/{system => }/Paths.java (90%) delete mode 100644 core/src/main/java/me/stringfromjava/funkin/backend/Reflect.java delete mode 100644 core/src/main/java/me/stringfromjava/funkin/backend/display/FunkinScreen.java create mode 100644 core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java create mode 100644 core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinObject.java create mode 100644 core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java rename core/src/main/java/me/stringfromjava/funkin/{backend/display/text/DisplayText.java => graphics/text/FunkinText.java} (77%) rename core/src/main/java/me/stringfromjava/funkin/{backend/system => util/signal}/FunkinSignal.java (87%) create mode 100644 core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java diff --git a/assets/another_test.groovy b/assets/another_test.groovy index b160cbc..0051f55 100644 --- a/assets/another_test.groovy +++ b/assets/another_test.groovy @@ -3,7 +3,8 @@ import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.Sprite import me.stringfromjava.funkin.Funkin -import me.stringfromjava.funkin.backend.system.Paths +import me.stringfromjava.funkin.backend.Paths +import me.stringfromjava.funkin.graphics.sprite.FunkinSprite import me.stringfromjava.funkin.polyverse.script.type.Script import me.stringfromjava.funkin.util.Constants @@ -27,7 +28,7 @@ class AnotherTestClass extends Script { super.onRender(delta) if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { - var sprite = new Sprite(new Texture(Paths.image("pauseAlt/bfLol"))) + var sprite = new FunkinSprite(new Texture(Paths.image("pauseAlt/bfLol"))) var randomPosX = new Random().nextInt(Constants.Display.WINDOW_WIDTH) var randomPosY = new Random().nextInt(Constants.Display.WINDOW_HEIGHT) diff --git a/assets/test.groovy b/assets/test.groovy index a3adf17..35af7c6 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -2,10 +2,10 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Texture -import com.badlogic.gdx.graphics.g2d.Sprite import me.stringfromjava.funkin.Funkin -import me.stringfromjava.funkin.backend.display.FunkinScreen -import me.stringfromjava.funkin.backend.system.Paths +import me.stringfromjava.funkin.graphics.screen.FunkinScreen +import me.stringfromjava.funkin.backend.Paths +import me.stringfromjava.funkin.graphics.sprite.FunkinSprite import me.stringfromjava.funkin.polyverse.script.type.SystemScript class TestScript extends SystemScript { @@ -38,13 +38,13 @@ class TestScript extends SystemScript { class TestScreen extends FunkinScreen { - private Sprite test + private FunkinSprite test @Override void show() { super.show() - test = new Sprite(new Texture(Paths.image('NOTE_hold_assets'))) + test = new FunkinSprite(new Texture(Paths.image('NOTE_hold_assets'))) add(test) bgColor = new Color(0, 1, 0, 1) diff --git a/core/src/main/java/me/stringfromjava/funkin/Funkin.java b/core/src/main/java/me/stringfromjava/funkin/Funkin.java index 8290389..7213040 100644 --- a/core/src/main/java/me/stringfromjava/funkin/Funkin.java +++ b/core/src/main/java/me/stringfromjava/funkin/Funkin.java @@ -3,11 +3,13 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.scenes.scene2d.Stage; import me.stringfromjava.funkin.audio.FunkinSound; -import me.stringfromjava.funkin.backend.display.FunkinScreen; -import me.stringfromjava.funkin.backend.system.FunkinSignal; -import me.stringfromjava.funkin.backend.system.Paths; +import me.stringfromjava.funkin.graphics.screen.FunkinScreen; +import me.stringfromjava.funkin.util.signal.FunkinSignal; +import me.stringfromjava.funkin.backend.Paths; import me.stringfromjava.funkin.util.Constants; +import me.stringfromjava.funkin.util.signal.FunkinSignalData.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -74,23 +76,23 @@ public static void initialize(FunkinGame gameInstance) { * Sets the current screen to the provided screen. This is just a more direct version of {@code * Funkin.game.setScreen(screen)} with some extra functionality put into it. * - * @param screen The new {@code FunkinScreen} to set as the current screen. + * @param newScreen The new {@code FunkinScreen} to set as the current screen. */ - public static void setScreen(FunkinScreen screen) { - Signals.preScreenSwitch.dispatch(new Signals.ScreenSwitchSignalData(screen)); + public static void setScreen(FunkinScreen newScreen) { + Signals.preScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); if (!initialized) { throw new IllegalStateException("FNF:JE has not been initialized yet!"); } - if (screen == null) { + if (newScreen == null) { throw new IllegalArgumentException("Screen cannot be null!"); } if (Funkin.screen != null) { Funkin.screen.hide(); Funkin.screen.dispose(); } - Funkin.screen = screen; - game.setScreen(screen); - Signals.postScreenSwitch.dispatch(new Signals.ScreenSwitchSignalData(screen)); + Funkin.screen = newScreen; + Funkin.screen.show(); + Signals.postScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); } /** @@ -150,7 +152,7 @@ public static Music playMusic(String path, float volume, boolean looped) { } public static void info(Object message) { - info("Funkin", message); + info(Constants.System.LOG_TAG, message); } public static void info(String tag, Object message) { @@ -158,7 +160,7 @@ public static void info(String tag, Object message) { } public static void warn(Object message) { - warn("Funkin", message); + warn(Constants.System.LOG_TAG, message); } public static void warn(String tag, Object message) { @@ -166,55 +168,27 @@ public static void warn(String tag, Object message) { } public static void error(String message) { - error("Funkin", message); + error(Constants.System.LOG_TAG, message, null); } public static void error(String tag, Object message) { - outputLog(tag, message, FunkinLogLevel.ERROR); + error(tag, message, null); } - public static FunkinGame getGame() { - return game; + public static void error(String tag, Object message, Throwable throwable) { + String msg = + (throwable != null) + ? (message + " | Exception: " + throwable.toString()) + : message.toString(); + outputLog(tag, msg, FunkinLogLevel.ERROR); } - // ====================================== - // UTILITY FUNCTIONS, IGNORE BELOW - // ====================================== - - private static void outputLog(String tag, Object message, FunkinLogLevel level) { - String color = switch (level) { - case INFO -> Constants.Colors.WHITE; - case WARN -> Constants.Colors.YELLOW; - case ERROR -> Constants.Colors.RED; - }; - - boolean underline = (level == FunkinLogLevel.ERROR); - String timeAndDate = colorText( - LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " ", - color, true, false, underline); - String formattedTag = colorText("[" + tag + "] [" + level + "] ", color, true, false, underline); - String formattedMessage = colorText(message.toString(), color, false, true, underline); - - System.out.println(timeAndDate + formattedTag + formattedMessage); + public static FunkinGame getGame() { + return game; } - - private static String colorText( - String text, String color, boolean bold, boolean italic, boolean underline) { - StringBuilder sb = new StringBuilder(); - if (bold) { - sb.append(Constants.Colors.BOLD); - } - if (italic) { - sb.append(Constants.Colors.ITALIC); - } - if (underline) { - sb.append(Constants.Colors.UNDERLINE); - } - sb.append(color); - sb.append(text); - sb.append(Constants.Colors.RESET); - return sb.toString(); + public static Stage getStage() { + return game.stage; } /** @@ -236,16 +210,59 @@ public static class Signals { new FunkinSignal<>(); public static final FunkinSignal preGameClose = new FunkinSignal<>(); public static final FunkinSignal postGameClose = new FunkinSignal<>(); + public static final FunkinSignal windowFocused = new FunkinSignal<>(); + public static final FunkinSignal windowUnfocused = new FunkinSignal<>(); + public static final FunkinSignal windowMinimized = + new FunkinSignal<>(); public static final FunkinSignal preSoundPlayed = new FunkinSignal<>(); public static final FunkinSignal postSoundPlayed = new FunkinSignal<>(); - public record RenderSignalData(float delta) {} + private Signals() {} + } - public record ScreenSwitchSignalData(FunkinScreen screen) {} + // ====================================== + // UTILITY FUNCTIONS, IGNORE BELOW + // ====================================== - public record SoundPlayedSignalData(FunkinSound sound) {} + private static void outputLog(String tag, Object message, FunkinLogLevel level) { + String color = + switch (level) { + case INFO -> Constants.AsciiCodes.WHITE; + case WARN -> Constants.AsciiCodes.YELLOW; + case ERROR -> Constants.AsciiCodes.RED; + }; - private Signals() {} + boolean underline = (level == FunkinLogLevel.ERROR); + String timeAndDate = + colorText( + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " ", + color, + true, + false, + underline); + String formattedTag = + colorText("[" + tag + "] [" + level + "] ", color, true, false, underline); + String formattedMessage = colorText(message.toString(), color, false, true, underline); + + System.out.println(timeAndDate + formattedTag + formattedMessage); + } + + private static String colorText( + String text, String color, boolean bold, boolean italic, boolean underline) { + StringBuilder sb = new StringBuilder(); + if (bold) { + sb.append(Constants.AsciiCodes.BOLD); + } + if (italic) { + sb.append(Constants.AsciiCodes.ITALIC); + } + if (underline) { + sb.append(Constants.AsciiCodes.UNDERLINE); + } + sb.append(color); + sb.append(text); + sb.append(Constants.AsciiCodes.RESET); + return sb.toString(); } private Funkin() {} diff --git a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java index 0cac92f..481fd26 100644 --- a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java +++ b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java @@ -1,54 +1,135 @@ package me.stringfromjava.funkin; -import com.badlogic.gdx.Game; +import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Sound; -import me.stringfromjava.funkin.backend.system.Paths; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.utils.ScreenUtils; +import com.badlogic.gdx.utils.viewport.FitViewport; +import me.stringfromjava.funkin.graphics.screen.FunkinScreen; +import me.stringfromjava.funkin.backend.Paths; import me.stringfromjava.funkin.game.InitScreen; +import me.stringfromjava.funkin.graphics.sprite.FunkinObject; +import me.stringfromjava.funkin.graphics.sprite.FunkinSprite; import me.stringfromjava.funkin.polyverse.Polyverse; import me.stringfromjava.funkin.polyverse.script.type.Script; import me.stringfromjava.funkin.polyverse.script.type.SystemScript; import me.stringfromjava.funkin.tween.FunkinTween; +import me.stringfromjava.funkin.util.Constants; import java.util.Set; -import static me.stringfromjava.funkin.Funkin.Signals.RenderSignalData; +import me.stringfromjava.funkin.util.signal.FunkinSignalData; /** - * An enhanced version of libGDX's {@link Game} object. + * The game object used for containing the main loop and core elements of Funkin'. * *

If you want to change what happens to the pre and window configurations, you might want to see * {@code Lwjgl3Launcher} in the {@code lwjgl3} folder. */ -public class FunkinGame extends Game { +public class FunkinGame implements ApplicationListener { /** Is the game's window currently minimized? */ protected boolean isMinimized = false; + /** The main stage used for rendering all screens and sprites on screen. */ + protected Stage stage; + + /** The main viewport used to fit the world no matter the screen size. */ + protected FitViewport viewport; + + /** The main camera used to see the world. */ + protected OrthographicCamera camera; + + /** The main sprite batch used for rendering all sprites on screen. */ + protected SpriteBatch batch; + + /** The 1x1 texture used to draw the background color of the current screen. */ + protected Texture bgTexture; + @Override public void create() { + // Configure the main view of the game's world and view. + var wWidth = Constants.Display.WINDOW_WIDTH; + var wHeight = Constants.Display.WINDOW_HEIGHT; + + batch = new SpriteBatch(); + viewport = new FitViewport(wWidth, wHeight); + viewport.apply(); + + camera = new OrthographicCamera(); + camera.setToOrtho(false, wWidth, wHeight); + + stage = new Stage(viewport, batch); + + Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888); + pixmap.setColor(Color.WHITE); + pixmap.fill(); + bgTexture = new Texture(pixmap); + pixmap.dispose(); + + // Configure the Polyverse scripting and modding engine for the game. configurePolyverse(); - setScreen(new InitScreen()); + Funkin.setScreen(new InitScreen()); + } + + @Override + public void resize(int width, int height) { + viewport.update(width, height, true); } @Override public void render() { - super.render(); float delta = Gdx.graphics.getDeltaTime(); + FunkinScreen screen = Funkin.screen; + + Funkin.Signals.preRender.dispatch(new FunkinSignalData.RenderSignalData(delta)); + + // Update and render the current screen that's active. + if (screen != null) { + ScreenUtils.clear(Color.BLACK); + viewport.apply(); + batch.setProjectionMatrix(camera.combined); + batch.begin(); + batch.setColor(screen.getBgColor()); + batch.draw(bgTexture, 0, 0, viewport.getWorldWidth(), viewport.getWorldHeight()); + batch.setColor(Color.WHITE); // Set color back to white so display objects aren't affected. + screen.render(delta); + for (FunkinObject object : screen.members) { + if (object instanceof FunkinSprite) { + ((FunkinSprite) object).draw(batch); + } + } + } + batch.end(); - Funkin.Signals.preRender.dispatch(new RenderSignalData(delta)); + // Update the current screen and stage. + stage.act(delta); + stage.draw(); FunkinTween.globalManager.update(delta); Polyverse.forAllScripts(script -> script.onRender(delta)); - Funkin.Signals.postRender.dispatch(new RenderSignalData(delta)); + Funkin.Signals.postRender.dispatch(new FunkinSignalData.RenderSignalData(delta)); } + @Override + public void pause() {} + + @Override + public void resume() {} + /** Called when the user regains focus on the game's window. */ public void onWindowFocused() { Funkin.masterVolume = 1.0f; Funkin.music.setVolume(1); + Funkin.Signals.windowFocused.dispatch(); Funkin.info("Game window has regained focus."); } @@ -59,6 +140,8 @@ public void onWindowUnfocused() { } Funkin.masterVolume = 0.008f; Funkin.music.setVolume(0.008f); + Funkin.Signals.windowMinimized.dispatch( + new FunkinSignalData.WindowMinimizedSignalData(isMinimized)); Funkin.info("Game window has lost focus."); } @@ -75,6 +158,7 @@ public void onWindowMinimized(boolean iconified) { } Funkin.masterVolume = 0.0f; Funkin.music.setVolume(0); + Funkin.Signals.windowMinimized.dispatch(); Funkin.info("Game window has been minimized."); } @@ -84,6 +168,14 @@ public void dispose() { Funkin.Signals.preGameClose.dispatch(); + // Dispose of the main stage, current screen and batch. + Funkin.info("Disposing the screen display..."); + Funkin.screen.hide(); + Funkin.screen.dispose(); + stage.dispose(); + batch.dispose(); + bgTexture.dispose(); + // Dispose of all sounds and the music (if there is any playing). Funkin.info("Disposing music..."); if (Funkin.music != null) { diff --git a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java b/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java index 77c199e..ee9c9c4 100644 --- a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java +++ b/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java @@ -4,7 +4,8 @@ import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.files.FileHandle; import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.backend.system.Paths; +import me.stringfromjava.funkin.backend.Paths; +import me.stringfromjava.funkin.util.signal.FunkinSignalData; /** * An enhanced version of libGDX's {@link Sound}. @@ -56,13 +57,13 @@ public long play(float volume, float pitch) { @Override public long play(float volume, float pitch, float pan) { - Funkin.Signals.preSoundPlayed.dispatch(new Funkin.Signals.SoundPlayedSignalData(this)); + Funkin.Signals.preSoundPlayed.dispatch(new FunkinSignalData.SoundPlayedSignalData(this)); this.volume = volume * Funkin.masterVolume; this.pitch = pitch; this.pan = pan; this.looping = false; this.isPaused = false; - Funkin.Signals.postSoundPlayed.dispatch(new Funkin.Signals.SoundPlayedSignalData(this)); + Funkin.Signals.postSoundPlayed.dispatch(new FunkinSignalData.SoundPlayedSignalData(this)); return thisSound.play(volume, pitch, pan); } @@ -82,13 +83,13 @@ public long loop(float volume, float pitch) { @Override public long loop(float volume, float pitch, float pan) { - Funkin.Signals.preSoundPlayed.dispatch(new Funkin.Signals.SoundPlayedSignalData(this)); + Funkin.Signals.preSoundPlayed.dispatch(new FunkinSignalData.SoundPlayedSignalData(this)); this.volume = volume * Funkin.masterVolume; this.pitch = pitch; this.pan = pan; this.looping = true; this.isPaused = false; - Funkin.Signals.postSoundPlayed.dispatch(new Funkin.Signals.SoundPlayedSignalData(this)); + Funkin.Signals.postSoundPlayed.dispatch(new FunkinSignalData.SoundPlayedSignalData(this)); return thisSound.loop(volume, pitch, pan); } diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/FunkinReflect.java b/core/src/main/java/me/stringfromjava/funkin/backend/FunkinReflect.java new file mode 100644 index 0000000..da13cfe --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/backend/FunkinReflect.java @@ -0,0 +1,53 @@ +package me.stringfromjava.funkin.backend; + +import me.stringfromjava.funkin.Funkin; +import me.stringfromjava.funkin.util.Constants; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Backend utility class for obtaining and manipulating fields on objects through the usage of Java + * reflection. + */ +public class FunkinReflect { + + /** + * Obtains all fields of a class, including the master types above it all the way to {@link + * Object}. + * + * @param type A class literal to obtain the fields from. + * @return All fields from itself and its master classes above it. + */ + public static List getAllFields(Class type) { + List fields = new ArrayList<>(); + for (Class c = type; c != null; c = c.getSuperclass()) { + fields.addAll(Arrays.asList(c.getDeclaredFields())); + } + return fields; + } + + /** + * Checks if a class of a certain package is final. + * + * @param classPath The package definition of the class to check if final. An example could be + * {@code "me.stringfromjava.funkin.Funkin"}. + * @return If the class provided is final. If there was an exception caught, then {@code false} is + * automatically returned. + */ + public static boolean isClassFinal(String classPath) { + try { + Class clazz = Class.forName(classPath); + // Uses the java.lang.reflect.Modifier utility. + return Modifier.isFinal(clazz.getModifiers()); + } catch (ClassNotFoundException e) { + // Treat non-existent class as non-final for safe binding, + // though the user will hit an error later. + Funkin.error(Constants.System.LOG_TAG, "Failed to check if a class was final.", e); + return false; + } + } +} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/system/Paths.java b/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java similarity index 90% rename from core/src/main/java/me/stringfromjava/funkin/backend/system/Paths.java rename to core/src/main/java/me/stringfromjava/funkin/backend/Paths.java index 6a1f874..9e72089 100644 --- a/core/src/main/java/me/stringfromjava/funkin/backend/system/Paths.java +++ b/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java @@ -1,4 +1,4 @@ -package me.stringfromjava.funkin.backend.system; +package me.stringfromjava.funkin.backend; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/Reflect.java b/core/src/main/java/me/stringfromjava/funkin/backend/Reflect.java deleted file mode 100644 index cd4ff4f..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/backend/Reflect.java +++ /dev/null @@ -1,18 +0,0 @@ -package me.stringfromjava.funkin.backend; - -import java.lang.reflect.Modifier; - -public class Reflect { - - public static boolean isClassFinal(String classPath) { - try { - Class clazz = Class.forName(classPath); - // Uses the java.lang.reflect.Modifier utility - return Modifier.isFinal(clazz.getModifiers()); - } catch (ClassNotFoundException e) { - // Treat non-existent class as non-final for safe binding, - // though the user will hit an error later. - return false; - } - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/display/FunkinScreen.java b/core/src/main/java/me/stringfromjava/funkin/backend/display/FunkinScreen.java deleted file mode 100644 index 3204807..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/backend/display/FunkinScreen.java +++ /dev/null @@ -1,100 +0,0 @@ -package me.stringfromjava.funkin.backend.display; - -import com.badlogic.gdx.Screen; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.OrthographicCamera; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.Sprite; -import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.utils.ScreenUtils; -import com.badlogic.gdx.utils.viewport.FitViewport; -import com.badlogic.gdx.utils.viewport.Viewport; -import me.stringfromjava.funkin.util.Constants; - -import java.util.ArrayList; - -/** - * Base class for creating a better screen display with more functionality than the default {@link - * com.badlogic.gdx.Screen} interface. - */ -public abstract class FunkinScreen implements Screen { - - // TODO: Create a way to add more than one camera! - /** The {@link OrthographicCamera} used to see the world. */ - public OrthographicCamera camera; - - /** The current {@link Viewport} of {@code this} current screen. */ - public Viewport viewport; - - /** The background color of {@code this} current screen. */ - public Color bgColor; - - /** All display objects that are shown in {@code this} screen. */ - public final ArrayList members = new ArrayList<>(); - - /** The {@link SpriteBatch} used to render sprites in the current screen. */ - protected SpriteBatch spriteBatch; - - @Override - public void show() { - spriteBatch = new SpriteBatch(); - camera = new OrthographicCamera(); - camera.setToOrtho(false, Constants.Display.WINDOW_WIDTH, Constants.Display.WINDOW_HEIGHT); - viewport = new FitViewport(Constants.Display.WINDOW_WIDTH, Constants.Display.WINDOW_HEIGHT, camera); - viewport.apply(); - bgColor = new Color(0, 0, 0, 1); - } - - @Override - public void render(float delta) { - // Refresh the screen display. - ScreenUtils.clear(bgColor); - - camera.update(); - spriteBatch.setProjectionMatrix(camera.combined); - - spriteBatch.begin(); - for (Sprite s : members) { - s.draw(spriteBatch); - } - - spriteBatch.end(); - } - - @Override - public void resize(int width, int height) { - viewport.update(width, height, true); - } - - @Override - public void pause() {} - - @Override - public void resume() {} - - @Override - public void hide() {} - - @Override - public void dispose() { - spriteBatch.dispose(); - for (Sprite s : members) { - Texture texture = s.getTexture(); - if (texture != null) { - texture.dispose(); - } - } - } - - /** - * Adds a new sprite to {@code this} screen. If it is {@code null}, it will not be added and - * simply ignored. - * - * @param s The sprite to add to the screen. - */ - public void add(Sprite s) { - if (s != null) { - members.add(s); - } - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/game/InitScreen.java b/core/src/main/java/me/stringfromjava/funkin/game/InitScreen.java index 27e5748..6f67f69 100644 --- a/core/src/main/java/me/stringfromjava/funkin/game/InitScreen.java +++ b/core/src/main/java/me/stringfromjava/funkin/game/InitScreen.java @@ -1,8 +1,8 @@ package me.stringfromjava.funkin.game; import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.backend.display.FunkinScreen; import me.stringfromjava.funkin.game.menus.TitleScreen; +import me.stringfromjava.funkin.graphics.screen.FunkinScreen; public class InitScreen extends FunkinScreen { diff --git a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java index 7e2e032..2130173 100644 --- a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java +++ b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java @@ -3,18 +3,18 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.Sprite; import me.stringfromjava.funkin.Funkin; import me.stringfromjava.funkin.audio.FunkinSound; -import me.stringfromjava.funkin.backend.display.FunkinScreen; -import me.stringfromjava.funkin.backend.system.Paths; +import me.stringfromjava.funkin.graphics.screen.FunkinScreen; +import me.stringfromjava.funkin.backend.Paths; +import me.stringfromjava.funkin.graphics.sprite.FunkinSprite; import me.stringfromjava.funkin.tween.FunkinTween; import me.stringfromjava.funkin.tween.FunkinEase; import me.stringfromjava.funkin.tween.settings.FunkinTweenSettings; public class TitleScreen extends FunkinScreen { - private Sprite logo; + private FunkinSprite logo; private FunkinSound tickleFight; private FunkinTween tween; @@ -23,7 +23,7 @@ public class TitleScreen extends FunkinScreen { public void show() { super.show(); - logo = new Sprite(new Texture(Paths.image("stage_light"))); + logo = new FunkinSprite(new Texture(Paths.image("stage_light"))); add(logo); tickleFight = new FunkinSound("shared/sounds/tickleFight.ogg"); @@ -35,7 +35,7 @@ public void show() { .addGoal("rotation", 180) .setDuration(0.7f) .setEase(FunkinEase::circInOut); - tween = FunkinTween.tween(logo, settings, (values) -> { + tween = FunkinTween.tween(logo, settings, values -> { logo.setX(values.get("x")); logo.setY(values.get("y")); logo.setRotation(values.get("rotation")); @@ -43,10 +43,10 @@ public void show() { } @Override - public void render(float delta) { - super.render(delta); + public void render(float elapsed) { + super.render(elapsed); - float speed = 200 * delta; + float speed = 500 * elapsed; if (Gdx.input.isKeyPressed(Input.Keys.W)) { logo.setY(logo.getY() + speed); } diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java b/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java new file mode 100644 index 0000000..64f82f9 --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java @@ -0,0 +1,58 @@ +package me.stringfromjava.funkin.graphics.screen; + +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.Sprite; +import me.stringfromjava.funkin.graphics.sprite.FunkinObject; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Base class for creating a better screen display with more functionality than the default {@link + * com.badlogic.gdx.Screen} interface. + */ +public abstract class FunkinScreen implements Screen { + + /** The background color of {@code this} current screen. */ + protected Color bgColor; + + /** All display objects that are shown in {@code this} screen. */ + public final CopyOnWriteArrayList members = new CopyOnWriteArrayList<>(); + + @Override + public void show() {} + + @Override + public void render(float delta) {} + + @Override + public void resize(int width, int height) {} + + @Override + public void pause() {} + + @Override + public void resume() {} + + @Override + public void hide() {} + + @Override + public void dispose() {} + + /** + * Adds a new sprite to {@code this} screen. If it is {@code null}, it will not be added and + * simply ignored. + * + * @param object The sprite to add to the screen. + */ + public void add(FunkinObject object) { + if (object != null) { + members.add(object); + } + } + + public Color getBgColor() { + return (bgColor != null) ? bgColor : Color.BLACK; + } +} diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinObject.java b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinObject.java new file mode 100644 index 0000000..f7ee545 --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinObject.java @@ -0,0 +1,7 @@ +package me.stringfromjava.funkin.graphics.sprite; + +/** + * An interface which allows any class that implements it to be added to a {@link + * me.stringfromjava.funkin.graphics.screen.FunkinScreen}. + */ +public interface FunkinObject {} diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java new file mode 100644 index 0000000..8935ad9 --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java @@ -0,0 +1,14 @@ +package me.stringfromjava.funkin.graphics.sprite; + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Sprite; + +/** + * An enhanced version of libGDX's {@link Sprite} class. + */ +public class FunkinSprite extends Sprite implements FunkinObject { + + public FunkinSprite(Texture texture) { + super(texture); + } +} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/display/text/DisplayText.java b/core/src/main/java/me/stringfromjava/funkin/graphics/text/FunkinText.java similarity index 77% rename from core/src/main/java/me/stringfromjava/funkin/backend/display/text/DisplayText.java rename to core/src/main/java/me/stringfromjava/funkin/graphics/text/FunkinText.java index f1b628c..da42da5 100644 --- a/core/src/main/java/me/stringfromjava/funkin/backend/display/text/DisplayText.java +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/text/FunkinText.java @@ -1,7 +1,7 @@ -package me.stringfromjava.funkin.backend.display.text; +package me.stringfromjava.funkin.graphics.text; /** A display object for creating a piece of text to show on the screen. */ -public class DisplayText { +public class FunkinText { /** The text to be written onto the screen. */ public String text; @@ -15,7 +15,7 @@ public class DisplayText { /** * @param text The string to be displayed. */ - public DisplayText(String text) { + public FunkinText(String text) { this.text = text; x = 0; y = 0; diff --git a/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java b/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java index e9759fe..b1f7007 100644 --- a/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java +++ b/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java @@ -92,7 +92,7 @@ public static void registerScript(FileHandle handle) { script.onCreate(); } } catch (Exception e) { - Funkin.error("Polyverse", "Failed to load script: " + handle.path()); + Funkin.error("Polyverse", "Failed to load script: " + handle.path(), e); } } @@ -113,7 +113,7 @@ public static void forEachScript(Class type, Consumer a try { action.accept(script); } catch (Exception e) { - Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName()); + Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName(), e); } } } @@ -138,7 +138,7 @@ public static void forAllScripts(Consumer action) { try { action.accept(script); } catch (Exception e) { - Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName()); + Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName(), e); } } } diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java index 6928584..eb592e6 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java @@ -1,6 +1,9 @@ package me.stringfromjava.funkin.tween; +import me.stringfromjava.funkin.Funkin; +import me.stringfromjava.funkin.backend.FunkinReflect; import me.stringfromjava.funkin.tween.settings.FunkinTweenSettings; +import me.stringfromjava.funkin.util.Constants; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; @@ -167,7 +170,7 @@ public FunkinTween start() { finished = false; // Ensure that the fields provided actually exist on the object and are floating point values. - var allFields = object.getClass().getDeclaredFields(); + var allFields = FunkinReflect.getAllFields(object.getClass()); var neededFields = tweenSettings.getGoalFields(); for (Field field : allFields) { try { @@ -183,7 +186,7 @@ public FunkinTween start() { } initialValues.put(fName, field.getFloat(object)); } catch (IllegalAccessException e) { - e.printStackTrace(); + Funkin.error(Constants.System.LOG_TAG, "Could not access field \"" + field.getName() + "\".", e); } } return this; diff --git a/core/src/main/java/me/stringfromjava/funkin/util/Constants.java b/core/src/main/java/me/stringfromjava/funkin/util/Constants.java index 7eade3f..24b8002 100644 --- a/core/src/main/java/me/stringfromjava/funkin/util/Constants.java +++ b/core/src/main/java/me/stringfromjava/funkin/util/Constants.java @@ -7,7 +7,7 @@ */ public final class Constants { - /** Static subclass for holding values for components such as the window's width and height. */ + /** Holds values for aspects of the game's display, such as the window's width and height. */ public static final class Display { /** @@ -25,7 +25,25 @@ public static final class Display { private Display() {} } - public static final class Colors { + /** + * Stores constants for things related to the backend of Funkin'. This includes components like + * logging, folder paths, etc. + */ + public static final class System { + + /** + * The default and globally recognized default tag for logs that are outputted inside the + * console. + */ + public static final String LOG_TAG = "Funkin"; + + private System() {} + } + + /** + * Holds ASCII color code constants for text in the console. + */ + public static final class AsciiCodes { public static final String RESET = "\u001B[0m"; public static final String BOLD = "\033[0;1m"; @@ -40,7 +58,7 @@ public static final class Colors { public static final String CYAN = "\u001B[36m"; public static final String WHITE = "\u001B[37m"; - private Colors() {} + private AsciiCodes() {} } private Constants() {} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/system/FunkinSignal.java b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignal.java similarity index 87% rename from core/src/main/java/me/stringfromjava/funkin/backend/system/FunkinSignal.java rename to core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignal.java index 59efa42..89d3af0 100644 --- a/core/src/main/java/me/stringfromjava/funkin/backend/system/FunkinSignal.java +++ b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignal.java @@ -1,4 +1,4 @@ -package me.stringfromjava.funkin.backend.system; +package me.stringfromjava.funkin.util.signal; import java.util.concurrent.CopyOnWriteArrayList; @@ -40,6 +40,16 @@ public void addOnce(SignalHandler callback) { } } + /** + * Removes a specific callback from {@code this} signal. + * + * @param callback The callback to remove. + */ + public void remove(SignalHandler callback) { + callbacks.remove(callback); + tempCallbacks.remove(callback); + } + /** Removes all callbacks from {@code this} signal. */ public void clear() { callbacks.clear(); diff --git a/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java new file mode 100644 index 0000000..a172e20 --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java @@ -0,0 +1,21 @@ +package me.stringfromjava.funkin.util.signal; + +import me.stringfromjava.funkin.audio.FunkinSound; +import me.stringfromjava.funkin.graphics.screen.FunkinScreen; + +/** + * Convenience class for holding all signal data records used in the default Funkin' signals. + */ +public final class FunkinSignalData { + + public record RenderSignalData(float delta) {} + + public record ScreenSwitchSignalData(FunkinScreen screen) {} + + public record SoundPlayedSignalData(FunkinSound sound) {} + + public record WindowMinimizedSignalData(boolean iconified) {} + + private FunkinSignalData() {} +} + diff --git a/gradle.properties b/gradle.properties index 5f61b4a..d779a5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,6 @@ org.gradle.logging.level=quiet anim8Version=0.5.3 ktxVersion=1.13.1-rc1 utilsVersion=0.13.7 -universalTweenVersion=6.3.3 graalHelperVersion=2.0.1 android.useAndroidX=true android.enableR8.fullMode=false From 84a4d33b7be1d2e2ec2cb2ae16133e162f929edf Mon Sep 17 00:00:00 2001 From: String Date: Wed, 14 Jan 2026 00:23:29 -0600 Subject: [PATCH 2/6] Add animation functionality to FunkinSprite class --- android/build.gradle | 5 +- assets/another_test.groovy | 3 +- assets/test.groovy | 2 +- core/build.gradle | 1 + .../java/me/stringfromjava/funkin/Funkin.java | 41 ++- .../me/stringfromjava/funkin/FunkinGame.java | 35 +- .../funkin/audio/FunkinSound.java | 67 ++-- .../stringfromjava/funkin/backend/Paths.java | 6 +- .../funkin/game/menus/TitleScreen.java | 9 +- .../funkin/graphics/sprite/FunkinSprite.java | 301 +++++++++++++++++- .../funkin/util/signal/FunkinSignalData.java | 3 +- gradle.properties | 3 +- lwjgl3/build.gradle | 4 +- 13 files changed, 415 insertions(+), 65 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index b8ef611..9e17554 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -65,6 +65,10 @@ dependencies { implementation "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion" implementation project(':core') + api "games.rednblack.miniaudio:gdx-miniaudio-platform:$miniaudioVersion:natives-armeabi-v7a" + api "games.rednblack.miniaudio:gdx-miniaudio-platform:$miniaudioVersion:natives-arm64-v8a" + api "games.rednblack.miniaudio:gdx-miniaudio-platform:$miniaudioVersion:natives-x86" + api "games.rednblack.miniaudio:gdx-miniaudio-platform:$miniaudioVersion:natives-x86_64" natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-arm64-v8a" natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-armeabi-v7a" natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86" @@ -73,7 +77,6 @@ dependencies { natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64" - } // Called every time gradle gets executed, takes the native dependencies of diff --git a/assets/another_test.groovy b/assets/another_test.groovy index 0051f55..32f0bd8 100644 --- a/assets/another_test.groovy +++ b/assets/another_test.groovy @@ -1,7 +1,6 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Texture -import com.badlogic.gdx.graphics.g2d.Sprite import me.stringfromjava.funkin.Funkin import me.stringfromjava.funkin.backend.Paths import me.stringfromjava.funkin.graphics.sprite.FunkinSprite @@ -28,7 +27,7 @@ class AnotherTestClass extends Script { super.onRender(delta) if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { - var sprite = new FunkinSprite(new Texture(Paths.image("pauseAlt/bfLol"))) + var sprite = new FunkinSprite().loadGraphic(new Texture(Paths.sharedImage("pauseAlt/bfLol"))) var randomPosX = new Random().nextInt(Constants.Display.WINDOW_WIDTH) var randomPosY = new Random().nextInt(Constants.Display.WINDOW_HEIGHT) diff --git a/assets/test.groovy b/assets/test.groovy index 35af7c6..7a74da9 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -44,7 +44,7 @@ class TestScreen extends FunkinScreen { void show() { super.show() - test = new FunkinSprite(new Texture(Paths.image('NOTE_hold_assets'))) + test = new FunkinSprite().loadGraphic(new Texture(Paths.sharedImage('NOTE_hold_assets'))) add(test) bgColor = new Color(0, 1, 0, 1) diff --git a/core/build.gradle b/core/build.gradle index 9b3110a..c0c016b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -8,6 +8,7 @@ dependencies { api "com.github.tommyettinger:anim8-gdx:$anim8Version" api "com.github.tommyettinger:libgdx-utils:$utilsVersion" api "io.github.libktx:ktx-freetype:$ktxVersion" + api "games.rednblack.miniaudio:miniaudio:$miniaudioVersion" // FNF:JE. implementation "org.apache.groovy:groovy:4.0.12" diff --git a/core/src/main/java/me/stringfromjava/funkin/Funkin.java b/core/src/main/java/me/stringfromjava/funkin/Funkin.java index 7213040..ab318e7 100644 --- a/core/src/main/java/me/stringfromjava/funkin/Funkin.java +++ b/core/src/main/java/me/stringfromjava/funkin/Funkin.java @@ -2,7 +2,6 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Music; -import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.scenes.scene2d.Stage; import me.stringfromjava.funkin.audio.FunkinSound; import me.stringfromjava.funkin.graphics.screen.FunkinScreen; @@ -39,20 +38,20 @@ public final class Funkin { *

The key is the sound's ID (created by libGDX), and the value is the sound itself. Note that * it's not recommended to access this unless you know what you're doing! */ - public static Map soundPool = new HashMap<>(); + public static Map soundPool = new HashMap<>(); /** The object where the current music being played is stored. */ public static Music music = null; - /** The global volume multiplier for all sounds and music. */ - public static float masterVolume = 1.0f; - /** * The static instance used to access the core elements of the game. This includes the loop, * setting the current screen, and more. */ private static FunkinGame game; + /** The global volume multiplier for all sounds and music. */ + private static float globalVolume = 1.0f; + /** Has the global manager been initialized yet? */ private static boolean initialized = false; @@ -151,6 +150,32 @@ public static Music playMusic(String path, float volume, boolean looped) { return music; } + /** + * Sets Funkin's global volume and applies it automatically to all sounds in the sound pool and + * the current music playing. + * + * @param v The new volume to apply globally. + * @return The new global volume. + */ + public static float setGlobalVolume(float v) { + globalVolume = v; + if (music != null) { + music.setVolume(music.getVolume() * v); + } + for (FunkinSound sound : soundPool.values()) { + sound.setVolume(sound.ID, sound.getVolume(), true); + } + return globalVolume; + } + + public static boolean keyPressed(int key) { + return Gdx.input.isKeyPressed(key); + } + + public static boolean keyJustPressed(int key) { + return Gdx.input.isKeyJustPressed(key); + } + public static void info(Object message) { info(Constants.System.LOG_TAG, message); } @@ -178,7 +203,7 @@ public static void error(String tag, Object message) { public static void error(String tag, Object message, Throwable throwable) { String msg = (throwable != null) - ? (message + " | Exception: " + throwable.toString()) + ? (message + " | Exception: " + throwable) : message.toString(); outputLog(tag, msg, FunkinLogLevel.ERROR); } @@ -187,6 +212,10 @@ public static FunkinGame getGame() { return game; } + public static float getGlobalVolume() { + return globalVolume; + } + public static Stage getStage() { return game.stage; } diff --git a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java index 481fd26..c1df87d 100644 --- a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java +++ b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java @@ -24,7 +24,8 @@ import java.util.Set; -import me.stringfromjava.funkin.util.signal.FunkinSignalData; +import static me.stringfromjava.funkin.util.signal.FunkinSignalData.RenderSignalData; +import static me.stringfromjava.funkin.util.signal.FunkinSignalData.WindowMinimizedSignalData; /** * The game object used for containing the main loop and core elements of Funkin'. @@ -89,34 +90,34 @@ public void render() { float delta = Gdx.graphics.getDeltaTime(); FunkinScreen screen = Funkin.screen; - Funkin.Signals.preRender.dispatch(new FunkinSignalData.RenderSignalData(delta)); + Funkin.Signals.preRender.dispatch(new RenderSignalData(delta)); // Update and render the current screen that's active. + ScreenUtils.clear(Color.BLACK); + viewport.apply(); + batch.setProjectionMatrix(camera.combined); + batch.begin(); if (screen != null) { - ScreenUtils.clear(Color.BLACK); - viewport.apply(); - batch.setProjectionMatrix(camera.combined); - batch.begin(); batch.setColor(screen.getBgColor()); batch.draw(bgTexture, 0, 0, viewport.getWorldWidth(), viewport.getWorldHeight()); batch.setColor(Color.WHITE); // Set color back to white so display objects aren't affected. screen.render(delta); for (FunkinObject object : screen.members) { - if (object instanceof FunkinSprite) { - ((FunkinSprite) object).draw(batch); + if (object instanceof FunkinSprite sprite) { + sprite.update(delta); + sprite.draw(batch); } } } batch.end(); - // Update the current screen and stage. stage.act(delta); stage.draw(); FunkinTween.globalManager.update(delta); Polyverse.forAllScripts(script -> script.onRender(delta)); - Funkin.Signals.postRender.dispatch(new FunkinSignalData.RenderSignalData(delta)); + Funkin.Signals.postRender.dispatch(new RenderSignalData(delta)); } @Override @@ -127,8 +128,7 @@ public void resume() {} /** Called when the user regains focus on the game's window. */ public void onWindowFocused() { - Funkin.masterVolume = 1.0f; - Funkin.music.setVolume(1); + Funkin.setGlobalVolume(1); Funkin.Signals.windowFocused.dispatch(); Funkin.info("Game window has regained focus."); } @@ -138,10 +138,8 @@ public void onWindowUnfocused() { if (isMinimized) { return; } - Funkin.masterVolume = 0.008f; - Funkin.music.setVolume(0.008f); - Funkin.Signals.windowMinimized.dispatch( - new FunkinSignalData.WindowMinimizedSignalData(isMinimized)); + Funkin.setGlobalVolume(0.008f); + Funkin.Signals.windowUnfocused.dispatch(); Funkin.info("Game window has lost focus."); } @@ -156,9 +154,8 @@ public void onWindowMinimized(boolean iconified) { if (!isMinimized) { return; } - Funkin.masterVolume = 0.0f; - Funkin.music.setVolume(0); - Funkin.Signals.windowMinimized.dispatch(); + Funkin.setGlobalVolume(0); + Funkin.Signals.windowMinimized.dispatch(new WindowMinimizedSignalData(isMinimized)); Funkin.info("Game window has been minimized."); } diff --git a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java b/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java index ee9c9c4..542d839 100644 --- a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java +++ b/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java @@ -5,7 +5,8 @@ import com.badlogic.gdx.files.FileHandle; import me.stringfromjava.funkin.Funkin; import me.stringfromjava.funkin.backend.Paths; -import me.stringfromjava.funkin.util.signal.FunkinSignalData; + +import static me.stringfromjava.funkin.util.signal.FunkinSignalData.SoundPlayedSignalData; /** * An enhanced version of libGDX's {@link Sound}. @@ -18,7 +19,7 @@ public class FunkinSound implements Sound { /** The ID of {@code this} specific sound. */ public final long ID; - private Sound thisSound; + private final Sound libGdxSound; private float volume; private float pitch; private float pan; @@ -30,20 +31,20 @@ public FunkinSound(String path) { } public FunkinSound(FileHandle path) { - thisSound = Gdx.audio.newSound(path); - ID = thisSound.play(); + libGdxSound = Gdx.audio.newSound(path); + ID = libGdxSound.play(); volume = 1.0f; pitch = 1.0f; pan = 0.0f; looping = false; isPaused = false; - thisSound.stop(); - Funkin.soundPool.put(ID, thisSound); + libGdxSound.stop(); + Funkin.soundPool.put(ID, this); } @Override public long play() { - return thisSound.play(volume, pitch, pan); + return libGdxSound.play(volume, pitch, pan); } @Override @@ -57,14 +58,14 @@ public long play(float volume, float pitch) { @Override public long play(float volume, float pitch, float pan) { - Funkin.Signals.preSoundPlayed.dispatch(new FunkinSignalData.SoundPlayedSignalData(this)); - this.volume = volume * Funkin.masterVolume; + Funkin.Signals.preSoundPlayed.dispatch(new SoundPlayedSignalData(this)); + this.volume = volume; this.pitch = pitch; this.pan = pan; this.looping = false; this.isPaused = false; - Funkin.Signals.postSoundPlayed.dispatch(new FunkinSignalData.SoundPlayedSignalData(this)); - return thisSound.play(volume, pitch, pan); + Funkin.Signals.postSoundPlayed.dispatch(new SoundPlayedSignalData(this)); + return libGdxSound.play(volume, pitch, pan); } @Override @@ -83,32 +84,32 @@ public long loop(float volume, float pitch) { @Override public long loop(float volume, float pitch, float pan) { - Funkin.Signals.preSoundPlayed.dispatch(new FunkinSignalData.SoundPlayedSignalData(this)); - this.volume = volume * Funkin.masterVolume; + Funkin.Signals.preSoundPlayed.dispatch(new SoundPlayedSignalData(this)); + this.volume = volume * Funkin.getGlobalVolume(); this.pitch = pitch; this.pan = pan; this.looping = true; this.isPaused = false; - Funkin.Signals.postSoundPlayed.dispatch(new FunkinSignalData.SoundPlayedSignalData(this)); - return thisSound.loop(volume, pitch, pan); + Funkin.Signals.postSoundPlayed.dispatch(new SoundPlayedSignalData(this)); + return libGdxSound.loop(volume, pitch, pan); } @Override public void stop() { isPaused = false; - thisSound.stop(); + libGdxSound.stop(); } @Override public void pause() { isPaused = true; - thisSound.pause(); + libGdxSound.pause(); } @Override public void resume() { isPaused = false; - thisSound.resume(); + libGdxSound.resume(); } @Override @@ -118,45 +119,55 @@ public void dispose() { pan = 0.0f; looping = false; isPaused = false; - thisSound.dispose(); + libGdxSound.dispose(); Funkin.soundPool.remove(ID); } @Override public void stop(long soundId) { isPaused = false; - thisSound.stop(); + libGdxSound.stop(); } @Override public void pause(long soundId) { isPaused = true; - thisSound.pause(soundId); + libGdxSound.pause(soundId); } @Override public void resume(long soundId) { isPaused = false; - thisSound.resume(); + libGdxSound.resume(); } @Override public void setLooping(long soundId, boolean looping) { this.looping = looping; - thisSound.setLooping(soundId, looping); + libGdxSound.setLooping(soundId, looping); } @Override public void setPitch(long soundId, float pitch) { this.pitch = pitch; - thisSound.setPitch(soundId, pitch); + libGdxSound.setPitch(soundId, pitch); } @Override public void setVolume(long soundId, float volume) { - float v = volume * Funkin.masterVolume; - this.volume = v; - thisSound.setVolume(soundId, v); + setVolume(soundId, volume, true); + } + + /** + * Sets the new volume for {@code this} sound. + * + * @param soundId The sound's unique ID, generated by libGDX. + * @param volume The new volume to use. + * @param useGlobalVolume Should the new volume automatically adapt to Funkin's global volume? + */ + public void setVolume(long soundId, float volume, boolean useGlobalVolume) { + this.volume = useGlobalVolume ? volume * Funkin.getGlobalVolume() : volume; + libGdxSound.setVolume(soundId, this.volume); } public void setPan(long soundId, float pan) { @@ -165,7 +176,7 @@ public void setPan(long soundId, float pan) { @Override public void setPan(long soundId, float pan, float volume) { - thisSound.setPan(soundId, pan, volume); + libGdxSound.setPan(soundId, pan, volume); } public float getVolume() { diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java b/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java index 9e72089..f47e1d3 100644 --- a/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java +++ b/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java @@ -14,7 +14,11 @@ public static FileHandle shared(String path) { return asset(String.format("shared/%s", path)); } - public static FileHandle image(String path) { + public static FileHandle xml(String path) { + return asset(path + ".xml"); + } + + public static FileHandle sharedImage(String path) { return shared(String.format("images/%s.png", path)); } diff --git a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java index 2130173..a9e9351 100644 --- a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java +++ b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java @@ -23,7 +23,10 @@ public class TitleScreen extends FunkinScreen { public void show() { super.show(); - logo = new FunkinSprite(new Texture(Paths.image("stage_light"))); + var t = Paths.sharedImage("noteStrumline"); + var xml = Paths.shared("images/noteStrumline.xml"); + logo = new FunkinSprite().loadSparrowFrames(t, xml); + logo.addAnimationByPrefix("test", "confirmDown", 24, false); add(logo); tickleFight = new FunkinSound("shared/sounds/tickleFight.ogg"); @@ -60,6 +63,10 @@ public void render(float elapsed) { logo.setX(logo.getX() + speed); } + if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { + logo.playAnimation("test", false); + } + if (Gdx.input.isKeyJustPressed(Input.Keys.T)) { tween.start(); } diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java index 8935ad9..6562aef 100644 --- a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java @@ -1,14 +1,311 @@ package me.stringfromjava.funkin.graphics.sprite; +import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Animation; +import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.XmlReader; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; /** * An enhanced version of libGDX's {@link Sprite} class. + * + *

It allows you to load animations, textures, and do much more with simplicity and ease. */ public class FunkinSprite extends Sprite implements FunkinObject { - public FunkinSprite(Texture texture) { - super(texture); + /** The texture image that {@code this} sprite uses. */ + protected Texture texture; + + /** The atlas regions used in this sprite (used for animations). */ + protected Array atlasRegions; + + /** A map that animations that stored and registered in. */ + protected final Map> animations; + + /** The current frame that {@code this} sprite is on in its animation (if one is playing). */ + protected TextureAtlas.AtlasRegion currentFrame; + + /** Used for updating {@code this} sprite's current animation. */ + protected float stateTime = 0; + + /** The name of the current animation playing. */ + protected String currentAnim = ""; + + /** Is {@code this} sprites current animation looping indefinitely? */ + protected boolean looping = true; + + /** + * Where all the image frames are stored. This is also where the main image is stored when using + * {@link #loadGraphic(FileHandle)}. + */ + protected TextureRegion[][] frames; + + public FunkinSprite() { + animations = new HashMap<>(); + } + + /** + * Updates {@code this} sprite. + * + * @param delta The amount of time that has passed since the last frame update. + */ + public void update(float delta) { + if (animations.containsKey(currentAnim)) { + stateTime += delta; + currentFrame = + (TextureAtlas.AtlasRegion) animations.get(currentAnim).getKeyFrame(stateTime, looping); + setRegion(currentFrame); + } + } + + /** + * Load's a texture and automatically resizes the size of {@code this} sprite. + * + * @param path The directory of the {@code .png} to load onto {@code this} sprite. + * @return {@code this} sprite for chaining. + */ + public FunkinSprite loadGraphic(FileHandle path) { + Texture texture = new Texture(path); + return loadGraphic(texture, texture.getWidth(), texture.getHeight()); + } + + /** + * Load's a texture and automatically resizes the size of {@code this} sprite. + * + * @param path The directory of the {@code .png} to load onto {@code this} sprite. + * @param frameWidth How wide the sprite should be. + * @return {@code this} sprite for chaining. + */ + public FunkinSprite loadGraphic(FileHandle path, int frameWidth) { + Texture texture = new Texture(path); + return loadGraphic(texture, frameWidth, texture.getHeight()); + } + + /** + * Load's a texture and automatically resizes the size of {@code this} sprite. + * + * @param path The directory of the {@code .png} to load onto {@code this} sprite. + * @param frameWidth How wide the sprite should be. + * @param frameHeight How tall the sprite should be. + * @return {@code this} sprite for chaining. + */ + public FunkinSprite loadGraphic(FileHandle path, int frameWidth, int frameHeight) { + return loadGraphic(new Texture(path), frameWidth, frameHeight); + } + + public FunkinSprite loadGraphic(Texture texture, int frameWidth, int frameHeight) { + frames = TextureRegion.split(texture, frameWidth, frameHeight); + // Set default visual to the first frame. + setRegion(frames[0][0]); + setSize(frameWidth, frameHeight); + setOriginCenter(); + return this; + } + + /** + * Loads an {@code .xml} spritesheet with {@code SubTexture} data inside of it. + * + * @param texture The path to the {@code .png} texture file for slicing and extracting the + * different frames from. + * @param xmlFile The path to the {@code .xml} file which contains the data for each subtexture of + * the sparrow atlas. + * @return {@code this} sprite for chaining. + */ + public FunkinSprite loadSparrowFrames(FileHandle texture, FileHandle xmlFile) { + return loadSparrowFrames(new Texture(texture), new XmlReader().parse(xmlFile)); + } + + /** + * Loads an {@code .xml} spritesheet with {@code SubTexture} data inside of it. + * + * @param texture The {@code .png} texture file for slicing and extracting the different frames + * from. + * @param xmlFile The {@link XmlReader.Element} data which contains the data for each subtexture + * of the sparrow atlas. + * @return {@code this} sprite for chaining. + */ + public FunkinSprite loadSparrowFrames(Texture texture, XmlReader.Element xmlFile) { + // We store regions in a list so we can filter them by prefix later. + // TextureAtlas.AtlasRegion is used because it supports offsets. + atlasRegions = new Array<>(); + + for (XmlReader.Element subTexture : xmlFile.getChildrenByName("SubTexture")) { + String name = subTexture.getAttribute("name"); + int x = subTexture.getInt("x"); + int y = subTexture.getInt("y"); + int width = subTexture.getInt("width"); + int height = subTexture.getInt("height"); + + this.texture = texture; + TextureAtlas.AtlasRegion region = new TextureAtlas.AtlasRegion(texture, x, y, width, height); + region.name = name; + + // Handle Trimming/Offsets (frameX, frameY, frameWidth, frameHeight). + // These are crucial for centering notes/characters correctly. + if (subTexture.hasAttribute("frameX")) { + region.offsetX = Math.abs(subTexture.getInt("frameX")); + region.offsetY = Math.abs(subTexture.getInt("frameY")); + region.originalWidth = subTexture.getInt("frameWidth"); + region.originalHeight = subTexture.getInt("frameHeight"); + } else { + region.offsetX = 0; + region.offsetY = 0; + region.originalWidth = width; + region.originalHeight = height; + } + + atlasRegions.add(region); + } + + // Set the default visual. + setRegion(atlasRegions.first()); + setSize(getRegionWidth(), getRegionHeight()); + return this; + } + + /** + * Adds an animation by looking for sub textures that start with the prefix passed down. + * + * @param name The name of the animation (e.g., "confirm"). + * @param prefix The prefix in the {@code .xml} file (e.g., "left confirm"). + * @param frameRate How fast the animation should play in frames-per-second. Standard is 24. + * @param loop Should the new animation loop indefinitely? + */ + public void addAnimationByPrefix(String name, String prefix, int frameRate, boolean loop) { + Array frames = new Array<>(); + + for (TextureAtlas.AtlasRegion region : atlasRegions) { + if (region.name.startsWith(prefix)) { + frames.add(region); + } + } + + if (frames.size > 0) { + // Ensure frames are sorted alphabetically (e.g., confirm0000, confirm0001). + frames.sort(Comparator.comparing(o -> o.name)); + + animations.put( + name, + new Animation<>( + 1f / frameRate, frames, loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL)); + } + } + + /** + * Adds a new animation to the animations list, if it doesn't exist already. + * + * @param name The name of the animation. This is what you'll use every time you use {@code + * playAnimation()}. + * @param frameIndices An array of integers used for animation frame indices. + * @param frameDuration How long each frame lasts for in seconds. + */ + public void addAnimation(String name, int[] frameIndices, float frameDuration) { + Array animFrames = new Array<>(); + + // Convert 1D indices (0, 1, 2...) to 2D grid coordinates. + int cols = frames[0].length; + for (int index : frameIndices) { + int row = index / cols; + int col = index % cols; + animFrames.add(frames[row][col]); + } + + animations.put(name, new Animation<>(frameDuration, animFrames)); + } + + /** + * Plays an animation {@code this} sprite has, with looping enabled by default. + * + * @param name The name of the animation to play. + */ + public void playAnimation(String name) { + playAnimation(name, true); + } + + /** + * Plays an animation {@code this} sprite has, if it exists. + * + * @param name The name of the animation to play. + * @param loop Should this animation loop indefinitely? + */ + public void playAnimation(String name, boolean loop) { + if (!currentAnim.equals(name) || isAnimationFinished()) { + this.currentAnim = name; + this.looping = loop; + this.stateTime = 0; + } + } + + @Override + public void draw(Batch batch) { + if (currentFrame != null) { + // We use the currentFrame's offsets to adjust the drawing position. + // This prevents the "shaking" or "jittering" in sparrow animations. + float drawX = getX() + currentFrame.offsetX; + + // libGDX coordinate system is bottom-left, but Sparrow is top-down. + // This math aligns the frame within its "original" box. + float drawY = + getY() + + (currentFrame.originalHeight + - currentFrame.getRegionHeight() + - currentFrame.offsetY); + + batch.draw( + currentFrame, + drawX, + drawY, + getOriginX(), + getOriginY(), + currentFrame.getRegionWidth(), + currentFrame.getRegionHeight(), + getScaleX(), + getScaleY(), + getRotation()); + } else { + super.draw(batch); // Fallback to standard sprite drawing. + } + } + + public boolean isAnimationFinished() { + Animation anim = animations.get(currentAnim); + if (anim == null) return true; + return anim.isAnimationFinished(stateTime); + } + + public Map> getAnimations() { + return animations; + } + + public Array getAtlasRegions() { + return atlasRegions; + } + + public TextureAtlas.AtlasRegion getCurrentFrame() { + return currentFrame; + } + + public float getStateTime() { + return stateTime; + } + + public String getCurrentAnim() { + return currentAnim; + } + + public boolean isLooping() { + return looping; + } + + public TextureRegion[][] getFrames() { + return frames; } } diff --git a/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java index a172e20..d27c22f 100644 --- a/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java +++ b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java @@ -4,7 +4,8 @@ import me.stringfromjava.funkin.graphics.screen.FunkinScreen; /** - * Convenience class for holding all signal data records used in the default Funkin' signals. + * Convenience class for holding all signal data records used in the default signals stored in + * the global {@link me.stringfromjava.funkin.Funkin} manager class. */ public final class FunkinSignalData { diff --git a/gradle.properties b/gradle.properties index d779a5e..d3f3f53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,4 +19,5 @@ android.useAndroidX=true android.enableR8.fullMode=false enableGraalNative=false gdxVersion=1.13.1 -projectVersion=1.0.0-ALPHA +miniaudioVersion=0.7 +projectVersion=0.1.0-ALPHA diff --git a/lwjgl3/build.gradle b/lwjgl3/build.gradle index 37d3d2e..9e4435f 100644 --- a/lwjgl3/build.gradle +++ b/lwjgl3/build.gradle @@ -28,6 +28,7 @@ if (JavaVersion.current().isJava9Compatible()) { } dependencies { + api "games.rednblack.miniaudio:gdx-miniaudio-platform:$miniaudioVersion:natives-desktop" implementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion" implementation "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop" implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" @@ -35,8 +36,7 @@ dependencies { if(enableGraalNative == 'true') { implementation "io.github.berstanio:gdx-svmhelper-backend-lwjgl3:$graalHelperVersion" - - } + } } def os = System.properties['os.name'].toLowerCase() From e280a4c0beea48294926176e2db1b9be200d79bb Mon Sep 17 00:00:00 2001 From: String Date: Wed, 14 Jan 2026 21:30:48 -0600 Subject: [PATCH 3/6] Rework the audio system to use something better --- assets/another_test.groovy | 2 +- assets/test.groovy | 2 +- .../java/me/stringfromjava/funkin/Funkin.java | 230 ++++++++++++------ .../me/stringfromjava/funkin/FunkinGame.java | 30 +-- .../funkin/audio/FunkinSound.java | 201 --------------- .../stringfromjava/funkin/backend/Paths.java | 12 +- .../funkin/game/menus/TitleScreen.java | 34 ++- .../funkin/graphics/sprite/FunkinSprite.java | 16 +- .../funkin/tween/FunkinTween.java | 4 +- .../FunkinReflectUtil.java} | 5 +- .../funkin/util/signal/FunkinSignalData.java | 6 +- 11 files changed, 209 insertions(+), 333 deletions(-) delete mode 100644 core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java rename core/src/main/java/me/stringfromjava/funkin/{backend/FunkinReflect.java => util/FunkinReflectUtil.java} (93%) diff --git a/assets/another_test.groovy b/assets/another_test.groovy index 32f0bd8..60ccb28 100644 --- a/assets/another_test.groovy +++ b/assets/another_test.groovy @@ -27,7 +27,7 @@ class AnotherTestClass extends Script { super.onRender(delta) if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { - var sprite = new FunkinSprite().loadGraphic(new Texture(Paths.sharedImage("pauseAlt/bfLol"))) + var sprite = new FunkinSprite().loadGraphic(Paths.sharedImageAsset("pauseAlt/bfLol")) var randomPosX = new Random().nextInt(Constants.Display.WINDOW_WIDTH) var randomPosY = new Random().nextInt(Constants.Display.WINDOW_HEIGHT) diff --git a/assets/test.groovy b/assets/test.groovy index 7a74da9..a046598 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -44,7 +44,7 @@ class TestScreen extends FunkinScreen { void show() { super.show() - test = new FunkinSprite().loadGraphic(new Texture(Paths.sharedImage('NOTE_hold_assets'))) + test = new FunkinSprite().loadGraphic(Paths.sharedImageAsset('NOTE_hold_assets')) add(test) bgColor = new Color(0, 1, 0, 1) diff --git a/core/src/main/java/me/stringfromjava/funkin/Funkin.java b/core/src/main/java/me/stringfromjava/funkin/Funkin.java index ab318e7..bf220e7 100644 --- a/core/src/main/java/me/stringfromjava/funkin/Funkin.java +++ b/core/src/main/java/me/stringfromjava/funkin/Funkin.java @@ -1,19 +1,20 @@ package me.stringfromjava.funkin; import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Music; +import com.badlogic.gdx.assets.AssetManager; +import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.scenes.scene2d.Stage; -import me.stringfromjava.funkin.audio.FunkinSound; +import games.rednblack.miniaudio.MAGroup; +import games.rednblack.miniaudio.MASound; +import games.rednblack.miniaudio.MiniAudio; +import games.rednblack.miniaudio.loader.MASoundLoader; import me.stringfromjava.funkin.graphics.screen.FunkinScreen; import me.stringfromjava.funkin.util.signal.FunkinSignal; -import me.stringfromjava.funkin.backend.Paths; import me.stringfromjava.funkin.util.Constants; import me.stringfromjava.funkin.util.signal.FunkinSignalData.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; /** * Global manager and utility class for the game. @@ -32,16 +33,19 @@ public final class Funkin { */ public static FunkinScreen screen = null; - /** - * A map containing all sounds that are currently playing. - * - *

The key is the sound's ID (created by libGDX), and the value is the sound itself. Note that - * it's not recommended to access this unless you know what you're doing! - */ - public static Map soundPool = new HashMap<>(); + /** The main audio object used to create, */ + private static MiniAudio engine; + + private static AssetManager assetManager; + + /** The audio group for regular small, sound effects. */ + private static MAGroup soundsGroup; + + /** The sound for playing music throughout the game. */ + public static MASound music; - /** The object where the current music being played is stored. */ - public static Music music = null; + /** The current master volume that is set. */ + private static float masterVolume = 1; /** * The static instance used to access the core elements of the game. This includes the loop, @@ -49,9 +53,6 @@ public final class Funkin { */ private static FunkinGame game; - /** The global volume multiplier for all sounds and music. */ - private static float globalVolume = 1.0f; - /** Has the global manager been initialized yet? */ private static boolean initialized = false; @@ -68,6 +69,15 @@ public static void initialize(FunkinGame gameInstance) { throw new IllegalStateException("FNF:JE has already been initialized!"); } game = gameInstance; + + assetManager = new AssetManager(); + + // Set up the game's global audio system. + engine = new MiniAudio(); + soundsGroup = engine.createGroup(); + assetManager.setLoader( + MASound.class, new MASoundLoader(engine, assetManager.getFileHandleResolver())); + initialized = true; } @@ -95,77 +105,138 @@ public static void setScreen(FunkinScreen newScreen) { } /** - * Plays a sound. (Duh.) + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * Funkin.playSound(Paths.external("your/path/here").path());
+   * }
* - * @param path The path to play the sound from. - * @return The sound instance itself, as a {@link FunkinSound}. + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @return The new sound instance. */ - public static FunkinSound playSound(String path) { - FunkinSound sound = new FunkinSound(path); - if (sound.ID != -1) { // libGDX will return -1 if the sound fails to play. - soundPool.put(sound.ID, sound); - } - sound.play(); - return sound; + public static MASound playSound(String path) { + return playSound(path, 1, false, null, false); } /** - * Plays new music. (Duh.) + * Plays a new sound effect. * - * @param path The path to play the music from. - * @return The music instance itself. + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * Funkin.playSound(Paths.external("your/path/here").path(), 1);
+   * }
+ * + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new sound with. + * @return The new sound instance. */ - public static Music playMusic(String path) { - return playMusic(path, 1.0f, true); + public static MASound playSound(String path, float volume) { + return playSound(path, volume, false, null, false); } /** - * Plays new music. (Duh.) + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: * - * @param path The path to play the music from. - * @param volume The volume to play the music at. - * @return The music instance itself. + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * Funkin.playSound(Paths.external("your/path/here").path(), 1, false);
+   * }
+ * + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new sound with. + * @param looping Should the new sound loop indefinitely? + * @return The new sound instance. */ - public static Music playMusic(String path, float volume) { - return playMusic(path, volume, true); + public static MASound playSound(String path, float volume, boolean looping) { + return playSound(path, volume, looping, null, false); } /** - * Plays new music. (Duh.) + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * // If null is passed down for the group, then the default sound group will be used.
+   * Funkin.playSound(Paths.external("your/path/here").path(), 1, false, null);
+   * }
* - * @param path The path to play the music from. - * @param volume The volume to play the music at. - * @param looped Should the music loop when it is finished playing? - * @return The music instance itself. + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new sound with. + * @param looping Should the new sound loop indefinitely? + * @param group The sound group to add the new sound to. If {@code null} is passed down, then the + * default sound group will be used. + * @return The new sound instance. */ - public static Music playMusic(String path, float volume, boolean looped) { - Music music = Gdx.audio.newMusic(Paths.asset(path)); - if (Funkin.music != null && Funkin.music.isPlaying()) { - Funkin.music.stop(); - } - Funkin.music = music; - music.setVolume(volume); - music.setLooping(looped); - music.play(); - return music; + public static MASound playSound(String path, float volume, boolean looping, MAGroup group) { + return playSound(path, volume, looping, group, false); } /** - * Sets Funkin's global volume and applies it automatically to all sounds in the sound pool and - * the current music playing. + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: * - * @param v The new volume to apply globally. - * @return The new global volume. + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * // If null is passed down for the group, then the default sound group will be used.
+   * // For the boolean attribuite "external", you only should make it true for mobile builds,
+   * // otherwise just simply leave it be or make it "false" for other platforms like desktop.
+   * Funkin.playSound(Paths.external("your/path/here").path(), 1, false, null, true);
+   * }
+ * + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new sound with. + * @param looping Should the new sound loop indefinitely? + * @param group The sound group to add the new sound to. If {@code null} is passed down, then the + * default sound group will be used. + * @param external Should this sound be loaded externally? (This is only for mobile platforms!) + * @return The new sound instance. */ - public static float setGlobalVolume(float v) { - globalVolume = v; - if (music != null) { - music.setVolume(music.getVolume() * v); - } - for (FunkinSound sound : soundPool.values()) { - sound.setVolume(sound.ID, sound.getVolume(), true); - } - return globalVolume; + public static MASound playSound( + String path, float volume, boolean looping, MAGroup group, boolean external) { + MASound sound = + engine.createSound(path, (short) 0, (group != null) ? group : soundsGroup, external); + sound.setVolume(volume); + sound.setLooping(looping); + sound.play(); + return sound; + } + + /** + * Sets the game master/global volume, which is automatically applied to all current sounds. + * + *

(This is just a helper method for creating a faster version of {@code + * Funkin.getAudioEngine().setMasterVolume(float)}). + * + * @param volume The new master volume to set. + */ + public static void setMasterVolume(float volume) { + engine.setMasterVolume(volume); + masterVolume = volume; } public static boolean keyPressed(int key) { @@ -202,9 +273,7 @@ public static void error(String tag, Object message) { public static void error(String tag, Object message, Throwable throwable) { String msg = - (throwable != null) - ? (message + " | Exception: " + throwable) - : message.toString(); + (throwable != null) ? (message + " | Exception: " + throwable) : message.toString(); outputLog(tag, msg, FunkinLogLevel.ERROR); } @@ -212,14 +281,26 @@ public static FunkinGame getGame() { return game; } - public static float getGlobalVolume() { - return globalVolume; - } - public static Stage getStage() { return game.stage; } + public static MiniAudio getAudioEngine() { + return engine; + } + + public static float getMasterVolume() { + return masterVolume; + } + + public static AssetManager getAssetManager() { + return assetManager; + } + + public static MAGroup getSoundsGroup() { + return soundsGroup; + } + /** * Contains all the global events that get dispatched when something happens in the game. * @@ -241,8 +322,7 @@ public static class Signals { public static final FunkinSignal postGameClose = new FunkinSignal<>(); public static final FunkinSignal windowFocused = new FunkinSignal<>(); public static final FunkinSignal windowUnfocused = new FunkinSignal<>(); - public static final FunkinSignal windowMinimized = - new FunkinSignal<>(); + public static final FunkinSignal windowMinimized = new FunkinSignal<>(); public static final FunkinSignal preSoundPlayed = new FunkinSignal<>(); public static final FunkinSignal postSoundPlayed = new FunkinSignal<>(); diff --git a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java index c1df87d..f47890e 100644 --- a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java +++ b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java @@ -2,7 +2,6 @@ import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Pixmap; @@ -22,10 +21,7 @@ import me.stringfromjava.funkin.tween.FunkinTween; import me.stringfromjava.funkin.util.Constants; -import java.util.Set; - import static me.stringfromjava.funkin.util.signal.FunkinSignalData.RenderSignalData; -import static me.stringfromjava.funkin.util.signal.FunkinSignalData.WindowMinimizedSignalData; /** * The game object used for containing the main loop and core elements of Funkin'. @@ -128,7 +124,7 @@ public void resume() {} /** Called when the user regains focus on the game's window. */ public void onWindowFocused() { - Funkin.setGlobalVolume(1); + Funkin.setMasterVolume(1); Funkin.Signals.windowFocused.dispatch(); Funkin.info("Game window has regained focus."); } @@ -138,7 +134,7 @@ public void onWindowUnfocused() { if (isMinimized) { return; } - Funkin.setGlobalVolume(0.008f); + Funkin.setMasterVolume(0.008f); Funkin.Signals.windowUnfocused.dispatch(); Funkin.info("Game window has lost focus."); } @@ -154,8 +150,8 @@ public void onWindowMinimized(boolean iconified) { if (!isMinimized) { return; } - Funkin.setGlobalVolume(0); - Funkin.Signals.windowMinimized.dispatch(new WindowMinimizedSignalData(isMinimized)); + Funkin.setMasterVolume(0); + Funkin.Signals.windowMinimized.dispatch(); Funkin.info("Game window has been minimized."); } @@ -165,7 +161,6 @@ public void dispose() { Funkin.Signals.preGameClose.dispatch(); - // Dispose of the main stage, current screen and batch. Funkin.info("Disposing the screen display..."); Funkin.screen.hide(); Funkin.screen.dispose(); @@ -173,24 +168,13 @@ public void dispose() { batch.dispose(); bgTexture.dispose(); - // Dispose of all sounds and the music (if there is any playing). - Funkin.info("Disposing music..."); + Funkin.info("Disposing all sounds from sound group and music..."); + Funkin.getAudioEngine().dispose(); + Funkin.getSoundsGroup().dispose(); if (Funkin.music != null) { - Funkin.music.stop(); Funkin.music.dispose(); } - Funkin.info("Disposing sound pool..."); - Set soundPoolKeys = Funkin.soundPool.keySet(); - for (long key : soundPoolKeys) { - Sound sound = Funkin.soundPool.get(key); - if (sound == null) { - continue; - } - sound.stop(); - sound.dispose(); - } - Funkin.info("Disposing and shutting down scripts..."); Polyverse.forAllScripts(Script::onDispose); diff --git a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java b/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java deleted file mode 100644 index 542d839..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java +++ /dev/null @@ -1,201 +0,0 @@ -package me.stringfromjava.funkin.audio; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Sound; -import com.badlogic.gdx.files.FileHandle; -import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.backend.Paths; - -import static me.stringfromjava.funkin.util.signal.FunkinSignalData.SoundPlayedSignalData; - -/** - * An enhanced version of libGDX's {@link Sound}. - * - *

This is mostly for ensuring that a sound's volume changes when the users' global volume - * changes. - */ -public class FunkinSound implements Sound { - - /** The ID of {@code this} specific sound. */ - public final long ID; - - private final Sound libGdxSound; - private float volume; - private float pitch; - private float pan; - private boolean looping; - private boolean isPaused; - - public FunkinSound(String path) { - this(Paths.asset(path)); - } - - public FunkinSound(FileHandle path) { - libGdxSound = Gdx.audio.newSound(path); - ID = libGdxSound.play(); - volume = 1.0f; - pitch = 1.0f; - pan = 0.0f; - looping = false; - isPaused = false; - libGdxSound.stop(); - Funkin.soundPool.put(ID, this); - } - - @Override - public long play() { - return libGdxSound.play(volume, pitch, pan); - } - - @Override - public long play(float volume) { - return play(volume, 1.0f, 0.0f); - } - - public long play(float volume, float pitch) { - return play(volume, pitch, 0.0f); - } - - @Override - public long play(float volume, float pitch, float pan) { - Funkin.Signals.preSoundPlayed.dispatch(new SoundPlayedSignalData(this)); - this.volume = volume; - this.pitch = pitch; - this.pan = pan; - this.looping = false; - this.isPaused = false; - Funkin.Signals.postSoundPlayed.dispatch(new SoundPlayedSignalData(this)); - return libGdxSound.play(volume, pitch, pan); - } - - @Override - public long loop() { - return loop(1.0f, 1.0f, 0.0f); - } - - @Override - public long loop(float volume) { - return loop(volume, 1.0f, 0.0f); - } - - public long loop(float volume, float pitch) { - return loop(volume, pitch, 0.0f); - } - - @Override - public long loop(float volume, float pitch, float pan) { - Funkin.Signals.preSoundPlayed.dispatch(new SoundPlayedSignalData(this)); - this.volume = volume * Funkin.getGlobalVolume(); - this.pitch = pitch; - this.pan = pan; - this.looping = true; - this.isPaused = false; - Funkin.Signals.postSoundPlayed.dispatch(new SoundPlayedSignalData(this)); - return libGdxSound.loop(volume, pitch, pan); - } - - @Override - public void stop() { - isPaused = false; - libGdxSound.stop(); - } - - @Override - public void pause() { - isPaused = true; - libGdxSound.pause(); - } - - @Override - public void resume() { - isPaused = false; - libGdxSound.resume(); - } - - @Override - public void dispose() { - volume = 1.0f; - pitch = 1.0f; - pan = 0.0f; - looping = false; - isPaused = false; - libGdxSound.dispose(); - Funkin.soundPool.remove(ID); - } - - @Override - public void stop(long soundId) { - isPaused = false; - libGdxSound.stop(); - } - - @Override - public void pause(long soundId) { - isPaused = true; - libGdxSound.pause(soundId); - } - - @Override - public void resume(long soundId) { - isPaused = false; - libGdxSound.resume(); - } - - @Override - public void setLooping(long soundId, boolean looping) { - this.looping = looping; - libGdxSound.setLooping(soundId, looping); - } - - @Override - public void setPitch(long soundId, float pitch) { - this.pitch = pitch; - libGdxSound.setPitch(soundId, pitch); - } - - @Override - public void setVolume(long soundId, float volume) { - setVolume(soundId, volume, true); - } - - /** - * Sets the new volume for {@code this} sound. - * - * @param soundId The sound's unique ID, generated by libGDX. - * @param volume The new volume to use. - * @param useGlobalVolume Should the new volume automatically adapt to Funkin's global volume? - */ - public void setVolume(long soundId, float volume, boolean useGlobalVolume) { - this.volume = useGlobalVolume ? volume * Funkin.getGlobalVolume() : volume; - libGdxSound.setVolume(soundId, this.volume); - } - - public void setPan(long soundId, float pan) { - setPan(soundId, pan, 1.0f); - } - - @Override - public void setPan(long soundId, float pan, float volume) { - libGdxSound.setPan(soundId, pan, volume); - } - - public float getVolume() { - return volume; - } - - public float getPitch() { - return pitch; - } - - public float getPan() { - return pan; - } - - public boolean isLooping() { - return looping; - } - - public boolean isPaused() { - return isPaused; - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java b/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java index f47e1d3..d406bab 100644 --- a/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java +++ b/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java @@ -3,7 +3,7 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; -/** Utility class for simplifying asset paths. */ +/** Utility class for simplifying asset paths and libGDX {@link FileHandle}s. */ public final class Paths { public static FileHandle asset(String path) { @@ -14,13 +14,17 @@ public static FileHandle shared(String path) { return asset(String.format("shared/%s", path)); } - public static FileHandle xml(String path) { - return asset(path + ".xml"); + public static FileHandle xmlAsset(String path) { + return asset(String.format("%s.xml", path)); } - public static FileHandle sharedImage(String path) { + public static FileHandle sharedImageAsset(String path) { return shared(String.format("images/%s.png", path)); } + public static FileHandle external(String path) { + return Gdx.files.external(path); + } + private Paths() {} } diff --git a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java index a9e9351..a6b5e86 100644 --- a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java +++ b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java @@ -1,10 +1,8 @@ package me.stringfromjava.funkin.game.menus; -import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; -import com.badlogic.gdx.graphics.Texture; +import games.rednblack.miniaudio.MASound; import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.audio.FunkinSound; import me.stringfromjava.funkin.graphics.screen.FunkinScreen; import me.stringfromjava.funkin.backend.Paths; import me.stringfromjava.funkin.graphics.sprite.FunkinSprite; @@ -15,22 +13,22 @@ public class TitleScreen extends FunkinScreen { private FunkinSprite logo; - private FunkinSound tickleFight; private FunkinTween tween; + private MASound tickleFight; @Override public void show() { super.show(); - var t = Paths.sharedImage("noteStrumline"); + var t = Paths.sharedImageAsset("noteStrumline"); var xml = Paths.shared("images/noteStrumline.xml"); logo = new FunkinSprite().loadSparrowFrames(t, xml); logo.addAnimationByPrefix("test", "confirmDown", 24, false); add(logo); - tickleFight = new FunkinSound("shared/sounds/tickleFight.ogg"); - Funkin.playMusic("preload/music/freakyMenu/freakyMenu.ogg", 0.5f); + tickleFight = Funkin.playSound("shared/sounds/tickleFight.ogg"); +// Funkin.playMusic("preload/music/freakyMenu/freakyMenu.ogg", 0.5f); FunkinTweenSettings settings = new FunkinTweenSettings() .addGoal("x", 600) @@ -50,32 +48,32 @@ public void render(float elapsed) { super.render(elapsed); float speed = 500 * elapsed; - if (Gdx.input.isKeyPressed(Input.Keys.W)) { + if (Funkin.keyPressed(Input.Keys.W)) { logo.setY(logo.getY() + speed); } - if (Gdx.input.isKeyPressed(Input.Keys.S)) { + if (Funkin.keyPressed(Input.Keys.S)) { logo.setY(logo.getY() - speed); } - if (Gdx.input.isKeyPressed(Input.Keys.A)) { + if (Funkin.keyPressed(Input.Keys.A)) { logo.setX(logo.getX() - speed); } - if (Gdx.input.isKeyPressed(Input.Keys.D)) { + if (Funkin.keyPressed(Input.Keys.D)) { logo.setX(logo.getX() + speed); } - if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { - logo.playAnimation("test", false); + if (Funkin.keyJustPressed(Input.Keys.SPACE)) { + logo.playAnimation("test", true); } - if (Gdx.input.isKeyJustPressed(Input.Keys.T)) { + if (Funkin.keyJustPressed(Input.Keys.T)) { tween.start(); } - if (Gdx.input.isKeyJustPressed(Input.Keys.R)) { + if (Funkin.keyJustPressed(Input.Keys.R)) { tween.reset(); } - if (Gdx.input.isKeyJustPressed(Input.Keys.Y)) { + if (Funkin.keyJustPressed(Input.Keys.Y)) { if (tween.paused) { tween.resume(); } else { @@ -83,8 +81,8 @@ public void render(float elapsed) { } } - if (Gdx.input.isKeyJustPressed(Input.Keys.Z)) { - tickleFight.play(1.0f); + if (Funkin.keyJustPressed(Input.Keys.Z)) { + tickleFight.play(); } } } diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java index 6562aef..9df4ed0 100644 --- a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java @@ -237,7 +237,21 @@ public void playAnimation(String name) { * @param loop Should this animation loop indefinitely? */ public void playAnimation(String name, boolean loop) { - if (!currentAnim.equals(name) || isAnimationFinished()) { + playAnimation(name, loop, true); + } + + /** + * Plays an animation {@code this} sprite has, if it exists. + * + * @param name The name of the animation to play. + * @param loop Should this animation loop indefinitely? + * @param forceRestart Should the animation automatically restart regardless if its playing? + */ + public void playAnimation(String name, boolean loop, boolean forceRestart) { + if (currentAnim.equals(name) && !forceRestart) { + return; + } + if (isAnimationFinished() || forceRestart) { this.currentAnim = name; this.looping = loop; this.stateTime = 0; diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java index eb592e6..c04a63d 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java @@ -1,7 +1,7 @@ package me.stringfromjava.funkin.tween; import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.backend.FunkinReflect; +import me.stringfromjava.funkin.util.FunkinReflectUtil; import me.stringfromjava.funkin.tween.settings.FunkinTweenSettings; import me.stringfromjava.funkin.util.Constants; import org.jetbrains.annotations.NotNull; @@ -170,7 +170,7 @@ public FunkinTween start() { finished = false; // Ensure that the fields provided actually exist on the object and are floating point values. - var allFields = FunkinReflect.getAllFields(object.getClass()); + var allFields = FunkinReflectUtil.getAllFields(object.getClass()); var neededFields = tweenSettings.getGoalFields(); for (Field field : allFields) { try { diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/FunkinReflect.java b/core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java similarity index 93% rename from core/src/main/java/me/stringfromjava/funkin/backend/FunkinReflect.java rename to core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java index da13cfe..41a64c7 100644 --- a/core/src/main/java/me/stringfromjava/funkin/backend/FunkinReflect.java +++ b/core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java @@ -1,7 +1,6 @@ -package me.stringfromjava.funkin.backend; +package me.stringfromjava.funkin.util; import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.util.Constants; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -13,7 +12,7 @@ * Backend utility class for obtaining and manipulating fields on objects through the usage of Java * reflection. */ -public class FunkinReflect { +public class FunkinReflectUtil { /** * Obtains all fields of a class, including the master types above it all the way to {@link diff --git a/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java index d27c22f..d48cb2c 100644 --- a/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java +++ b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java @@ -1,6 +1,6 @@ package me.stringfromjava.funkin.util.signal; -import me.stringfromjava.funkin.audio.FunkinSound; +import games.rednblack.miniaudio.MASound; import me.stringfromjava.funkin.graphics.screen.FunkinScreen; /** @@ -13,9 +13,7 @@ public record RenderSignalData(float delta) {} public record ScreenSwitchSignalData(FunkinScreen screen) {} - public record SoundPlayedSignalData(FunkinSound sound) {} - - public record WindowMinimizedSignalData(boolean iconified) {} + public record SoundPlayedSignalData(MASound sound) {} private FunkinSignalData() {} } From 25a6012182ced59a12b434cb2f62f0ed4787fb05 Mon Sep 17 00:00:00 2001 From: String Date: Fri, 16 Jan 2026 01:15:07 -0600 Subject: [PATCH 4/6] Too tired to make a real title sob --- android/build.gradle | 4 +- assets/another_test.groovy | 1 - assets/test.groovy | 2 +- .../java/me/stringfromjava/funkin/Funkin.java | 142 ++++++++++++++++-- .../me/stringfromjava/funkin/FunkinGame.java | 79 ++++++---- .../funkin/graphics/screen/FunkinScreen.java | 3 +- .../funkin/graphics/sprite/FunkinSprite.java | 28 +++- .../funkin/tween/FunkinTween.java | 141 +++++++++++------ .../funkin/tween/FunkinTweenManager.java | 50 +++++- .../funkin/util/FunkinReflectUtil.java | 33 ++++ .../funkin/util/FunkinRuntimeUtil.java | 57 +++++++ .../funkin/util/signal/FunkinSignalData.java | 2 + 12 files changed, 442 insertions(+), 100 deletions(-) create mode 100644 core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java diff --git a/android/build.gradle b/android/build.gradle index 9e17554..5218c0e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,7 +9,7 @@ apply plugin: 'com.android.application' android { namespace "me.stringfromjava.funkin" - compileSdk 35 + compileSdk 36 sourceSets { main { manifest.srcFile 'AndroidManifest.xml' @@ -31,7 +31,7 @@ android { } defaultConfig { applicationId 'me.stringfromjava.funkin' - minSdkVersion 26 + minSdkVersion 34 targetSdkVersion 35 versionCode 1 versionName "1.0" diff --git a/assets/another_test.groovy b/assets/another_test.groovy index 60ccb28..b162bc9 100644 --- a/assets/another_test.groovy +++ b/assets/another_test.groovy @@ -1,6 +1,5 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input -import com.badlogic.gdx.graphics.Texture import me.stringfromjava.funkin.Funkin import me.stringfromjava.funkin.backend.Paths import me.stringfromjava.funkin.graphics.sprite.FunkinSprite diff --git a/assets/test.groovy b/assets/test.groovy index a046598..452a41d 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -49,7 +49,7 @@ class TestScreen extends FunkinScreen { bgColor = new Color(0, 1, 0, 1) - Funkin.playMusic('songs/guns/Inst.ogg') + Funkin.playMusic('songs/darnell/Inst.ogg') } @Override diff --git a/core/src/main/java/me/stringfromjava/funkin/Funkin.java b/core/src/main/java/me/stringfromjava/funkin/Funkin.java index bf220e7..c91f5e5 100644 --- a/core/src/main/java/me/stringfromjava/funkin/Funkin.java +++ b/core/src/main/java/me/stringfromjava/funkin/Funkin.java @@ -12,6 +12,7 @@ import me.stringfromjava.funkin.util.signal.FunkinSignal; import me.stringfromjava.funkin.util.Constants; import me.stringfromjava.funkin.util.signal.FunkinSignalData.*; +import org.jetbrains.annotations.NotNull; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -24,21 +25,16 @@ */ public final class Funkin { - /** - * The current {@code FunkinScreen} being displayed, stored in a static instance for global - * access. - * - *

Use this instead of {@code Funkin.game.getScreen()} for actually accessing all the custom - * functions and attributes that a regular {@code Screen} doesn't have! - */ - public static FunkinScreen screen = null; + /** The current {@code FunkinScreen} being displayed. */ + private static FunkinScreen screen; /** The main audio object used to create, */ private static MiniAudio engine; + /** The global asset manager used to obtain preloaded assets. */ private static AssetManager assetManager; - /** The audio group for regular small, sound effects. */ + /** The audio group for all sound effects, including the current music. */ private static MAGroup soundsGroup; /** The sound for playing music throughout the game. */ @@ -48,7 +44,7 @@ public final class Funkin { private static float masterVolume = 1; /** - * The static instance used to access the core elements of the game. This includes the loop, + * The static instance used to access the core elements of the game. This includes the main loop, * setting the current screen, and more. */ private static FunkinGame game; @@ -217,15 +213,109 @@ public static MASound playSound(String path, float volume, boolean looping, MAGr * @return The new sound instance. */ public static MASound playSound( - String path, float volume, boolean looping, MAGroup group, boolean external) { + @NotNull String path, float volume, boolean looping, MAGroup group, boolean external) { MASound sound = engine.createSound(path, (short) 0, (group != null) ? group : soundsGroup, external); + Signals.preSoundPlayed.dispatch(new SoundPlayedSignalData(sound)); sound.setVolume(volume); sound.setLooping(looping); sound.play(); + Signals.postSoundPlayed.dispatch(new SoundPlayedSignalData(sound)); return sound; } + /** + * Sets the current music playing for the entire game. + * + *

When you want to play music located externally, outside the assets folder, you can use a + * {@link FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * Funkin.playMusic(Paths.external("your/path/here").path());
+   * }
+ * + * @param path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + */ + public static void playMusic(String path) { + playMusic(path, 1, true, false); + } + + /** + * Sets the current music playing for the entire game. + * + *

When you want to play music located externally, outside the assets folder, you can use a + * {@link FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * Funkin.playMusic(Paths.external("your/path/here").path(), 1);
+   * }
+ * + * @param path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new music with. + */ + public static void playMusic(String path, float volume) { + playMusic(path, volume, true, false); + } + + /** + * Sets the current music playing for the entire game. + * + *

When you want to play music located externally, outside the assets folder, you can use a + * {@link FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * Funkin.playMusic(Paths.external("your/path/here").path(), 1, false);
+   * }
+ * + * @param path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new music with. + * @param looping Should the new music loop indefinitely? + */ + public static void playMusic(String path, float volume, boolean looping) { + playMusic(path, volume, looping, false); + } + + /** + * Sets the current music playing for the entire game. + * + *

When you want to play music located externally, outside the assets folder, you can use a + * {@link FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * // For the boolean attribuite "external", you only should make it true for mobile builds,
+   * // otherwise just simply leave it be or make it "false" for other platforms like desktop.
+   * Funkin.playMusic(Paths.external("your/path/here").path(), 1, false, true);
+   * }
+ * + * @param path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new music with. + * @param looping Should the new music loop indefinitely? + * @param external Should this music be loaded externally? (This is only for mobile platforms!) + */ + public static void playMusic(String path, float volume, boolean looping, boolean external) { + Signals.preMusicPlayed.dispatch(new MusicPlayedSignalData(music)); + if (music != null) { + music.stop(); + } + music = engine.createSound(path, (short) 0, soundsGroup, external); + music.setVolume(volume); + music.setLooping(looping); + music.play(); + Signals.postMusicPlayed.dispatch(new MusicPlayedSignalData(music)); + } + /** * Sets the game master/global volume, which is automatically applied to all current sounds. * @@ -285,6 +375,14 @@ public static Stage getStage() { return game.stage; } + public static FunkinScreen getScreen() { + return screen; + } + + public static MASound getMusic() { + return music; + } + public static MiniAudio getAudioEngine() { return engine; } @@ -311,7 +409,7 @@ public static MAGroup getSoundsGroup() { * same thing. If a signal has {@code pre}, then the signal gets ran BEFORE any functionality is * executed, and {@code post} means AFTER all functionality was executed. */ - public static class Signals { + public static final class Signals { public static final FunkinSignal preRender = new FunkinSignal<>(); public static final FunkinSignal postRender = new FunkinSignal<>(); @@ -325,6 +423,8 @@ public static class Signals { public static final FunkinSignal windowMinimized = new FunkinSignal<>(); public static final FunkinSignal preSoundPlayed = new FunkinSignal<>(); public static final FunkinSignal postSoundPlayed = new FunkinSignal<>(); + public static final FunkinSignal preMusicPlayed = new FunkinSignal<>(); + public static final FunkinSignal postMusicPlayed = new FunkinSignal<>(); private Signals() {} } @@ -334,6 +434,17 @@ private Signals() {} // ====================================== private static void outputLog(String tag, Object message, FunkinLogLevel level) { + StackWalker.StackFrame caller = StackWalker.getInstance() + .walk(s -> s.skip(3).findFirst()) + .orElse(null); + + String file = "UnknownFile.java:0"; + String method = "unknownMethod()"; + if (caller != null) { + file = caller.getFileName() + ":" + caller.getLineNumber(); + method = caller.getMethodName() + "()"; + } + String color = switch (level) { case INFO -> Constants.AsciiCodes.WHITE; @@ -350,7 +461,12 @@ private static void outputLog(String tag, Object message, FunkinLogLevel level) false, underline); String formattedTag = - colorText("[" + tag + "] [" + level + "] ", color, true, false, underline); + colorText( + "[" + level + "] [" + tag + "] [" + file + "] [" + method + "] ", + color, + true, + false, + underline); String formattedMessage = colorText(message.toString(), color, false, true, underline); System.out.println(timeAndDate + formattedTag + formattedMessage); diff --git a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java index f47890e..ab8d10d 100644 --- a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java +++ b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java @@ -1,5 +1,6 @@ package me.stringfromjava.funkin; +import com.badlogic.gdx.Application; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; @@ -20,6 +21,7 @@ import me.stringfromjava.funkin.polyverse.script.type.SystemScript; import me.stringfromjava.funkin.tween.FunkinTween; import me.stringfromjava.funkin.util.Constants; +import me.stringfromjava.funkin.util.FunkinRuntimeUtil; import static me.stringfromjava.funkin.util.signal.FunkinSignalData.RenderSignalData; @@ -51,28 +53,9 @@ public class FunkinGame implements ApplicationListener { @Override public void create() { - // Configure the main view of the game's world and view. - var wWidth = Constants.Display.WINDOW_WIDTH; - var wHeight = Constants.Display.WINDOW_HEIGHT; - - batch = new SpriteBatch(); - viewport = new FitViewport(wWidth, wHeight); - viewport.apply(); - - camera = new OrthographicCamera(); - camera.setToOrtho(false, wWidth, wHeight); - - stage = new Stage(viewport, batch); - - Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888); - pixmap.setColor(Color.WHITE); - pixmap.fill(); - bgTexture = new Texture(pixmap); - pixmap.dispose(); - - // Configure the Polyverse scripting and modding engine for the game. - configurePolyverse(); - + configureCrashHandler(); // Crash handler for uncaught exceptions. + configureWindow(); // Window and viewport. + configurePolyverse(); // Polyverse scripting and modding system. Funkin.setScreen(new InitScreen()); } @@ -84,7 +67,7 @@ public void resize(int width, int height) { @Override public void render() { float delta = Gdx.graphics.getDeltaTime(); - FunkinScreen screen = Funkin.screen; + FunkinScreen screen = Funkin.getScreen(); Funkin.Signals.preRender.dispatch(new RenderSignalData(delta)); @@ -98,7 +81,8 @@ public void render() { batch.draw(bgTexture, 0, 0, viewport.getWorldWidth(), viewport.getWorldHeight()); batch.setColor(Color.WHITE); // Set color back to white so display objects aren't affected. screen.render(delta); - for (FunkinObject object : screen.members) { + for (int i = screen.members.size - 1; i >= 0; i--) { + FunkinObject object = screen.members.get(i); if (object instanceof FunkinSprite sprite) { sprite.update(delta); sprite.draw(batch); @@ -110,7 +94,7 @@ public void render() { stage.act(delta); stage.draw(); - FunkinTween.globalManager.update(delta); + FunkinTween.getGlobalManager().update(delta); Polyverse.forAllScripts(script -> script.onRender(delta)); Funkin.Signals.postRender.dispatch(new RenderSignalData(delta)); @@ -162,18 +146,18 @@ public void dispose() { Funkin.Signals.preGameClose.dispatch(); Funkin.info("Disposing the screen display..."); - Funkin.screen.hide(); - Funkin.screen.dispose(); + Funkin.getScreen().hide(); + Funkin.getScreen().dispose(); stage.dispose(); batch.dispose(); bgTexture.dispose(); Funkin.info("Disposing all sounds from sound group and music..."); - Funkin.getAudioEngine().dispose(); - Funkin.getSoundsGroup().dispose(); - if (Funkin.music != null) { - Funkin.music.dispose(); + if (Funkin.getMusic() != null) { + Funkin.getMusic().dispose(); } + Funkin.getSoundsGroup().dispose(); + Funkin.getAudioEngine().dispose(); Funkin.info("Disposing and shutting down scripts..."); Polyverse.forAllScripts(Script::onDispose); @@ -185,6 +169,39 @@ public boolean isMinimized() { return isMinimized; } + private void configureCrashHandler() { + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + String logs = FunkinRuntimeUtil.getFullExceptionMessage(throwable); + String msg = "There was an uncaught exception on thread \"" + thread.getName() + "\"!\n\t" + logs; + Funkin.error(Constants.System.LOG_TAG, msg); + dispose(); + // Only use Gdx.app.exit() on non-iOS platforms to avoid App Store guideline violations! + if (Gdx.app.getType() != Application.ApplicationType.iOS) { + Gdx.app.exit(); + } + }); + } + + private void configureWindow() { + var wWidth = Constants.Display.WINDOW_WIDTH; + var wHeight = Constants.Display.WINDOW_HEIGHT; + + batch = new SpriteBatch(); + viewport = new FitViewport(wWidth, wHeight); + viewport.apply(); + + camera = new OrthographicCamera(); + camera.setToOrtho(false, wWidth, wHeight); + + stage = new Stage(viewport, batch); + + Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888); + pixmap.setColor(Color.WHITE); + pixmap.fill(); + bgTexture = new Texture(pixmap); + pixmap.dispose(); + } + private void configurePolyverse() { // Register Polyverse script types. Polyverse.registerScriptType(Script.class); // Master type, DO NOT REMOVE THIS! diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java b/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java index 64f82f9..39a2aa8 100644 --- a/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java @@ -3,6 +3,7 @@ import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.utils.Array; import me.stringfromjava.funkin.graphics.sprite.FunkinObject; import java.util.concurrent.CopyOnWriteArrayList; @@ -17,7 +18,7 @@ public abstract class FunkinScreen implements Screen { protected Color bgColor; /** All display objects that are shown in {@code this} screen. */ - public final CopyOnWriteArrayList members = new CopyOnWriteArrayList<>(); + public final Array members = new Array<>(); @Override public void show() {} diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java index 9df4ed0..8fd3f70 100644 --- a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java @@ -8,6 +8,7 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Pool; import com.badlogic.gdx.utils.XmlReader; import java.util.Comparator; @@ -19,7 +20,7 @@ * *

It allows you to load animations, textures, and do much more with simplicity and ease. */ -public class FunkinSprite extends Sprite implements FunkinObject { +public class FunkinSprite extends Sprite implements FunkinObject, Pool.Poolable { /** The texture image that {@code this} sprite uses. */ protected Texture texture; @@ -295,6 +296,31 @@ public boolean isAnimationFinished() { return anim.isAnimationFinished(stateTime); } + @Override + public void reset() { + setPosition(0f, 0f); + stateTime = 0; + currentAnim = null; + looping = true; + texture.dispose(); + texture = null; + currentFrame.getTexture().dispose(); + currentFrame = null; + for (int i = atlasRegions.size; i >= 0; i--) { + var region = atlasRegions.items[i]; + region.getTexture().dispose(); + } + atlasRegions.setSize(0); + atlasRegions = null; + for (int i = frames.length - 1; i >= 0; i--) { + var frame = frames[i]; + for (TextureRegion region : frame) { + region.getTexture().dispose(); + } + } + frames = null; + } + public Map> getAnimations() { return animations; } diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java index c04a63d..65d71a0 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java @@ -1,5 +1,6 @@ package me.stringfromjava.funkin.tween; +import com.badlogic.gdx.utils.Pool; import me.stringfromjava.funkin.Funkin; import me.stringfromjava.funkin.util.FunkinReflectUtil; import me.stringfromjava.funkin.tween.settings.FunkinTweenSettings; @@ -7,18 +8,21 @@ import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * Core class for creating new tweens to add nice and smooth animations to visual objects. * *

Note that this doesn't have to be used on sprites, it can be used on just about anything! */ -public class FunkinTween { +public class FunkinTween implements Pool.Poolable { /** The global tween manager for the entire game. */ - public static FunkinTweenManager globalManager = new FunkinTweenManager(); + protected static FunkinTweenManager globalManager = new FunkinTweenManager(); /** The object to tween. */ protected Object object; @@ -59,15 +63,25 @@ public class FunkinTween { protected final Map initialValues = new HashMap<>(); /** + * Cache of the fields being tweened for faster access so they aren't accessed every time the + * {@code start()} method is called. + */ + protected Field[] fieldsCache = null; + + /** + * Constructs a new tween. + * + *

Note that this does NOT add the tween to the global manager, it just assigns its main + * values. That's it. If you wish to create a tween to automatically start working, you might want + * to see {@code tween()} + * * @param object The object to tween values. * @param settings The settings that configure and determine how the tween should animate and last * for. * @param updateCallback Callback function for updating the objects values when the tween updates. */ - public FunkinTween( - Object object, - @NotNull FunkinTweenSettings settings, - FunkinTweenUpdateCallback updateCallback) { + protected FunkinTween( + Object object, FunkinTweenSettings settings, FunkinTweenUpdateCallback updateCallback) { this.object = object; this.tweenSettings = settings; this.updateCallback = updateCallback; @@ -90,6 +104,15 @@ public void update(float delta) { if (paused || finished) { return; } + if (object == null) { + return; + } + if (tweenSettings == null) { + return; + } + if (updateCallback == null) { + return; + } var ease = tweenSettings.getEase(); var duration = tweenSettings.getDuration(); @@ -125,13 +148,6 @@ public void update(float delta) { } } - if (secondsSinceStart > delay && !running) { - running = true; - if (onStart != null) { - onStart.run(this); - } - } - // Update the object's fields based on the tween progress. var newValues = new HashMap(); for (String field : tweenSettings.getGoalFields()) { @@ -147,11 +163,6 @@ public void update(float delta) { if (secondsSinceStart >= duration + delay) { scale = (backward) ? 0 : 1; finished = true; - switch (tweenSettings.getType()) { - case ONESHOT -> { - manager.activeTweens.remove(this); - } - } } else { if (postTick > preTick && onUpdate != null) { onUpdate.run(this); @@ -169,24 +180,43 @@ public FunkinTween start() { running = true; finished = false; - // Ensure that the fields provided actually exist on the object and are floating point values. - var allFields = FunkinReflectUtil.getAllFields(object.getClass()); + if (tweenSettings == null) { + Funkin.warn("FunkinTween", "No tween settings were provided for the tween."); + return this; + } + var neededFields = tweenSettings.getGoalFields(); - for (Field field : allFields) { + if (neededFields == null || neededFields.isEmpty()) { + Funkin.warn("FunkinTween", "No fields were provided to tween on the object."); + return this; + } + if (fieldsCache == null) { + fieldsCache = FunkinReflectUtil.getAllFieldsAsArray(object.getClass()); + } + + // Ensure that the fields provided actually exist on the object and are floating point values. + // If there are fields that the tween is trying to tween that don't exist, then throw an error. + Set fieldIds = Arrays.stream(fieldsCache) + .map(Field::getName) + .collect(Collectors.toSet()); + for (String neededField : neededFields) { + if (!fieldIds.contains(neededField)) { + String message = "Field \"" + neededField + "\" does not exist on the given object."; + Funkin.error("FunkinTween", message); + throw new RuntimeException(message); + } + } + + for (Field field : fieldsCache) { try { String fName = field.getName(); - if (!field.trySetAccessible()) { - continue; - } - if (!neededFields.contains(fName)) { - continue; - } if (field.getType() != float.class) { continue; } initialValues.put(fName, field.getFloat(object)); } catch (IllegalAccessException e) { - Funkin.error(Constants.System.LOG_TAG, "Could not access field \"" + field.getName() + "\".", e); + Funkin.error( + Constants.System.LOG_TAG, "Could not access field \"" + field.getName() + "\".", e); } } return this; @@ -226,31 +256,43 @@ public FunkinTween stop() { } /** - * Cancels {@code this} tween and immediately removes it from the active tweens in its manager. + * Cancels {@code this} tween and automatically defaults its values, removing it from the manager + * by default. * * @return {@code this} tween. */ public FunkinTween cancel() { - reset(); - manager.activeTweens.remove(this); - return this; + return cancel(true); } /** - * Resets {@code this} tween back to its initial state without removing it from its manager. + * Cancels {@code this} tween and automatically defaults its values. * + * @param remove Should {@code this} tween be removed from its manager? * @return {@code this} tween. */ - public FunkinTween reset() { - paused = false; - backward = false; - running = false; - finished = true; + public FunkinTween cancel(boolean remove) { + reset(); + if (remove) { + manager.getTweenPool().free(this); + } + return this; + } + + @Override + public void reset() { + object = null; + tweenSettings = null; + updateCallback = null; + manager = null; scale = 0.0f; secondsSinceStart = 0.0f; executions = 0; + paused = false; + running = false; + finished = false; + backward = false; initialValues.clear(); - return this; } public FunkinTweenSettings getTweenSettings() { @@ -262,10 +304,19 @@ public FunkinTween setTweenSettings(@NotNull FunkinTweenSettings tweenSettings) return this; } - public FunkinTween setManager(FunkinTweenManager manager) { - if (manager != null) { - this.manager = manager; - this.manager.activeTweens.add(this); + public static FunkinTweenManager getGlobalManager() { + return globalManager; + } + + public FunkinTween setManager(FunkinTweenManager newManager) { + if (newManager != null) { + if (manager != null) { + int index = manager.getActiveTweens().indexOf(this, true); + manager.getActiveTweens().removeIndex(index); + manager.getTweenPool().free(this); + } + manager = newManager; + manager.getTweenPool().obtain(); } return this; } @@ -275,8 +326,8 @@ public FunkinTween setManager(FunkinTweenManager manager) { public interface FunkinTweenUpdateCallback { /** - * A callback method that is called when the tween updates its values during its tweening - * (or animating) process. + * A callback method that is called when the tween updates its values during its tweening (or + * animating) process. * * @param values The new current values of the fields being tweened during the animation. */ diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java index 709be1d..feff3fe 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java @@ -1,19 +1,59 @@ package me.stringfromjava.funkin.tween; -import java.util.concurrent.CopyOnWriteArrayList; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Pool; /** Core manager class for handling all {@link FunkinTween}s that are currently active. */ -public final class FunkinTweenManager { +public class FunkinTweenManager { - /** A list where all current active tweens are stored. */ - public final CopyOnWriteArrayList activeTweens = new CopyOnWriteArrayList<>(); + /** Array where all current active tweens are stored. */ + protected final Array activeTweens = new Array<>(); + /** A pool where all tweens are stored to preserve memory. */ + protected final Pool tweenPool = new Pool<>() { + @Override + protected FunkinTween newObject() { + return new FunkinTween(null, null, null); + } + }; + + /** + * Updates all active tweens that are stored and updated in {@code this} manager. + * + * @param delta The amount of time that has passed since the last frame. + */ public void update(float delta) { - for (FunkinTween tween : activeTweens) { + for (int i = activeTweens.size - 1; i >= 0; i--) { + FunkinTween tween = activeTweens.items[i]; if (tween == null) { continue; } tween.update(delta); + + if (tween.finished) { + if (tween.manager != this) { + continue; + } + var settings = tween.getTweenSettings(); + if (settings == null) { + continue; + } + + switch (settings.getType()) { + case ONESHOT -> { + activeTweens.removeIndex(i); + tweenPool.free(tween); + } + } + } } } + + public Array getActiveTweens() { + return activeTweens; + } + + public Pool getTweenPool() { + return tweenPool; + } } diff --git a/core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java b/core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java index 41a64c7..2fae80d 100644 --- a/core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java +++ b/core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; /** * Backend utility class for obtaining and manipulating fields on objects through the usage of Java @@ -14,6 +15,26 @@ */ public class FunkinReflectUtil { + /** + * Checks if a field exists on a given object. + * + * @param target The object to verify the existence of the given field to check. + * @param fieldName The name of the + * @return If the field exists on the given object. + */ + public static boolean hasField(Object target, String fieldName) { + AtomicBoolean exists = new AtomicBoolean(); + getAllFields(target.getClass()) + .forEach( + field -> { + if (field.getName().equals(fieldName)) { + exists.set(true); + } + exists.set(false); + }); + return exists.get(); + } + /** * Obtains all fields of a class, including the master types above it all the way to {@link * Object}. @@ -29,6 +50,18 @@ public static List getAllFields(Class type) { return fields; } + /** + * Obtains all fields of a class, including the master types above it all the way to {@link + * Object}. Obvious to the name, this version returns an array instead of a list unlike its + * similar function . + * + * @param type A class literal to obtain the fields from. + * @return All fields from itself and its master classes above it. + */ + public static Field[] getAllFieldsAsArray(Class type) { + return getAllFields(type).toArray(new Field[0]); + } + /** * Checks if a class of a certain package is final. * diff --git a/core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java b/core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java new file mode 100644 index 0000000..84b8b04 --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java @@ -0,0 +1,57 @@ +package me.stringfromjava.funkin.util; + +/** + * Utility class for handling operation related to the runtime environment, including OS detection, + * extracting runtime information, obtaining information from exceptions, and other related tasks. + */ +public final class FunkinRuntimeUtil { + + /** + * Obtains a string representation of where an exception was thrown from, including the class, + * method, file, and line number. + * + * @param exception The exception to obtain the location from. + * @return A string representation of where the exception was thrown from. + */ + public static String getExceptionLocation(Throwable exception) { + if (exception == null) { + return "Unknown Location"; + } + StackTraceElement[] stackTrace = exception.getStackTrace(); + if (stackTrace.length == 0) { + return "Unknown Location"; + } + StackTraceElement element = stackTrace[0]; + return "FILE=" + + element.getFileName() + + ", CLASS=" + + element.getClassName() + + ", METHOD=" + + element.getMethodName() + + "(), LINE=" + + element.getLineNumber(); + } + + /** + * Obtains a full detailed message from an exception, including its type, location, and stack + * trace. + * + * @param exception The exception to obtain the message from. + * @return A full detailed message from the exception. + */ + public static String getFullExceptionMessage(Throwable exception) { + if (exception == null) { + return "No Exception Provided"; + } + StringBuilder messageBuilder = new StringBuilder(); + messageBuilder.append("Exception: ").append(exception.toString()).append("\n"); + messageBuilder.append("Location: ").append(getExceptionLocation(exception)).append("\n"); + messageBuilder.append("Stack Trace:\n"); + for (StackTraceElement element : exception.getStackTrace()) { + messageBuilder.append("\tat ").append(element.toString()).append("\n"); + } + return messageBuilder.toString(); + } + + private FunkinRuntimeUtil() {} +} diff --git a/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java index d48cb2c..8bb6fef 100644 --- a/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java +++ b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java @@ -15,6 +15,8 @@ public record ScreenSwitchSignalData(FunkinScreen screen) {} public record SoundPlayedSignalData(MASound sound) {} + public record MusicPlayedSignalData(MASound music) {} + private FunkinSignalData() {} } From 5d257b978818a73600529e567b0e36324d121a8f Mon Sep 17 00:00:00 2001 From: String Date: Sun, 18 Jan 2026 01:42:10 -0600 Subject: [PATCH 5/6] Fix offset bugs, add multiple optimizations and add fullscreen --- .../me/stringfromjava/funkin/FunkinGame.java | 47 ++++++++++++----- .../funkin/game/menus/TitleScreen.java | 4 +- .../funkin/graphics/screen/FunkinScreen.java | 7 +-- .../funkin/graphics/sprite/FunkinSprite.java | 50 +++++++++++------- .../funkin/polyverse/Polyverse.java | 51 ++++++++----------- .../funkin/tween/FunkinTween.java | 43 ++++++++++++---- .../funkin/tween/FunkinTweenManager.java | 9 ++-- .../tween/settings/FunkinTweenSettings.java | 5 ++ 8 files changed, 138 insertions(+), 78 deletions(-) diff --git a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java index ab8d10d..fa198c4 100644 --- a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java +++ b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java @@ -3,6 +3,7 @@ import com.badlogic.gdx.Application; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Pixmap; @@ -71,26 +72,32 @@ public void render() { Funkin.Signals.preRender.dispatch(new RenderSignalData(delta)); + if (Funkin.keyJustPressed(Input.Keys.F11)) { + toggleFullscreen(); + } + // Update and render the current screen that's active. ScreenUtils.clear(Color.BLACK); viewport.apply(); batch.setProjectionMatrix(camera.combined); batch.begin(); + if (screen != null) { batch.setColor(screen.getBgColor()); batch.draw(bgTexture, 0, 0, viewport.getWorldWidth(), viewport.getWorldHeight()); batch.setColor(Color.WHITE); // Set color back to white so display objects aren't affected. screen.render(delta); - for (int i = screen.members.size - 1; i >= 0; i--) { - FunkinObject object = screen.members.get(i); + var members = screen.members.begin(); + for (FunkinObject object : members) { if (object instanceof FunkinSprite sprite) { sprite.update(delta); sprite.draw(batch); } } + screen.members.end(); } - batch.end(); + batch.end(); stage.act(delta); stage.draw(); @@ -139,6 +146,18 @@ public void onWindowMinimized(boolean iconified) { Funkin.info("Game window has been minimized."); } + /** Toggles fullscreen mode on or off, depending on the current state. */ + public void toggleFullscreen() { + boolean isFullscreen = Gdx.graphics.isFullscreen(); + if (isFullscreen) { + Gdx.graphics.setWindowedMode(Constants.Display.WINDOW_WIDTH, Constants.Display.WINDOW_HEIGHT); + Funkin.info("Exiting fullscreen mode."); + } else { + Gdx.graphics.setFullscreenMode(Gdx.graphics.getDisplayMode()); + Funkin.info("Entering fullscreen mode."); + } + } + @Override public void dispose() { Funkin.warn("SHUTTING DOWN GAME AND DISPOSING ALL RESOURCES."); @@ -170,16 +189,18 @@ public boolean isMinimized() { } private void configureCrashHandler() { - Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { - String logs = FunkinRuntimeUtil.getFullExceptionMessage(throwable); - String msg = "There was an uncaught exception on thread \"" + thread.getName() + "\"!\n\t" + logs; - Funkin.error(Constants.System.LOG_TAG, msg); - dispose(); - // Only use Gdx.app.exit() on non-iOS platforms to avoid App Store guideline violations! - if (Gdx.app.getType() != Application.ApplicationType.iOS) { - Gdx.app.exit(); - } - }); + Thread.setDefaultUncaughtExceptionHandler( + (thread, throwable) -> { + String logs = FunkinRuntimeUtil.getFullExceptionMessage(throwable); + String msg = + "There was an uncaught exception on thread \"" + thread.getName() + "\"!\n" + logs; + Funkin.error(Constants.System.LOG_TAG, msg); + dispose(); + // Only use Gdx.app.exit() on non-iOS platforms to avoid App Store guideline violations! + if (Gdx.app.getType() != Application.ApplicationType.iOS) { + Gdx.app.exit(); + } + }); } private void configureWindow() { diff --git a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java index a6b5e86..b2cb975 100644 --- a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java +++ b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java @@ -9,6 +9,7 @@ import me.stringfromjava.funkin.tween.FunkinTween; import me.stringfromjava.funkin.tween.FunkinEase; import me.stringfromjava.funkin.tween.settings.FunkinTweenSettings; +import me.stringfromjava.funkin.tween.settings.FunkinTweenType; public class TitleScreen extends FunkinScreen { @@ -35,7 +36,8 @@ public void show() { .addGoal("y", 40) .addGoal("rotation", 180) .setDuration(0.7f) - .setEase(FunkinEase::circInOut); + .setEase(FunkinEase::circInOut) + .setType(FunkinTweenType.PERSIST); tween = FunkinTween.tween(logo, settings, values -> { logo.setX(values.get("x")); logo.setY(values.get("y")); diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java b/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java index 39a2aa8..69d390f 100644 --- a/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java @@ -2,12 +2,9 @@ import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.g2d.Sprite; -import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.SnapshotArray; import me.stringfromjava.funkin.graphics.sprite.FunkinObject; -import java.util.concurrent.CopyOnWriteArrayList; - /** * Base class for creating a better screen display with more functionality than the default {@link * com.badlogic.gdx.Screen} interface. @@ -18,7 +15,7 @@ public abstract class FunkinScreen implements Screen { protected Color bgColor; /** All display objects that are shown in {@code this} screen. */ - public final Array members = new Array<>(); + public final SnapshotArray members = new SnapshotArray<>(FunkinObject.class); @Override public void show() {} diff --git a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java index 8fd3f70..0811048 100644 --- a/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java @@ -7,6 +7,7 @@ import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Pool; import com.badlogic.gdx.utils.XmlReader; @@ -25,6 +26,11 @@ public class FunkinSprite extends Sprite implements FunkinObject, Pool.Poolable /** The texture image that {@code this} sprite uses. */ protected Texture texture; + /** + * The hitbox used for collision detection and angling. + */ + protected Rectangle hitbox; + /** The atlas regions used in this sprite (used for animations). */ protected Array atlasRegions; @@ -166,6 +172,15 @@ public FunkinSprite loadSparrowFrames(Texture texture, XmlReader.Element xmlFile atlasRegions.add(region); } + if (atlasRegions.size > 0) { + currentFrame = atlasRegions.first(); + setRegion(currentFrame); + + // This ensures the Sprite's internal width/height matches + // the frame before the first update(). + setSize(currentFrame.getRegionWidth(), currentFrame.getRegionHeight()); + } + // Set the default visual. setRegion(atlasRegions.first()); setSize(getRegionWidth(), getRegionHeight()); @@ -261,33 +276,34 @@ public void playAnimation(String name, boolean loop, boolean forceRestart) { @Override public void draw(Batch batch) { - if (currentFrame != null) { - // We use the currentFrame's offsets to adjust the drawing position. - // This prevents the "shaking" or "jittering" in sparrow animations. - float drawX = getX() + currentFrame.offsetX; - - // libGDX coordinate system is bottom-left, but Sparrow is top-down. - // This math aligns the frame within its "original" box. - float drawY = + if (currentFrame == null) { + super.draw(batch); + return; + } + + // Calculate the center based on the ORIGINAL frame size. + // This ensures the rotation point stays consistent across frames. + float originX = currentFrame.originalWidth / 2f; + float originY = currentFrame.originalHeight / 2f; + + // Adjust drawing position by the Sparrow offsets + float drawX = getX() + currentFrame.offsetX; + float drawY = getY() - + (currentFrame.originalHeight - - currentFrame.getRegionHeight() - - currentFrame.offsetY); + + (currentFrame.originalHeight - currentFrame.getRegionHeight() - currentFrame.offsetY); - batch.draw( + batch.draw( currentFrame, drawX, drawY, - getOriginX(), - getOriginY(), + originX - currentFrame.offsetX, + originY + - (currentFrame.originalHeight - currentFrame.getRegionHeight() - currentFrame.offsetY), currentFrame.getRegionWidth(), currentFrame.getRegionHeight(), getScaleX(), getScaleY(), getRotation()); - } else { - super.draw(batch); // Fallback to standard sprite drawing. - } } public boolean isAnimationFinished() { diff --git a/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java b/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java index b1f7007..50a98e6 100644 --- a/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java +++ b/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java @@ -46,7 +46,8 @@ public static List getScripts(Class type) { /** * Registers a new script instance into the Polyverse system, auto-detecting its type accordingly. - * Note that this automatically calls the script's {@link Script#onCreate()} method after registration. + * Note that this automatically calls the script's {@link Script#onCreate()} method after + * registration. * * @param handle The {@link FileHandle} that holds where the script is located. */ @@ -103,43 +104,35 @@ public static void registerScript(FileHandle handle) { * @param action The action to perform on each script. */ public static void forEachScript(Class type, Consumer action) { - List scriptList = getScripts(type); - - // Use a standard for-loop to prevent ConcurrentModificationException - // and ensure we are iterating over the current snapshot of scripts. - for (int i = 0; i < scriptList.size(); i++) { - T script = scriptList.get(i); - if (script != null) { - try { - action.accept(script); - } catch (Exception e) { - Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName(), e); - } - } - } + executeScriptList(getScripts(type), action); } /** - * Executes an action for all scripts of all types, with error handling. - * Note that this function is NOT recommended to be used frequently due to how generic it is. It's - * mainly intended for cleanup and disposal tasks. If you know the specific type of scripts you - * want to operate on, use {@link #forEachScript(Class, Consumer)} instead. + * Executes an action for all scripts of all types, with error handling. Note that this function + * is NOT recommended to be used frequently due to how generic it is. It's mainly intended for + * cleanup and disposal tasks. If you know the specific type of scripts you want to operate on, + * use {@link #forEachScript(Class, Consumer)} instead. * * @param action The action to perform on each script. */ public static void forAllScripts(Consumer action) { for (Class type : scripts.keySet()) { + executeScriptList(scripts.get(type), action); + } + } + + private static void executeScriptList( + List scriptList, Consumer action) { + // Use a standard for-loop to prevent ConcurrentModificationException + // and ensure we are iterating over the current snapshot of scripts. + for (int i = 0; i < scriptList.size(); i++) { @SuppressWarnings("unchecked") - List scriptList = (List) scripts.get(type); - - for (int i = 0; i < scriptList.size(); i++) { - T script = scriptList.get(i); - if (script != null) { - try { - action.accept(script); - } catch (Exception e) { - Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName(), e); - } + T script = (T) scriptList.get(i); + if (script != null) { + try { + action.accept(script); + } catch (Exception e) { + Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName(), e); } } } diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java index 65d71a0..ca533d4 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java @@ -68,6 +68,9 @@ public class FunkinTween implements Pool.Poolable { */ protected Field[] fieldsCache = null; + /** Default constructor for pooling purposes. */ + protected FunkinTween() {} + /** * Constructs a new tween. * @@ -176,7 +179,7 @@ public void update(float delta) { * @return {@code this} tween. */ public FunkinTween start() { - reset(); + resetBasic(); running = true; finished = false; @@ -184,21 +187,20 @@ public FunkinTween start() { Funkin.warn("FunkinTween", "No tween settings were provided for the tween."); return this; } - var neededFields = tweenSettings.getGoalFields(); if (neededFields == null || neededFields.isEmpty()) { Funkin.warn("FunkinTween", "No fields were provided to tween on the object."); return this; } + if (fieldsCache == null) { fieldsCache = FunkinReflectUtil.getAllFieldsAsArray(object.getClass()); } // Ensure that the fields provided actually exist on the object and are floating point values. // If there are fields that the tween is trying to tween that don't exist, then throw an error. - Set fieldIds = Arrays.stream(fieldsCache) - .map(Field::getName) - .collect(Collectors.toSet()); + Set fieldIds = + Arrays.stream(fieldsCache).map(Field::getName).collect(Collectors.toSet()); for (String neededField : neededFields) { if (!fieldIds.contains(neededField)) { String message = "Field \"" + neededField + "\" does not exist on the given object."; @@ -210,6 +212,12 @@ public FunkinTween start() { for (Field field : fieldsCache) { try { String fName = field.getName(); + if (!field.trySetAccessible()) { + continue; + } + if (!neededFields.contains(fName)) { + continue; + } if (field.getType() != float.class) { continue; } @@ -281,10 +289,26 @@ public FunkinTween cancel(boolean remove) { @Override public void reset() { - object = null; - tweenSettings = null; - updateCallback = null; +// object = null; +// tweenSettings = null; +// updateCallback = null; manager = null; + scale = 0.0f; + secondsSinceStart = 0.0f; + executions = 0; + paused = false; + running = false; + finished = false; + backward = false; + fieldsCache = null; + initialValues.clear(); + } + + /** + * Resets only the basic values of {@code this} tween without removing any references to the + * object, its settings or its callback function. + */ + public void resetBasic() { scale = 0.0f; secondsSinceStart = 0.0f; executions = 0; @@ -316,7 +340,8 @@ public FunkinTween setManager(FunkinTweenManager newManager) { manager.getTweenPool().free(this); } manager = newManager; - manager.getTweenPool().obtain(); +// manager.getTweenPool().obtain(); + manager.getActiveTweens().add(this); } return this; } diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java index feff3fe..a6c20e5 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java @@ -7,13 +7,13 @@ public class FunkinTweenManager { /** Array where all current active tweens are stored. */ - protected final Array activeTweens = new Array<>(); + protected final Array activeTweens = new Array<>(FunkinTween.class); - /** A pool where all tweens are stored to preserve memory. */ + /** A pool where all unused tweens are stored to preserve memory. */ protected final Pool tweenPool = new Pool<>() { @Override protected FunkinTween newObject() { - return new FunkinTween(null, null, null); + return new FunkinTween(); } }; @@ -24,7 +24,7 @@ protected FunkinTween newObject() { */ public void update(float delta) { for (int i = activeTweens.size - 1; i >= 0; i--) { - FunkinTween tween = activeTweens.items[i]; + var tween = activeTweens.items[i]; if (tween == null) { continue; } @@ -44,6 +44,7 @@ public void update(float delta) { activeTweens.removeIndex(i); tweenPool.free(tween); } + case PERSIST -> {} // Do nothing, let it be. } } } diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenSettings.java b/core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenSettings.java index 2f2ccaf..554cd2a 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenSettings.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenSettings.java @@ -159,6 +159,11 @@ public FunkinTweenSettings setFramerate(float framerate) { return this; } + public FunkinTweenSettings setType(@NotNull FunkinTweenType type) { + this.type = type; + return this; + } + /** * A record containing basic info for a tween goal (aka a field to tween a numeric value to). * From b9c49679cb76dca44f233b6f2d7523bb691f7095 Mon Sep 17 00:00:00 2001 From: String Date: Sun, 18 Jan 2026 19:28:26 -0600 Subject: [PATCH 6/6] Fix many null errors with tweening --- .../funkin/tween/FunkinTween.java | 34 ++++++++++--------- .../funkin/util/FunkinRuntimeUtil.java | 4 +-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java index ca533d4..01db10b 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java @@ -64,7 +64,7 @@ public class FunkinTween implements Pool.Poolable { /** * Cache of the fields being tweened for faster access so they aren't accessed every time the - * {@code start()} method is called. + * {@link #start()} method is called. */ protected Field[] fieldsCache = null; @@ -76,7 +76,8 @@ protected FunkinTween() {} * *

Note that this does NOT add the tween to the global manager, it just assigns its main * values. That's it. If you wish to create a tween to automatically start working, you might want - * to see {@code tween()} + * to see {@link FunkinTween#tween(Object object, FunkinTweenSettings tweenSettings, + * FunkinTweenUpdateCallback updateCallback)}. * * @param object The object to tween values. * @param settings The settings that configure and determine how the tween should animate and last @@ -153,14 +154,26 @@ public void update(float delta) { // Update the object's fields based on the tween progress. var newValues = new HashMap(); - for (String field : tweenSettings.getGoalFields()) { + var goals = tweenSettings.getGoalFields(); + for (String field : goals) { FunkinTweenSettings.FunkinTweenGoal goal = tweenSettings.getGoal(field); + if (goal == null) { + continue; + } + if (initialValues.isEmpty()) { + continue; + } + if (!initialValues.containsKey(field)) { + continue; + } float startValue = initialValues.get(field); float goalValue = goal.value(); float newValue = startValue + (goalValue - startValue) * scale; newValues.put(field, newValue); } - updateCallback.update(newValues); + if (!newValues.isEmpty() && initialValues.keySet().containsAll(newValues.keySet())) { + updateCallback.update(newValues); + } // Check if the tween has finished. if (secondsSinceStart >= duration + delay) { @@ -289,19 +302,9 @@ public FunkinTween cancel(boolean remove) { @Override public void reset() { -// object = null; -// tweenSettings = null; -// updateCallback = null; + resetBasic(); manager = null; - scale = 0.0f; - secondsSinceStart = 0.0f; - executions = 0; - paused = false; - running = false; - finished = false; - backward = false; fieldsCache = null; - initialValues.clear(); } /** @@ -340,7 +343,6 @@ public FunkinTween setManager(FunkinTweenManager newManager) { manager.getTweenPool().free(this); } manager = newManager; -// manager.getTweenPool().obtain(); manager.getActiveTweens().add(this); } return this; diff --git a/core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java b/core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java index 84b8b04..f5b4572 100644 --- a/core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java +++ b/core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java @@ -41,10 +41,10 @@ public static String getExceptionLocation(Throwable exception) { */ public static String getFullExceptionMessage(Throwable exception) { if (exception == null) { - return "No Exception Provided"; + return "No exception provided."; } StringBuilder messageBuilder = new StringBuilder(); - messageBuilder.append("Exception: ").append(exception.toString()).append("\n"); + messageBuilder.append("Exception: ").append(exception).append("\n"); messageBuilder.append("Location: ").append(getExceptionLocation(exception)).append("\n"); messageBuilder.append("Stack Trace:\n"); for (StackTraceElement element : exception.getStackTrace()) {