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 extends Script> type : scripts.keySet()) {
+ executeScriptList(scripts.get(type), action);
+ }
+ }
+
+ private static void executeScriptList(
+ List extends Script> 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()