diff --git a/android/build.gradle b/android/build.gradle index b8ef611..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" @@ -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 b160cbc..b162bc9 100644 --- a/assets/another_test.groovy +++ b/assets/another_test.groovy @@ -1,9 +1,8 @@ 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.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 +26,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().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 a3adf17..452a41d 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,18 +38,18 @@ 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().loadGraphic(Paths.sharedImageAsset('NOTE_hold_assets')) add(test) bgColor = new Color(0, 1, 0, 1) - Funkin.playMusic('songs/guns/Inst.ogg') + Funkin.playMusic('songs/darnell/Inst.ogg') } @Override 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 8290389..c91f5e5 100644 --- a/core/src/main/java/me/stringfromjava/funkin/Funkin.java +++ b/core/src/main/java/me/stringfromjava/funkin/Funkin.java @@ -1,18 +1,21 @@ package me.stringfromjava.funkin; import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Music; -import com.badlogic.gdx.audio.Sound; -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 com.badlogic.gdx.assets.AssetManager; +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.scenes.scene2d.Stage; +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.util.Constants; +import me.stringfromjava.funkin.util.signal.FunkinSignalData.*; +import org.jetbrains.annotations.NotNull; 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. @@ -22,31 +25,26 @@ */ 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; - /** - * 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; + + /** The global asset manager used to obtain preloaded assets. */ + private static AssetManager assetManager; + + /** The audio group for all sound effects, including the current music. */ + private static MAGroup soundsGroup; - /** The object where the current music being played is stored. */ - public static Music music = null; + /** The sound for playing music throughout the game. */ + public static MASound music; - /** The global volume multiplier for all sounds and music. */ - public static float masterVolume = 1.0f; + /** 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, + * 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; @@ -67,6 +65,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; } @@ -74,83 +81,264 @@ 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)); } /** - * 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: * - * @param path The path to play the sound from. - * @return The sound instance itself, as a {@link FunkinSound}. + *

{@code
+   * // Notice how it uses the Paths class provided by Funkin'.
+   * Funkin.playSound(Paths.external("your/path/here").path());
+   * }
+ * + * @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); - } + public static MASound playSound(String path) { + return playSound(path, 1, false, null, false); + } + + /** + * 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(), 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 MASound playSound(String path, float volume) { + return playSound(path, volume, false, null, false); + } + + /** + * 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(), 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 MASound playSound(String path, float volume, boolean looping) { + return playSound(path, volume, looping, null, false); + } + + /** + * 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 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 MASound playSound(String path, float volume, boolean looping, MAGroup group) { + return playSound(path, volume, looping, group, false); + } + + /** + * 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.
+   * // 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 MASound playSound( + @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; } /** - * Plays new music. (Duh.) + * 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: * - * @param path The path to play the music from. - * @return The music instance itself. + *

{@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 Music playMusic(String path) { - return playMusic(path, 1.0f, true); + public static void playMusic(String path, float volume) { + playMusic(path, volume, true, false); } /** - * Plays new music. (Duh.) + * 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: * - * @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.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 Music playMusic(String path, float volume) { - return playMusic(path, volume, true); + public static void playMusic(String path, float volume, boolean looping) { + playMusic(path, volume, looping, false); } /** - * Plays new music. (Duh.) + * 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: * - * @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. + *

{@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 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(); + public static void playMusic(String path, float volume, boolean looping, boolean external) { + Signals.preMusicPlayed.dispatch(new MusicPlayedSignalData(music)); + if (music != null) { + music.stop(); } - Funkin.music = music; + music = engine.createSound(path, (short) 0, soundsGroup, external); music.setVolume(volume); - music.setLooping(looped); + music.setLooping(looping); music.play(); - return music; + Signals.postMusicPlayed.dispatch(new MusicPlayedSignalData(music)); + } + + /** + * 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) { + return Gdx.input.isKeyPressed(key); + } + + public static boolean keyJustPressed(int key) { + return Gdx.input.isKeyJustPressed(key); } 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 +346,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 +354,49 @@ 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 void error(String tag, Object message, Throwable throwable) { + String msg = + (throwable != null) ? (message + " | Exception: " + throwable) : message.toString(); + outputLog(tag, msg, FunkinLogLevel.ERROR); } public static FunkinGame getGame() { return game; } - // ====================================== - // UTILITY FUNCTIONS, IGNORE BELOW - // ====================================== + public static Stage getStage() { + return game.stage; + } - 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; - }; + public static FunkinScreen getScreen() { + return screen; + } - 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); + public static MASound getMusic() { + return music; + } - System.out.println(timeAndDate + formattedTag + formattedMessage); + public static MiniAudio getAudioEngine() { + return engine; } + public static float getMasterVolume() { + return masterVolume; + } - 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 AssetManager getAssetManager() { + return assetManager; + } + + public static MAGroup getSoundsGroup() { + return soundsGroup; } /** @@ -227,7 +409,7 @@ private static String colorText( * 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<>(); @@ -236,16 +418,76 @@ 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 static final FunkinSignal preMusicPlayed = new FunkinSignal<>(); + public static final FunkinSignal postMusicPlayed = 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) { + 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() + "()"; + } - private Signals() {} + String color = + switch (level) { + case INFO -> Constants.AsciiCodes.WHITE; + case WARN -> Constants.AsciiCodes.YELLOW; + case ERROR -> Constants.AsciiCodes.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( + "[" + level + "] [" + tag + "] [" + file + "] [" + method + "] ", + 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..fa198c4 100644 --- a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java +++ b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java @@ -1,54 +1,122 @@ package me.stringfromjava.funkin; -import com.badlogic.gdx.Game; +import com.badlogic.gdx.Application; +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.Input; +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 me.stringfromjava.funkin.util.FunkinRuntimeUtil; -import java.util.Set; - -import static me.stringfromjava.funkin.Funkin.Signals.RenderSignalData; +import static me.stringfromjava.funkin.util.signal.FunkinSignalData.RenderSignalData; /** - * 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() { - configurePolyverse(); + configureCrashHandler(); // Crash handler for uncaught exceptions. + configureWindow(); // Window and viewport. + configurePolyverse(); // Polyverse scripting and modding system. + Funkin.setScreen(new InitScreen()); + } - 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.getScreen(); Funkin.Signals.preRender.dispatch(new RenderSignalData(delta)); - FunkinTween.globalManager.update(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); + 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(); + stage.act(delta); + stage.draw(); + + FunkinTween.getGlobalManager().update(delta); Polyverse.forAllScripts(script -> script.onRender(delta)); Funkin.Signals.postRender.dispatch(new 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.setMasterVolume(1); + Funkin.Signals.windowFocused.dispatch(); Funkin.info("Game window has regained focus."); } @@ -57,8 +125,8 @@ public void onWindowUnfocused() { if (isMinimized) { return; } - Funkin.masterVolume = 0.008f; - Funkin.music.setVolume(0.008f); + Funkin.setMasterVolume(0.008f); + Funkin.Signals.windowUnfocused.dispatch(); Funkin.info("Game window has lost focus."); } @@ -73,34 +141,42 @@ public void onWindowMinimized(boolean iconified) { if (!isMinimized) { return; } - Funkin.masterVolume = 0.0f; - Funkin.music.setVolume(0); + Funkin.setMasterVolume(0); + Funkin.Signals.windowMinimized.dispatch(); 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."); Funkin.Signals.preGameClose.dispatch(); - // Dispose of all sounds and the music (if there is any playing). - Funkin.info("Disposing music..."); - if (Funkin.music != null) { - Funkin.music.stop(); - Funkin.music.dispose(); - } + Funkin.info("Disposing the screen display..."); + Funkin.getScreen().hide(); + Funkin.getScreen().dispose(); + stage.dispose(); + batch.dispose(); + bgTexture.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 all sounds from sound group and music..."); + if (Funkin.getMusic() != null) { + Funkin.getMusic().dispose(); } + Funkin.getSoundsGroup().dispose(); + Funkin.getAudioEngine().dispose(); Funkin.info("Disposing and shutting down scripts..."); Polyverse.forAllScripts(Script::onDispose); @@ -112,6 +188,41 @@ 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" + 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/audio/FunkinSound.java b/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java deleted file mode 100644 index 77c199e..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java +++ /dev/null @@ -1,189 +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.system.Paths; - -/** - * 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 Sound thisSound; - 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) { - thisSound = Gdx.audio.newSound(path); - ID = thisSound.play(); - volume = 1.0f; - pitch = 1.0f; - pan = 0.0f; - looping = false; - isPaused = false; - thisSound.stop(); - Funkin.soundPool.put(ID, thisSound); - } - - @Override - public long play() { - return thisSound.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 Funkin.Signals.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)); - return thisSound.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 Funkin.Signals.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)); - return thisSound.loop(volume, pitch, pan); - } - - @Override - public void stop() { - isPaused = false; - thisSound.stop(); - } - - @Override - public void pause() { - isPaused = true; - thisSound.pause(); - } - - @Override - public void resume() { - isPaused = false; - thisSound.resume(); - } - - @Override - public void dispose() { - volume = 1.0f; - pitch = 1.0f; - pan = 0.0f; - looping = false; - isPaused = false; - thisSound.dispose(); - Funkin.soundPool.remove(ID); - } - - @Override - public void stop(long soundId) { - isPaused = false; - thisSound.stop(); - } - - @Override - public void pause(long soundId) { - isPaused = true; - thisSound.pause(soundId); - } - - @Override - public void resume(long soundId) { - isPaused = false; - thisSound.resume(); - } - - @Override - public void setLooping(long soundId, boolean looping) { - this.looping = looping; - thisSound.setLooping(soundId, looping); - } - - @Override - public void setPitch(long soundId, float pitch) { - this.pitch = pitch; - thisSound.setPitch(soundId, pitch); - } - - @Override - public void setVolume(long soundId, float volume) { - float v = volume * Funkin.masterVolume; - this.volume = v; - thisSound.setVolume(soundId, v); - } - - public void setPan(long soundId, float pan) { - setPan(soundId, pan, 1.0f); - } - - @Override - public void setPan(long soundId, float pan, float volume) { - thisSound.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/system/Paths.java b/core/src/main/java/me/stringfromjava/funkin/backend/Paths.java similarity index 50% 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..d406bab 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,9 +1,9 @@ -package me.stringfromjava.funkin.backend.system; +package me.stringfromjava.funkin.backend; 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,9 +14,17 @@ public static FileHandle shared(String path) { return asset(String.format("shared/%s", path)); } - public static FileHandle image(String path) { + public static FileHandle xmlAsset(String path) { + return asset(String.format("%s.xml", 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/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..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 @@ -1,41 +1,44 @@ package me.stringfromjava.funkin.game.menus; -import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.Sprite; +import games.rednblack.miniaudio.MASound; 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; +import me.stringfromjava.funkin.tween.settings.FunkinTweenType; public class TitleScreen extends FunkinScreen { - private Sprite logo; - private FunkinSound tickleFight; + private FunkinSprite logo; private FunkinTween tween; + private MASound tickleFight; @Override public void show() { super.show(); - logo = new Sprite(new Texture(Paths.image("stage_light"))); + 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) .addGoal("y", 40) .addGoal("rotation", 180) .setDuration(0.7f) - .setEase(FunkinEase::circInOut); - tween = FunkinTween.tween(logo, settings, (values) -> { + .setEase(FunkinEase::circInOut) + .setType(FunkinTweenType.PERSIST); + tween = FunkinTween.tween(logo, settings, values -> { logo.setX(values.get("x")); logo.setY(values.get("y")); logo.setRotation(values.get("rotation")); @@ -43,32 +46,36 @@ public void show() { } @Override - public void render(float delta) { - super.render(delta); + public void render(float elapsed) { + super.render(elapsed); - float speed = 200 * delta; - if (Gdx.input.isKeyPressed(Input.Keys.W)) { + float speed = 500 * elapsed; + 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.T)) { + if (Funkin.keyJustPressed(Input.Keys.SPACE)) { + logo.playAnimation("test", true); + } + + 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 { @@ -76,8 +83,8 @@ public void render(float delta) { } } - 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/screen/FunkinScreen.java b/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java new file mode 100644 index 0000000..69d390f --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/screen/FunkinScreen.java @@ -0,0 +1,56 @@ +package me.stringfromjava.funkin.graphics.screen; + +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.utils.SnapshotArray; +import me.stringfromjava.funkin.graphics.sprite.FunkinObject; + +/** + * 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 SnapshotArray members = new SnapshotArray<>(FunkinObject.class); + + @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..0811048 --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/graphics/sprite/FunkinSprite.java @@ -0,0 +1,367 @@ +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.math.Rectangle; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Pool; +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, 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; + + /** 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); + } + + 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()); + 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) { + 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; + } + } + + @Override + public void draw(Batch batch) { + 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); + + batch.draw( + currentFrame, + drawX, + drawY, + originX - currentFrame.offsetX, + originY + - (currentFrame.originalHeight - currentFrame.getRegionHeight() - currentFrame.offsetY), + currentFrame.getRegionWidth(), + currentFrame.getRegionHeight(), + getScaleX(), + getScaleY(), + getRotation()); + } + + public boolean isAnimationFinished() { + Animation anim = animations.get(currentAnim); + if (anim == null) return true; + 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; + } + + 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/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..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. */ @@ -92,7 +93,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); } } @@ -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()); - } - } - } + 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()); - } + 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 6928584..01db10b 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java +++ b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java @@ -1,21 +1,28 @@ 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; +import me.stringfromjava.funkin.util.Constants; 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; @@ -56,15 +63,29 @@ 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 + * {@link #start()} method is called. + */ + protected Field[] fieldsCache = null; + + /** Default constructor for pooling purposes. */ + protected FunkinTween() {} + + /** + * 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 {@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 * 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; @@ -87,6 +108,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(); @@ -122,33 +152,33 @@ 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()) { + 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) { scale = (backward) ? 0 : 1; finished = true; - switch (tweenSettings.getType()) { - case ONESHOT -> { - manager.activeTweens.remove(this); - } - } } else { if (postTick > preTick && onUpdate != null) { onUpdate.run(this); @@ -162,14 +192,37 @@ public void update(float delta) { * @return {@code this} tween. */ public FunkinTween start() { - reset(); + resetBasic(); running = true; finished = false; - // Ensure that the fields provided actually exist on the object and are floating point values. - var allFields = object.getClass().getDeclaredFields(); + 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()) { @@ -183,7 +236,8 @@ 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; @@ -223,31 +277,49 @@ 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() { + resetBasic(); + manager = null; + fieldsCache = null; + } + + /** + * 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; + paused = false; + running = false; + finished = false; + backward = false; initialValues.clear(); - return this; } public FunkinTweenSettings getTweenSettings() { @@ -259,10 +331,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.getActiveTweens().add(this); } return this; } @@ -272,8 +353,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..a6c20e5 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,60 @@ 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<>(FunkinTween.class); + /** A pool where all unused tweens are stored to preserve memory. */ + protected final Pool tweenPool = new Pool<>() { + @Override + protected FunkinTween newObject() { + return new FunkinTween(); + } + }; + + /** + * 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--) { + var 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); + } + case PERSIST -> {} // Do nothing, let it be. + } + } } } + + public Array getActiveTweens() { + return activeTweens; + } + + public Pool getTweenPool() { + return tweenPool; + } } 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). * 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/util/FunkinReflectUtil.java b/core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java new file mode 100644 index 0000000..2fae80d --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/util/FunkinReflectUtil.java @@ -0,0 +1,85 @@ +package me.stringfromjava.funkin.util; + +import me.stringfromjava.funkin.Funkin; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +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 + * reflection. + */ +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}. + * + * @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; + } + + /** + * 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. + * + * @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/util/FunkinRuntimeUtil.java b/core/src/main/java/me/stringfromjava/funkin/util/FunkinRuntimeUtil.java new file mode 100644 index 0000000..f5b4572 --- /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).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/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..8bb6fef --- /dev/null +++ b/core/src/main/java/me/stringfromjava/funkin/util/signal/FunkinSignalData.java @@ -0,0 +1,22 @@ +package me.stringfromjava.funkin.util.signal; + +import games.rednblack.miniaudio.MASound; +import me.stringfromjava.funkin.graphics.screen.FunkinScreen; + +/** + * 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 { + + public record RenderSignalData(float delta) {} + + public record ScreenSwitchSignalData(FunkinScreen screen) {} + + public record SoundPlayedSignalData(MASound sound) {} + + public record MusicPlayedSignalData(MASound music) {} + + private FunkinSignalData() {} +} + diff --git a/gradle.properties b/gradle.properties index 5f61b4a..d3f3f53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,10 +14,10 @@ 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 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()