From 56ebfd18dcb0c3dd19d347fcb1f7b893dcf704fe Mon Sep 17 00:00:00 2001 From: String Date: Tue, 27 Jan 2026 21:21:08 -0600 Subject: [PATCH 1/9] Made a new function executing scripts, including its subtypes --- assets/oml.groovy | 15 +++++++++++++++ assets/test.groovy | 6 ++++++ .../me/stringdotjar/flixelgdx/FlixelGame.java | 5 +---- .../java/me/stringdotjar/funkin/FunkinGame.java | 9 ++++++++- .../me/stringdotjar/polyverse/Polyverse.java | 17 ++++++++++++++++- .../polyverse/script/type/AnotherType.java | 8 ++++++++ .../polyverse/script/type/SystemScript.java | 2 ++ 7 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 assets/oml.groovy create mode 100644 polyverse/src/main/java/me/stringdotjar/polyverse/script/type/AnotherType.java diff --git a/assets/oml.groovy b/assets/oml.groovy new file mode 100644 index 0000000..3bcee2c --- /dev/null +++ b/assets/oml.groovy @@ -0,0 +1,15 @@ +import me.stringdotjar.flixelgdx.Flixel +import me.stringdotjar.polyverse.script.type.AnotherType + +class Sob extends AnotherType { + + Sob() { + super('Sob') + } + + @Override + void onWindowFocused() { + super.onWindowFocused() + Flixel.info("i hate [insert particular ethnicity here]") + } +} diff --git a/assets/test.groovy b/assets/test.groovy index 0a291d2..fba075e 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -33,6 +33,12 @@ class TestScript extends SystemScript { super.onDispose() Flixel.info("TestClass", "Script has been disposed!") } + + @Override + void onWindowFocused() { + super.onWindowFocused() + Flixel.info("i like em young") + } } class TestScreen extends FlixelScreen { diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java index 36b8078..cfca1f4 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java @@ -99,6 +99,7 @@ public void create() { @Override public void resize(int width, int height) { + windowSize.set(width, height); viewport.update(width, height, true); } @@ -109,10 +110,6 @@ public void render() { Flixel.Signals.preRender.dispatch(new RenderSignalData(delta)); - if (Flixel.keyJustPressed(Input.Keys.F11)) { - toggleFullscreen(); - } - // Update and render the current screen that's active. ScreenUtils.clear(Color.BLACK); viewport.apply(); diff --git a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java index cfdce50..33ac973 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java @@ -1,5 +1,6 @@ package me.stringdotjar.funkin; +import com.badlogic.gdx.Input; import me.stringdotjar.flixelgdx.Flixel; import me.stringdotjar.flixelgdx.FlixelGame; import me.stringdotjar.flixelgdx.backend.FlixelPaths; @@ -28,6 +29,11 @@ public void create() { @Override public void render() { super.render(); + + if (Flixel.keyJustPressed(Input.Keys.F11)) { + toggleFullscreen(); + } + Polyverse.forAllScripts(script -> script.onRender(Flixel.getDelta())); } @@ -41,7 +47,7 @@ public void dispose() { public void onWindowFocused() { super.onWindowFocused(); Flixel.setMasterVolume(lastVolume); - Polyverse.forEachScript(SystemScript.class, SystemScript::onWindowFocused); + Polyverse.forEachScriptSuccessor(SystemScript.class, SystemScript::onWindowFocused); } @Override @@ -67,5 +73,6 @@ private void configurePolyverse() { Polyverse.registerScript(FlixelPaths.asset("test.groovy")); Polyverse.registerScript(FlixelPaths.asset("another_test.groovy")); + Polyverse.registerScript(FlixelPaths.asset("oml.groovy")); } } diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java b/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java index a8383af..297105d 100644 --- a/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java @@ -98,7 +98,7 @@ public static void registerScript(FileHandle handle) { } /** - * Executes an action for each script of a certain type, with error handling. + * Executes a function that pertains to a specific type for each script. * * @param type The class type of scripts to iterate over. * @param action The action to perform on each script. @@ -107,6 +107,21 @@ public static void forEachScript(Class type, Consumer a executeScriptList(getScripts(type), action); } + /** + * Executes a function for all scripts that are successors (subclasses or implementations) that are + * of a specific type. + * + * @param type The class type of scripts to iterate over, including its subtypes. + * @param action The function to perform on each script. + */ + public static void forEachScriptSuccessor(Class type, Consumer action) { + for (Class scriptType : scripts.keySet()) { + if (type.isAssignableFrom(scriptType)) { + executeScriptList(getScripts(scriptType), 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 diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/AnotherType.java b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/AnotherType.java new file mode 100644 index 0000000..6e3e4fd --- /dev/null +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/AnotherType.java @@ -0,0 +1,8 @@ +package me.stringdotjar.polyverse.script.type; + +public abstract class AnotherType extends SystemScript { + + public AnotherType(String id) { + super(id); + } +} diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java index 1024e0e..0dd14fc 100644 --- a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java @@ -14,4 +14,6 @@ public void onWindowUnfocused() {} /** Called when the game window is minimized. */ public void onWindowMinimized(boolean iconified) {} + + public void rawr() {} } From 582a40c380756ef4484407794b31505fc03a92d3 Mon Sep 17 00:00:00 2001 From: String Date: Thu, 29 Jan 2026 19:15:30 -0600 Subject: [PATCH 2/9] Add window alerts and update the crash handler --- PROJECT.md | 8 ++-- .../me/stringdotjar/flixelgdx/FlixelGame.java | 35 +++++++------- .../flixelgdx/util/FlixelRuntimeUtil.java | 46 +++++++++++++++++++ .../me/stringdotjar/funkin/FunkinGame.java | 4 +- 4 files changed, 71 insertions(+), 22 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 6681816..ebc1a2e 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -1,11 +1,11 @@ ## This is a [libGDX](https://libgdx.com/) project generated with [gdx-liftoff](https://github.com/libgdx/gdx-liftoff). -# Platforms +# Project Modules -FNF:JE is designed to run on multiple different platforms. Below are the different modules that hold the code for each one. +FNF:JE has many different submodules to organize the environment. - `funkin`: The core part of the game's code. This is where all the game logic is implemented. -- `flixelgdx`: Custom framework that bridges [HaxeFlixel](https://haxeflixel.com/) and is based on libGDX. This is where the HaxeFlixel-like API is implemented. +- `flixelgdx`: Custom framework that bridges [HaxeFlixel](https://haxeflixel.com/) to Java and is based on libGDX. This is where the HaxeFlixel-like API is implemented. - `polyverse`: Custom library for adding modding capabilities to the game. - `lwjgl3`: Primary desktop platform using [LWJGL3](https://www.lwjgl.org/). This is what launches the desktop versions of the game! - `android`: Android mobile platform. This requires the Android SDK, which can be downloaded and configured simply by running the universal [setup file](setup/android_setup.sh)! @@ -34,7 +34,7 @@ The Gradle wrapper was included, so you can run Gradle tasks using `gradlew.bat` - `clean`: removes `build` folders, which store compiled classes and built archives. - `eclipse`: generates Eclipse project data. - `idea`: generates IntelliJ project data. -- `funkin:exportModSDK`: Exports the game's API and its dependencies as `.jar` files to the assets folder. +- `funkin:exportModSDK`: exports the game's API and its dependencies as `.jar` files to the assets folder. - `lwjgl3:jar`: builds the game's runnable jar, which can be found at `lwjgl3/build/libs`. - `lwjgl3:run`: starts the desktop version of the game. - `test`: runs unit tests (if any). diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java index cfca1f4..8d1b17a 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java @@ -19,6 +19,8 @@ import me.stringdotjar.flixelgdx.util.FlixelConstants; import me.stringdotjar.flixelgdx.util.FlixelRuntimeUtil; +import java.awt.EventQueue; + import static me.stringdotjar.flixelgdx.signal.FlixelSignalData.RenderSignalData; /** @@ -217,6 +219,23 @@ public void dispose() { Flixel.Signals.postGameClose.dispatch(); } + /** + * Configurers Flixel's crash handler to safely catch uncaught exceptions and gracefully close the game. + */ + protected void configureCrashHandler() { + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + String logs = FlixelRuntimeUtil.getFullExceptionMessage(throwable); + String msg = "There was an uncaught exception on thread \"" + thread.getName() + "\"!\n" + logs; + Flixel.error(FlixelConstants.System.LOG_TAG, msg); + FlixelRuntimeUtil.showErrorAlert("Uncaught Exception", 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(); + } + }); + } + public String getTitle() { return title; } @@ -252,20 +271,4 @@ public Texture getBgTexture() { public boolean isMinimized() { return isMinimized; } - - /** - * Configurers Flixel's crash handler to safely catch uncaught exceptions and gracefully close the game. - */ - protected void configureCrashHandler() { - Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { - String logs = FlixelRuntimeUtil.getFullExceptionMessage(throwable); - String msg = "There was an uncaught exception on thread \"" + thread.getName() + "\"!\n" + logs; - Flixel.error(FlixelConstants.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(); - } - }); - } } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java index 4d8a48c..a321235 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java @@ -1,5 +1,8 @@ package me.stringdotjar.flixelgdx.util; +import javax.swing.JOptionPane; +import java.awt.EventQueue; + /** * Utility class for handling operation related to the runtime environment, including OS detection, * extracting runtime information, obtaining information from exceptions, and other related tasks. @@ -52,5 +55,48 @@ public static String getFullExceptionMessage(Throwable exception) { return messageBuilder.toString(); } + /** + * Displays a new general info alert window with the given title and message. + * + * @param title The title of the alert window. + * @param message The message content to display in the alert window. + */ + public static void showInfoAlert(String title, Object message) { + showAlert(title, message, JOptionPane.INFORMATION_MESSAGE); + } + + /** + * Displays a new warning alert window with the given title and message. + * + * @param title The title of the alert window. + * @param message The message content to display in the alert window. + */ + public static void showWarningAlert(String title, Object message) { + showAlert(title, message, JOptionPane.WARNING_MESSAGE); + } + + /** + * Displays a new error alert window with the given title and message. + * + * @param title The title of the alert window. + * @param message The message content to display in the alert window. + */ + public static void showErrorAlert(String title, Object message) { + showAlert(title, message, JOptionPane.ERROR_MESSAGE); + } + + /** + * Displays a new alert window with the given title, message, and type. + * + * @param title The title of the alert window. + * @param message The message content to display in the alert window. + * @param type The type of alert (e.g., JOptionPane.INFORMATION_MESSAGE). + */ + public static void showAlert(String title, Object message, int type) { + EventQueue.invokeLater(() -> { + JOptionPane.showMessageDialog(null, message, title, type); + }); + } + private FlixelRuntimeUtil() {} } diff --git a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java index 33ac973..9f424ae 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java @@ -55,7 +55,7 @@ public void onWindowUnfocused() { super.onWindowUnfocused(); lastVolume = Flixel.getMasterVolume(); Flixel.setMasterVolume(0.008f); - Polyverse.forEachScript(SystemScript.class, SystemScript::onWindowUnfocused); + Polyverse.forEachScriptSuccessor(SystemScript.class, SystemScript::onWindowUnfocused); } @Override @@ -63,7 +63,7 @@ public void onWindowMinimized(boolean iconified) { super.onWindowMinimized(iconified); lastVolume = Flixel.getMasterVolume(); Flixel.setMasterVolume(0); - Polyverse.forEachScript(SystemScript.class, script -> script.onWindowMinimized(iconified)); + Polyverse.forEachScriptSuccessor(SystemScript.class, script -> script.onWindowMinimized(iconified)); } private void configurePolyverse() { From 34809106c59ed0b5dc87f9ae6de0a4720caf4354 Mon Sep 17 00:00:00 2001 From: String Date: Sat, 31 Jan 2026 07:13:36 -0600 Subject: [PATCH 3/9] Add groups and the ability to add many viewports --- assets/test.groovy | 10 +- .../me/stringdotjar/flixelgdx/Flixel.java | 27 ++- .../me/stringdotjar/flixelgdx/FlixelGame.java | 168 ++++++++++++------ .../graphics/{sprite => }/FlixelObject.java | 4 +- .../flixelgdx/graphics/FlixelScreen.java | 96 ++++++++++ .../flixelgdx/graphics/FlixelViewport.java | 22 +++ .../graphics/screen/FlixelScreen.java | 56 ------ .../graphics/sprite/FlixelSprite.java | 55 +++--- .../flixelgdx/group/FlixelGroup.java | 60 +++++++ .../flixelgdx/group/FlixelGroupable.java | 13 ++ .../flixelgdx/input/FlixelKey.java | 8 + .../flixelgdx/signal/FlixelSignalData.java | 2 +- .../flixelgdx/util/FlixelConstants.java | 2 + .../flixelgdx/util/FlixelReflectUtil.java | 4 +- .../me/stringdotjar/funkin/FunkinGame.java | 3 +- .../me/stringdotjar/funkin/InitScreen.java | 6 +- .../funkin/menus/TitleScreen.java | 30 ++-- 17 files changed, 398 insertions(+), 168 deletions(-) rename flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/{sprite => }/FlixelObject.java (67%) create mode 100644 flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelScreen.java create mode 100644 flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelViewport.java delete mode 100644 flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/screen/FlixelScreen.java create mode 100644 flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java create mode 100644 flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java create mode 100644 flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java diff --git a/assets/test.groovy b/assets/test.groovy index fba075e..70f8420 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -2,7 +2,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import me.stringdotjar.flixelgdx.Flixel -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen +import me.stringdotjar.flixelgdx.graphics.FlixelScreen import me.stringdotjar.flixelgdx.backend.FlixelPaths import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite import me.stringdotjar.polyverse.script.type.SystemScript @@ -46,8 +46,8 @@ class TestScreen extends FlixelScreen { private FlixelSprite test @Override - void show() { - super.show() + void create() { + super.create() test = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset('NOTE_hold_assets')) add(test) @@ -58,7 +58,7 @@ class TestScreen extends FlixelScreen { } @Override - void render(float delta) { - super.render(delta) + void update(float delta) { + super.update(delta) } } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java index 615053f..e78ff25 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java @@ -3,12 +3,14 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.assets.AssetManager; import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.math.Vector2; 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.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelViewport; import me.stringdotjar.flixelgdx.util.FlixelConstants; import me.stringdotjar.flixelgdx.signal.FlixelSignal; import me.stringdotjar.flixelgdx.signal.FlixelSignalData.MusicPlayedSignalData; @@ -88,12 +90,13 @@ public static void setScreen(FlixelScreen newScreen) { if (newScreen == null) { throw new IllegalArgumentException("Screen cannot be null!"); } - if (Flixel.screen != null) { - Flixel.screen.hide(); - Flixel.screen.dispose(); + if (screen != null) { + screen.hide(); + screen.dispose(); } - Flixel.screen = newScreen; - Flixel.screen.show(); + game.resetViewports(); + screen = newScreen; + screen.create(); Signals.postScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); } @@ -377,6 +380,10 @@ public static MASound getMusic() { return music; } + public static Vector2 getWindowSize() { + return game.windowSize; + } + public static MiniAudio getAudioEngine() { return engine; } @@ -397,6 +404,14 @@ public static float getDelta() { return Gdx.graphics.getDeltaTime(); } + public static FlixelViewport getViewport() { + return game.getViewport(); + } + + public static boolean isFullscreen() { + return Gdx.graphics.isFullscreen(); + } + /** * Contains all the global events that get dispatched when something happens in the game. * diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java index 8d1b17a..faa04b5 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java @@ -3,24 +3,21 @@ import com.badlogic.gdx.Application; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Input; import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.utils.ScreenUtils; -import com.badlogic.gdx.utils.viewport.FitViewport; -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; -import me.stringdotjar.flixelgdx.graphics.sprite.FlixelObject; +import com.badlogic.gdx.utils.SnapshotArray; +import me.stringdotjar.flixelgdx.graphics.FlixelObject; +import me.stringdotjar.flixelgdx.graphics.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelViewport; +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; import me.stringdotjar.flixelgdx.tween.FlixelTween; -import me.stringdotjar.flixelgdx.util.FlixelConstants; import me.stringdotjar.flixelgdx.util.FlixelRuntimeUtil; -import java.awt.EventQueue; - import static me.stringdotjar.flixelgdx.signal.FlixelSignalData.RenderSignalData; /** @@ -41,26 +38,23 @@ public abstract class FlixelGame implements ApplicationListener { protected FlixelScreen initialScreen; /** Is the game's window currently focused? */ - protected boolean isFocused = true; + private boolean isFocused = true; /** Is the game's window currently minimized? */ - protected boolean isMinimized = false; + private 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; + /** Where all the global viewports are stored. */ + protected SnapshotArray viewports; + /** * Creates a new game instance with the specified title, window width/height, and initial screen. This configures * the game's core parts, such as the viewport, stage, etc. @@ -81,13 +75,10 @@ public void create() { configureCrashHandler(); // This should ALWAYS be called first no matter what! batch = new SpriteBatch(); - viewport = new FitViewport(windowSize.x, windowSize.y); - viewport.apply(); - - camera = new OrthographicCamera(); - camera.setToOrtho(false, windowSize.x, windowSize.y); + viewports = new SnapshotArray<>(FlixelViewport.class); + viewports.add(new FlixelViewport((int) windowSize.x, (int) windowSize.y)); - stage = new Stage(viewport, batch); + stage = new Stage(getViewport(), batch); Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888); pixmap.setColor(Color.WHITE); @@ -101,8 +92,14 @@ public void create() { @Override public void resize(int width, int height) { - windowSize.set(width, height); - viewport.update(width, height, true); + FlixelViewport[] viewportsArray = viewports.begin(); + for (int i = 0; i < viewports.size; i++) { + FlixelViewport viewport = viewportsArray[i]; + if (viewport != null) { + viewport.update(width, height, true); + } + } + viewports.end(); } @Override @@ -112,34 +109,60 @@ public void render() { Flixel.Signals.preRender.dispatch(new RenderSignalData(delta)); + stage.act(delta); + FlixelTween.getGlobalManager().update(delta); + // Update and render the current screen that's active. ScreenUtils.clear(Color.BLACK); - viewport.apply(); - batch.setProjectionMatrix(camera.combined); - batch.begin(); if (screen != null) { - 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 (FlixelObject object : members) { - if (object == null) { + screen.update(delta); + + // Loop through all viewports and draw the current screen's members onto their set viewports. + FlixelViewport[] vps = viewports.begin(); + SnapshotArray members = screen.getMembers(); + for (int i = 0; i < viewports.size; i++) { + FlixelViewport viewport = vps[i]; + if (viewport == null) { continue; } - object.update(delta); - object.draw(batch); + + viewport.apply(); + var camera = viewport.getCamera(); + camera.update(); + batch.setProjectionMatrix(camera.combined); + batch.begin(); + + // Draw the background color first. + batch.setColor(screen.getBgColor()); + batch.draw(bgTexture, 0, 0, getViewport().getWorldWidth(), getViewport().getWorldHeight()); + batch.setColor(Color.WHITE); // Set the batch color back to white so display objects aren't affected. + // Draw all the current screens members. + FlixelObject[] mbrs = members.begin(); + for (int j = 0; j < members.size; j++) { + FlixelObject member = mbrs[j]; + if (member == null) { + continue; + } + member.update(delta); + if (member instanceof FlixelSprite s) { + // Check if the current sprite is in the visible part of the viewport. + // If it cannot be seen, then we don't draw the sprite to save performance. + if (camera.frustum.boundsInFrustum(s.getX(), s.getY(), 0, s.getWidth(), s.getHeight(), 0)) { + s.draw(batch); + } + } else { + member.draw(batch); + } + } + batch.end(); + members.end(); } - screen.members.end(); + viewports.end(); + screen.draw(batch); } - - batch.end(); - stage.act(delta); stage.draw(); - FlixelTween.getGlobalManager().update(delta); - Flixel.Signals.postRender.dispatch(new RenderSignalData(delta)); } @@ -170,7 +193,7 @@ public void onWindowUnfocused() { * Called when the user minimizes the game's window. * * @param iconified Whether the window is iconified (minimized) or not. This parameter is provided - * for compatibility with the window listener in the LWJGL3 (desktop) launcher. + * for compatibility with the window listener in the LWJGL3 (desktop) launcher. */ public void onWindowMinimized(boolean iconified) { isMinimized = iconified; @@ -182,18 +205,29 @@ public void onWindowMinimized(boolean iconified) { Flixel.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((int) windowSize.x, (int) windowSize.y); - Flixel.info("Exiting fullscreen mode."); - } else { + /** + * Sets fullscreen mode for the game's window. + * + * @param enabled If the game's window should be in fullscreen mode. + */ + public void setFullscreen(boolean enabled) { + if (enabled) { + if (!Flixel.isFullscreen()) { + windowSize.set(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + } Gdx.graphics.setFullscreenMode(Gdx.graphics.getDisplayMode()); - Flixel.info("Entering fullscreen mode."); + Flixel.info("Entered fullscreen mode."); + } else { + Gdx.graphics.setWindowedMode((int) windowSize.x, (int) windowSize.y); + Flixel.info("Exited fullscreen mode."); } } + /** Toggles fullscreen mode on or off, depending on the current state. */ + public void toggleFullscreen() { + setFullscreen(!Flixel.isFullscreen()); + } + @Override public void dispose() { Flixel.warn("SHUTTING DOWN GAME AND DISPOSING ALL RESOURCES."); @@ -214,8 +248,6 @@ public void dispose() { Flixel.getSoundsGroup().dispose(); Flixel.getAudioEngine().dispose(); - Flixel.info("Disposing and shutting down scripts..."); - Flixel.Signals.postGameClose.dispatch(); } @@ -226,7 +258,7 @@ protected void configureCrashHandler() { Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { String logs = FlixelRuntimeUtil.getFullExceptionMessage(throwable); String msg = "There was an uncaught exception on thread \"" + thread.getName() + "\"!\n" + logs; - Flixel.error(FlixelConstants.System.LOG_TAG, msg); + Flixel.error(msg); FlixelRuntimeUtil.showErrorAlert("Uncaught Exception", msg); dispose(); // Only use Gdx.app.exit() on non-iOS platforms to avoid App Store guideline violations! @@ -252,12 +284,26 @@ public Stage getStage() { return stage; } - public FitViewport getViewport() { - return viewport; + public FlixelViewport getViewport() { + Vector2 windowSize = Flixel.getWindowSize(); + if (viewports == null) { + Flixel.warn("Viewport list is null. Resigning with fresh array..."); + viewports = new SnapshotArray<>(); + } + if (viewports.isEmpty()) { + Flixel.warn("Viewport list is empty. Adding new fresh default viewport..."); + viewports.add(new FlixelViewport((int) windowSize.x, (int) windowSize.y)); + } + return viewports.first(); } - public OrthographicCamera getCamera() { - return camera; + public void resetViewports() { + viewports.clear(); + viewports.add(new FlixelViewport((int) windowSize.x, (int) windowSize.y)); + } + + public SnapshotArray getViewports() { + return viewports; } public SpriteBatch getBatch() { @@ -271,4 +317,10 @@ public Texture getBgTexture() { public boolean isMinimized() { return isMinimized; } + + public void setWindowSize(Vector2 windowSize) { + this.windowSize = windowSize; + Gdx.graphics.setWindowedMode((int) windowSize.x, (int) windowSize.y); + Flixel.info("Set window size. (WIDTH=" + windowSize.x + ", HEIGHT=" + windowSize.y + ")"); + } } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java similarity index 67% rename from flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java index 6ebd5c2..489ba9d 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java @@ -1,10 +1,10 @@ -package me.stringdotjar.flixelgdx.graphics.sprite; +package me.stringdotjar.flixelgdx.graphics; import com.badlogic.gdx.graphics.g2d.Batch; -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; /** An interface which allows any class that implements it to be added to a {@link FlixelScreen}. */ public interface FlixelObject { void update(float delta); void draw(Batch batch); + void destroy(); } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelScreen.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelScreen.java new file mode 100644 index 0000000..6957ec6 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelScreen.java @@ -0,0 +1,96 @@ +package me.stringdotjar.flixelgdx.graphics; + +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.utils.SnapshotArray; +import me.stringdotjar.flixelgdx.group.FlixelGroup; + +/** + * Base class for creating a better screen display with more functionality than the default {@link + * com.badlogic.gdx.Screen} interface. + */ +public abstract class FlixelScreen extends FlixelGroup implements Screen { + + /** Should {@code this} screen update its logic even when a substate is currently opened? */ + public boolean persistentUpdate = false; + + /** Should {@code this} screen draw its members even when a substate is currently opened? */ + public boolean persistentDraw = true; + + /** The background color of {@code this} current screen. */ + protected Color bgColor; + + public FlixelScreen() { + members = new SnapshotArray<>(FlixelObject.class); + } + + @Override + public final void show() {} + + @Override + public final void render(float delta) {} + + /** + * Called when the screen is first created. This is where you want to assign your + * sprites and setup everything your screen uses! + */ + public void create() {} + + /** + * Updates the logic of {@code this} screen. + * + * @param delta The amount of time that occurred since the last frame. + */ + public void update(float delta) { + if (!persistentUpdate) { + return; + } + + forEachMember(object -> { + object.update(delta); + }); + } + + /** + * Draws its members onto the screen. + * + * @param batch The batch that's used to draw the + */ + public void draw(Batch batch) { + if (!persistentDraw) { + return; + } + } + + @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(FlixelObject object) { + if (object != null) { + members.add(object); + } + } + + public Color getBgColor() { + return (bgColor != null) ? bgColor : Color.BLACK; + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelViewport.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelViewport.java new file mode 100644 index 0000000..be9b225 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelViewport.java @@ -0,0 +1,22 @@ +package me.stringdotjar.flixelgdx.graphics; + +import com.badlogic.gdx.graphics.Camera; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.utils.viewport.FitViewport; + +import static me.stringdotjar.flixelgdx.Flixel.getWindowSize; + +public class FlixelViewport extends FitViewport { + + public FlixelViewport() { + this((int)getWindowSize().x, (int)getWindowSize().y, new OrthographicCamera(getWindowSize().x, getWindowSize().y)); + } + + public FlixelViewport(int width, int height) { + this(width, height, new OrthographicCamera(width, height)); + } + + public FlixelViewport(int width, int height, Camera camera) { + super(width, height, camera); + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/screen/FlixelScreen.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/screen/FlixelScreen.java deleted file mode 100644 index 8f00693..0000000 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/screen/FlixelScreen.java +++ /dev/null @@ -1,56 +0,0 @@ -package me.stringdotjar.flixelgdx.graphics.screen; - -import com.badlogic.gdx.Screen; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.utils.SnapshotArray; -import me.stringdotjar.flixelgdx.graphics.sprite.FlixelObject; - -/** - * Base class for creating a better screen display with more functionality than the default {@link - * com.badlogic.gdx.Screen} interface. - */ -public abstract class FlixelScreen 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<>(FlixelObject.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(FlixelObject object) { - if (object != null) { - members.add(object); - } - } - - public Color getBgColor() { - return (bgColor != null) ? bgColor : Color.BLACK; - } -} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java index ca82924..30b5946 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java @@ -5,13 +5,15 @@ 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.SpriteBatch; 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 me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.flixelgdx.graphics.FlixelObject; +import me.stringdotjar.flixelgdx.graphics.FlixelViewport; import java.util.Comparator; import java.util.HashMap; @@ -30,6 +32,9 @@ public class FlixelSprite extends Sprite implements FlixelObject, Pool.Poolable /** The hitbox used for collision detection and angling. */ protected Rectangle hitbox; + /** The viewports that {@code this} sprite is projected onto. */ + protected FlixelViewport[] viewports; + /** The atlas regions used in this sprite (used for animations). */ protected Array atlasRegions; @@ -43,10 +48,10 @@ public class FlixelSprite extends Sprite implements FlixelObject, Pool.Poolable protected float stateTime = 0; /** The name of the current animation playing. */ - protected String currentAnim = ""; + private String currentAnim = ""; /** Is {@code this} sprites current animation looping indefinitely? */ - protected boolean looping = true; + private boolean looping = true; /** * Where all the image frames are stored. This is also where the main image is stored when using @@ -56,6 +61,7 @@ public class FlixelSprite extends Sprite implements FlixelObject, Pool.Poolable public FlixelSprite() { animations = new HashMap<>(); + viewports = new FlixelViewport[]{Flixel.getViewport()}; } /** @@ -86,7 +92,7 @@ public FlixelSprite loadGraphic(FileHandle path) { /** * 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 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. */ @@ -98,8 +104,8 @@ public FlixelSprite loadGraphic(FileHandle path, int frameWidth) { /** * 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 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. */ @@ -120,9 +126,9 @@ public FlixelSprite loadGraphic(Texture texture, int frameWidth, int frameHeight * 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. + * different frames from. * @param xmlFile The path to the {@code .xml} file which contains the data for each subtexture of - * the sparrow atlas. + * the sparrow atlas. * @return {@code this} sprite for chaining. */ public FlixelSprite loadSparrowFrames(FileHandle texture, FileHandle xmlFile) { @@ -133,9 +139,9 @@ public FlixelSprite loadSparrowFrames(FileHandle texture, FileHandle 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. + * from. * @param xmlFile The {@link XmlReader.Element} data which contains the data for each subtexture - * of the sparrow atlas. + * of the sparrow atlas. * @return {@code this} sprite for chaining. */ public FlixelSprite loadSparrowFrames(Texture texture, XmlReader.Element xmlFile) { @@ -189,10 +195,10 @@ public FlixelSprite loadSparrowFrames(Texture texture, XmlReader.Element xmlFile /** * 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 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? + * @param loop Should the new animation loop indefinitely? */ public void addAnimationByPrefix(String name, String prefix, int frameRate, boolean loop) { Array frames = new Array<>(); @@ -209,17 +215,17 @@ public void addAnimationByPrefix(String name, String prefix, int frameRate, bool animations.put( name, - new Animation<>( - 1f / frameRate, frames, loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL)); + 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 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) { @@ -258,8 +264,8 @@ public void playAnimation(String name, boolean loop) { /** * 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 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) { @@ -302,6 +308,11 @@ public void draw(Batch batch) { getRotation()); } + @Override + public void destroy() { + reset(); + } + public boolean isAnimationFinished() { Animation anim = animations.get(currentAnim); if (anim == null) return true; @@ -349,6 +360,10 @@ public Array getAtlasRegions() { return atlasRegions; } + public FlixelViewport[] getViewports() { + return viewports; + } + public TextureAtlas.AtlasRegion getCurrentFrame() { return currentFrame; } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java new file mode 100644 index 0000000..40eadec --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java @@ -0,0 +1,60 @@ +package me.stringdotjar.flixelgdx.group; + +import com.badlogic.gdx.utils.SnapshotArray; + +import java.util.function.Consumer; + +/** + * Base class for creating groups with a list of members inside of it. + */ +public abstract class FlixelGroup implements FlixelGroupable { + + /** + * The list of members that {@code this} group contains. + */ + protected SnapshotArray members; + + public FlixelGroup() { + members = new SnapshotArray<>(); + } + + @Override + public void add(T member) { + members.add(member); + } + + @Override + public void remove(T member) { + members.removeValue(member, true); + } + + @Override + public void clear() { + members.clear(); + } + + @Override + public void forEachMember(Consumer callback) { + for (T member : members.begin()) { + if (member == null) { + continue; + } + callback.accept(member); + } + } + + public void forEachMemberType(Class type, Consumer callback) { + T[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + T member = items[i]; + if (type.isInstance(member)) { + callback.accept(type.cast(member)); + } + } + members.end(); + } + + public SnapshotArray getMembers() { + return members; + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java new file mode 100644 index 0000000..c464452 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java @@ -0,0 +1,13 @@ +package me.stringdotjar.flixelgdx.group; + +import java.util.function.Consumer; + +/** + * Interface for creating new groups with members inside of them. + */ +public interface FlixelGroupable { + void add(T member); + void remove(T member); + void clear(); + void forEachMember(Consumer callback); +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java new file mode 100644 index 0000000..963a94b --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java @@ -0,0 +1,8 @@ +package me.stringdotjar.flixelgdx.input; + +import com.badlogic.gdx.Input; + +/** + * A simple extension of {@link Input.Keys} to simplify accessing key constants. + */ +public class FlixelKey extends Input.Keys {} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java index 84f010b..0da3e0d 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java @@ -1,7 +1,7 @@ package me.stringdotjar.flixelgdx.signal; import games.rednblack.miniaudio.MASound; -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelScreen; import me.stringdotjar.flixelgdx.Flixel; /** diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java index 149dda7..3d4a1df 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java @@ -42,4 +42,6 @@ public static final class AsciiCodes { private AsciiCodes() {} } + + private FlixelConstants() {} } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java index 2c0ec5c..9fdc8b3 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java @@ -13,7 +13,9 @@ * Backend utility class for obtaining and manipulating fields on objects through the usage of Java * reflection. */ -public class FlixelReflectUtil { +public final class FlixelReflectUtil { + + private FlixelReflectUtil() {} /** * Checks if a field exists on a given object. diff --git a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java index 9f424ae..62e2258 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java @@ -4,7 +4,7 @@ import me.stringdotjar.flixelgdx.Flixel; import me.stringdotjar.flixelgdx.FlixelGame; import me.stringdotjar.flixelgdx.backend.FlixelPaths; -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelScreen; import me.stringdotjar.polyverse.Polyverse; import me.stringdotjar.polyverse.script.type.Script; import me.stringdotjar.polyverse.script.type.SystemScript; @@ -40,6 +40,7 @@ public void render() { @Override public void dispose() { super.dispose(); + Flixel.info("Funkin", "Disposing scripts..."); Polyverse.forAllScripts(Script::onDispose); } diff --git a/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java b/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java index e90d619..a9ccf3f 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java @@ -1,14 +1,14 @@ package me.stringdotjar.funkin; -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelScreen; import me.stringdotjar.flixelgdx.Flixel; import me.stringdotjar.funkin.menus.TitleScreen; public class InitScreen extends FlixelScreen { @Override - public void show() { - super.show(); + public void create() { + super.create(); Flixel.setScreen(new TitleScreen()); } } diff --git a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java index 99c78e2..79fab0a 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java @@ -1,11 +1,11 @@ package me.stringdotjar.funkin.menus; -import com.badlogic.gdx.Input; import games.rednblack.miniaudio.MASound; import me.stringdotjar.flixelgdx.Flixel; import me.stringdotjar.flixelgdx.backend.FlixelPaths; -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelScreen; import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; +import me.stringdotjar.flixelgdx.input.FlixelKey; import me.stringdotjar.flixelgdx.tween.FlixelEase; import me.stringdotjar.flixelgdx.tween.FlixelTween; import me.stringdotjar.flixelgdx.tween.settings.FlixelTweenSettings; @@ -19,8 +19,8 @@ public class TitleScreen extends FlixelScreen { private MASound tickleFight; @Override - public void show() { - super.show(); + public void create() { + super.create(); var t = FlixelPaths.sharedImageAsset("noteStrumline"); var xml = FlixelPaths.shared("images/noteStrumline.xml"); @@ -46,36 +46,36 @@ public void show() { } @Override - public void render(float elapsed) { - super.render(elapsed); + public void update(float elapsed) { + super.update(elapsed); float speed = 500 * elapsed; - if (Flixel.keyPressed(Input.Keys.W)) { + if (Flixel.keyPressed(FlixelKey.W)) { logo.changeY(speed); } - if (Flixel.keyPressed(Input.Keys.S)) { + if (Flixel.keyPressed(FlixelKey.S)) { logo.changeY(-speed); } - if (Flixel.keyPressed(Input.Keys.A)) { + if (Flixel.keyPressed(FlixelKey.A)) { logo.changeX(-speed); } - if (Flixel.keyPressed(Input.Keys.D)) { + if (Flixel.keyPressed(FlixelKey.D)) { logo.changeX(speed); } - if (Flixel.keyJustPressed(Input.Keys.SPACE)) { + if (Flixel.keyJustPressed(FlixelKey.SPACE)) { logo.playAnimation("test", true); } - if (Flixel.keyJustPressed(Input.Keys.T)) { + if (Flixel.keyJustPressed(FlixelKey.T)) { tween.start(); } - if (Flixel.keyJustPressed(Input.Keys.R)) { + if (Flixel.keyJustPressed(FlixelKey.R)) { tween.reset(); } - if (Flixel.keyJustPressed(Input.Keys.Y)) { + if (Flixel.keyJustPressed(FlixelKey.Y)) { if (tween.paused) { tween.resume(); } else { @@ -83,7 +83,7 @@ public void render(float elapsed) { } } - if (Flixel.keyJustPressed(Input.Keys.Z)) { + if (Flixel.keyJustPressed(FlixelKey.Z)) { tickleFight.play(); } } From 8030b9d993d9586ead8123cb8f636dbcbd805cef Mon Sep 17 00:00:00 2001 From: String Date: Thu, 5 Feb 2026 05:54:11 -0600 Subject: [PATCH 4/9] Added error popups for scripts and fixed popup alerts --- assets/test.groovy | 8 ++++-- .../me/stringdotjar/flixelgdx/Flixel.java | 3 -- .../flixelgdx/util/FlixelRuntimeUtil.java | 18 ++++++++++-- .../me/stringdotjar/polyverse/Polyverse.java | 28 ++++++++++++------- .../polyverse/script/type/Script.java | 2 +- 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/assets/test.groovy b/assets/test.groovy index 70f8420..ea4b33b 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -49,12 +49,14 @@ class TestScreen extends FlixelScreen { void create() { super.create() - test = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset('NOTE_hold_assets')) - add(test) - bgColor = new Color(0, 1, 0, 1) Flixel.playMusic('songs/darnell/Inst.ogg') + +// test.changeX(-30) + + test = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset('NOTE_hold_assets')) + add(test) } @Override diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java index e78ff25..bb89a0e 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java @@ -317,9 +317,6 @@ public static void playMusic(String path, float volume, boolean looping, boolean /** * 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 - * Flixel.getAudioEngine().setMasterVolume(float)}). - * * @param volume The new master volume to set. */ public static void setMasterVolume(float volume) { diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java index a321235..7f35c86 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java @@ -1,7 +1,10 @@ package me.stringdotjar.flixelgdx.util; +import me.stringdotjar.flixelgdx.Flixel; + import javax.swing.JOptionPane; import java.awt.EventQueue; +import java.lang.reflect.InvocationTargetException; /** * Utility class for handling operation related to the runtime environment, including OS detection, @@ -93,9 +96,18 @@ public static void showErrorAlert(String title, Object message) { * @param type The type of alert (e.g., JOptionPane.INFORMATION_MESSAGE). */ public static void showAlert(String title, Object message, int type) { - EventQueue.invokeLater(() -> { - JOptionPane.showMessageDialog(null, message, title, type); - }); + String msg = message != null ? message.toString() : "null"; + if (EventQueue.isDispatchThread()) { + JOptionPane.showMessageDialog(null, msg, title, type); + } else { + try { + EventQueue.invokeAndWait(() -> { + JOptionPane.showMessageDialog(null, msg, title, type); + }); + } catch (InterruptedException | InvocationTargetException e) { + Flixel.error(FlixelConstants.System.LOG_TAG, "Failed to show alert message.", e); + } + } } private FlixelRuntimeUtil() {} diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java b/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java index 297105d..8e7942d 100644 --- a/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java @@ -3,6 +3,7 @@ import com.badlogic.gdx.files.FileHandle; import groovy.lang.GroovyClassLoader; import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.flixelgdx.util.FlixelRuntimeUtil; import me.stringdotjar.polyverse.script.type.Script; import java.util.ArrayList; @@ -81,10 +82,7 @@ public static void registerScript(FileHandle handle) { var typeScripts = scripts.get(mostSpecificType); if (!typeScripts.contains(script)) { typeScripts.add(script); - Flixel.info( - "Polyverse", - "Registered Polyverse script \"" - + script.getClass().getSimpleName() + Flixel.info("Polyverse", "Registered Polyverse script \"" + script.getClass().getSimpleName() + "\" of script type \"" + mostSpecificType.getSimpleName() + "\"."); @@ -93,7 +91,12 @@ public static void registerScript(FileHandle handle) { script.onCreate(); } } catch (Exception e) { - Flixel.error("Polyverse", "Failed to load script: " + handle.path(), e); + StringBuilder errorWindowMessage = new StringBuilder(); + errorWindowMessage.append("There was an uncaught exception for a script during compilation.\n"); + errorWindowMessage.append("Location: ").append(handle.path()).append("\n"); + errorWindowMessage.append("Exception: ").append(e); + Flixel.error("Polyverse", "Failed to compile script: " + handle.path(), e); + FlixelRuntimeUtil.showErrorAlert("Polyverse Script Exception", errorWindowMessage); } } @@ -136,10 +139,10 @@ public static void forAllScripts(Consumer 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. + 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. Plus, it's also to prevent stuttering since we are using + // ArrayLists for storing scripts. for (int i = 0; i < scriptList.size(); i++) { @SuppressWarnings("unchecked") T script = (T) scriptList.get(i); @@ -147,7 +150,12 @@ private static void executeScriptList( try { action.accept(script); } catch (Exception e) { - Flixel.error("Polyverse", "Error in " + script.getClass().getSimpleName(), e); + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append("There was an uncaught exception for a script when executing it.\n"); + errorMsg.append("Script ID: \"").append(script.getId()).append("\"\n"); + errorMsg.append("Exception: ").append(e); + Flixel.error("Polyverse", errorMsg, e); + FlixelRuntimeUtil.showErrorAlert("Polyverse Script Exception", errorMsg); } } } diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java index 6b41e28..bfc6788 100644 --- a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java @@ -4,7 +4,7 @@ public abstract class Script { /** The unique identifier {@code this} script. */ - protected String id; + private final String id; public Script(String id) { this.id = id; From 30e1d3466ada204704cff8ac7bf10a8187a3ab04 Mon Sep 17 00:00:00 2001 From: String Date: Sat, 7 Feb 2026 01:42:20 -0600 Subject: [PATCH 5/9] Redo markdown docs --- COMPILING.md | 47 ++++++++++++++++++++++++++--------------------- LICENSE.md | 4 ++-- PROJECT.md | 2 +- README.md | 6 +++--- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/COMPILING.md b/COMPILING.md index 465f4d7..f29f9d6 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -1,28 +1,23 @@ -# How to Compile FNF:JE +# How to Compile the Polyverse Environment > [!TIP] -> If you want some helpful info about the Gradle tasks (or the framework FNF:JE uses), consider taking a look at [LIBGDX.md](PROJECT.md)! +> If you want some helpful info about the Gradle tasks (or the frameworks Polyverse uses), consider taking a look at [PROJECT.md](PROJECT.md)! -There are two main ways you can download and compile the game's code: with GitHub Desktop or the terminal. - -In this guide, we'll use the GitHub Desktop method, since it the most user-friendly experience, which provides you a nice UI and does all the hard stuff for you! - -# Prerequisites (for all methods) -- A [GitHub](https://github.com) account to download the game's GitHub repository. +# Prerequisites (*REQUIRED*) +- A [GitHub](https://github.com) account to download the game's GitHub repository (only if you're using GitHub Desktop!). - A [Java Development Kit (JDK)](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) to compile the game's code. -- An integrated development environment. - - We recommend either one of the options listed below: - - [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) - - [Eclipse](https://www.eclipse.org/downloads/) - - [VS Code](https://code.visualstudio.com/) -- Some basic knowledge of programming (especially Gradle) and how to navigate an IDE. +- The [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) IDE. +- Some basic knowledge of programming (especially with using Gradle) and how to navigate an IDE. > [!TIP] -> Although every IDE listed is great for Java, we STRONGLY recommend IntelliJ IDEA, due to how beginner-friendly and integrated it is with FNF:JE! +> Although Eclipse is great for Java as well, we STRONGLY recommend IntelliJ IDEA, due to how beginner-friendly and integrated it is with Polyverse! # Step-by-Step Guide -1. Visit the official [GitHub Desktop website](https://desktop.github.com/download/) and download the app. +1. Download the JDK and run the installer accordingly. You generally don't need to change any settings + +2. Visit the official [GitHub Desktop website](https://desktop.github.com/download/) and download the app. Make sure to run the installer and + sign in with your GitHub account! > [!TIP] > If you're on Linux, don't worry! You can install a community made version by running the following commands in the terminal: @@ -35,19 +30,29 @@ In this guide, we'll use the GitHub Desktop method, since it the most user-frien > sudo apt update && sudo apt install github-desktop > ``` -2. When GitHub Desktop is done installing, sign in with your GitHub account when prompted to do so. +3. When GitHub Desktop is done installing, sign in with your GitHub account when prompted to do so. + +4. Go back to your browser and (on the official home page for Polyverse's repository), click the green `<> Code` button and select `Open with GitHub Desktop`. + You should see a prompt asking if you want to clone the game's code. + +> [!NOTE] +> If you're on Linux, cloning through the GitHub website and desktop app won't work. Although most Linux distros already have Git installed, +> you can check if you have it by running the command `git --version`. If you get an error saying the command doesn't exist, then you will have to install it onto your system. +> You can install Polyverse through Git with the following command (make sure you're running it in the folder you want it to be located in!): -3. Go back to your browser and (on the official home page for FNF:JE's repository), click the green `<> Code` button and select `Open with GitHub Desktop`. You should see a prompt asking if you want to clone the game's code. +```shell +git clone https://github.com/stringdotjar/Polyverse-Funkin.git optional/path/to/clone/into/ +``` -4. Click the blue `Clone` button and wait for the game's code to download. +5. Click the blue `Clone` button and wait for the game's code to download. > [!TIP] > We recommend putting the game's code in a place where you'll easily remember where it's at, such as your `Documents\GitHub` folder if you're on Windows! -5. Launch your IDE and open the game's folder when prompted to do so. +6. Open IntelliJ IDEA and open the game's folder when prompted to do so. > [!IMPORTANT] > When you open the game's folder, your IDE will most likely start running Gradle tasks. Don't worry, this is normal and expected! -6. When the tasks are finished, you can now run the game's code! You can do so by running the applicable task depending on the platform. +7. When the tasks are finished, you can now run the game's code! You can do so by running the applicable task depending on the platform. - For example, you can run the desktop version by executing the task `lwjgl3:run`. diff --git a/LICENSE.md b/LICENSE.md index 4a3c97d..a986765 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,8 +1,8 @@ -# Friday Night Funkin': Java Edition License +# Polyverse Funkin' License The original game (created by Cameron Taylor and the Funkin' Crew) uses a license called the *Apache License*, which is a permissive, free software license that allows users to freely use, modify, and distribute the software for any purpose, including commercially. -Because Friday Night Funkin': Java Edition (FNF:JE) is based off of the base game, we also adapt the same license they have to maintain consistency. +Because Polyverse Funkin' is an environment made for the base game, we also adapt the same license they have to maintain consistency. ## License diff --git a/PROJECT.md b/PROJECT.md index ebc1a2e..50ad7ee 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -2,7 +2,7 @@ # Project Modules -FNF:JE has many different submodules to organize the environment. +Polyverse has many different submodules to organize the environment. - `funkin`: The core part of the game's code. This is where all the game logic is implemented. - `flixelgdx`: Custom framework that bridges [HaxeFlixel](https://haxeflixel.com/) to Java and is based on libGDX. This is where the HaxeFlixel-like API is implemented. diff --git a/README.md b/README.md index cdb21a4..36e498d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Friday Night Funkin': Java Edition +# Polyverse Funkin' -Welcome to the official GitHub repository for the Java edition of Friday Night Funkin'! +Welcome to the official GitHub repository for the Java modding environment of Friday Night Funkin'! > [!NOTE] > This is a fan-made project and is not affiliated with the original developers. You can play the original game [here](https://ninja-muffin24.itch.io/funkin). @@ -8,7 +8,7 @@ Welcome to the official GitHub repository for the Java edition of Friday Night F ## Please note that this unofficial version of the game is NOT completed! This has a long way to go, but with YOUR help, we can make this project come alive! <3 # About the Project -Friday Night Funkin': Java Edition is an open-source project that aims to recreate the popular rhythm game [Friday Night Funkin'](https://github.com/FunkinCrew/Funkin) using Java. +Polyverse Funkin' is an open-source project that aims to create the popular rhythm game [Friday Night Funkin'](https://github.com/FunkinCrew/Funkin) using Java. It is built using its own custom framework based on libGDX, called FlixelGDX: a Java port of the HaxeFlixel framework used in the original game. The project is designed to have practically endless modding capabilities, empowering developers to use features for mods From a1e639acd2976697bf9613bd2fea81f26f8fcc72 Mon Sep 17 00:00:00 2001 From: String Date: Sat, 7 Feb 2026 07:29:43 -0600 Subject: [PATCH 6/9] Rename classes to match Flixel and fixed state switch bug --- .../funkin/android/AndroidLauncher.java | 4 +- assets/another_test.groovy | 2 +- assets/test.groovy | 6 +- .../me/stringdotjar/flixelgdx/Flixel.java | 128 ++++++------- .../me/stringdotjar/flixelgdx/FlixelGame.java | 169 +++++++++++------- .../flixelgdx/graphics/FlixelObject.java | 2 +- .../{FlixelScreen.java => FlixelState.java} | 10 +- .../flixelgdx/signal/FlixelSignalData.java | 6 +- .../me/stringdotjar/funkin/FunkinGame.java | 6 +- .../me/stringdotjar/funkin/InitScreen.java | 14 -- .../me/stringdotjar/funkin/InitState.java | 14 ++ .../{TitleScreen.java => TitleState.java} | 4 +- .../funkin/util/FunkinConstants.java | 2 +- .../funkin/lwjgl3/Lwjgl3Launcher.java | 4 +- 14 files changed, 204 insertions(+), 167 deletions(-) rename flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/{FlixelScreen.java => FlixelState.java} (89%) delete mode 100644 funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java create mode 100644 funkin/src/main/java/me/stringdotjar/funkin/InitState.java rename funkin/src/main/java/me/stringdotjar/funkin/menus/{TitleScreen.java => TitleState.java} (95%) diff --git a/android/src/main/java/me/stringdotjar/funkin/android/AndroidLauncher.java b/android/src/main/java/me/stringdotjar/funkin/android/AndroidLauncher.java index 999621a..a1f2eb8 100644 --- a/android/src/main/java/me/stringdotjar/funkin/android/AndroidLauncher.java +++ b/android/src/main/java/me/stringdotjar/funkin/android/AndroidLauncher.java @@ -4,7 +4,7 @@ import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import me.stringdotjar.funkin.FunkinGame; -import me.stringdotjar.funkin.InitScreen; +import me.stringdotjar.funkin.InitState; import me.stringdotjar.funkin.util.FunkinConstants; /** Launches the Android application. */ @@ -17,7 +17,7 @@ protected void onCreate(Bundle savedInstanceState) { FunkinConstants.WINDOW_TITLE, FunkinConstants.WINDOW_WIDTH, FunkinConstants.WINDOW_HEIGHT, - new InitScreen() + new InitState() ); AndroidApplicationConfiguration configuration = new AndroidApplicationConfiguration(); configuration.useImmersiveMode = true; // Recommended, but not required. diff --git a/assets/another_test.groovy b/assets/another_test.groovy index 243bdcb..e7bed07 100644 --- a/assets/another_test.groovy +++ b/assets/another_test.groovy @@ -32,7 +32,7 @@ class AnotherTestClass extends Script { sprite.setPosition(randomPosX, randomPosY) - Flixel.screen.add(sprite) + Flixel.state.add(sprite) } } diff --git a/assets/test.groovy b/assets/test.groovy index ea4b33b..78e664b 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -2,7 +2,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import me.stringdotjar.flixelgdx.Flixel -import me.stringdotjar.flixelgdx.graphics.FlixelScreen +import me.stringdotjar.flixelgdx.graphics.FlixelState import me.stringdotjar.flixelgdx.backend.FlixelPaths import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite import me.stringdotjar.polyverse.script.type.SystemScript @@ -24,7 +24,7 @@ class TestScript extends SystemScript { super.onRender(delta) if (Gdx.input.isKeyJustPressed(Input.Keys.Q)) { - Flixel.setScreen(new TestScreen()) + Flixel.switchState(new TestState()) } } @@ -41,7 +41,7 @@ class TestScript extends SystemScript { } } -class TestScreen extends FlixelScreen { +class TestState extends FlixelState { private FlixelSprite test diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java index bb89a0e..59c8328 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java @@ -9,14 +9,14 @@ import games.rednblack.miniaudio.MASound; import games.rednblack.miniaudio.MiniAudio; import games.rednblack.miniaudio.loader.MASoundLoader; -import me.stringdotjar.flixelgdx.graphics.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelState; import me.stringdotjar.flixelgdx.graphics.FlixelViewport; -import me.stringdotjar.flixelgdx.util.FlixelConstants; import me.stringdotjar.flixelgdx.signal.FlixelSignal; import me.stringdotjar.flixelgdx.signal.FlixelSignalData.MusicPlayedSignalData; -import me.stringdotjar.flixelgdx.signal.FlixelSignalData.RenderSignalData; -import me.stringdotjar.flixelgdx.signal.FlixelSignalData.ScreenSwitchSignalData; +import me.stringdotjar.flixelgdx.signal.FlixelSignalData.UpdateSignalData; +import me.stringdotjar.flixelgdx.signal.FlixelSignalData.StateSwitchSignalData; import me.stringdotjar.flixelgdx.signal.FlixelSignalData.SoundPlayedSignalData; +import me.stringdotjar.flixelgdx.util.FlixelConstants; import org.jetbrains.annotations.NotNull; import java.time.LocalDateTime; @@ -29,8 +29,8 @@ */ public final class Flixel { - /** The current {@code FlixelScreen} being displayed. */ - private static FlixelScreen screen; + /** The current {@code FlixelState} being displayed. */ + private static FlixelState state; /** The main audio object used to create, */ private static MiniAudio engine; @@ -63,7 +63,7 @@ public final class Flixel { */ public static void initialize(FlixelGame gameInstance) { if (initialized) { - throw new IllegalStateException("FNF:JE has already been initialized!"); + throw new IllegalStateException("FlixelGDX has already been initialized!"); } game = gameInstance; @@ -80,24 +80,24 @@ public static void initialize(FlixelGame gameInstance) { /** * Sets the current screen to the provided screen. * - * @param newScreen The new {@code FlixelScreen} to set as the current screen. + * @param newState The new {@code FlixelState} to set as the current screen. */ - public static void setScreen(FlixelScreen newScreen) { - Signals.preScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); + public static void switchState(FlixelState newState) { + Signals.preStateSwitch.dispatch(new StateSwitchSignalData(newState)); if (!initialized) { - throw new IllegalStateException("FNF:JE has not been initialized yet!"); + throw new IllegalStateException("Polyverse has not been initialized yet!"); } - if (newScreen == null) { - throw new IllegalArgumentException("Screen cannot be null!"); + if (newState == null) { + throw new IllegalArgumentException("New state cannot be null!"); } - if (screen != null) { - screen.hide(); - screen.dispose(); + if (state != null) { + state.hide(); + state.dispose(); } game.resetViewports(); - screen = newScreen; - screen.create(); - Signals.postScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); + state = newState; + state.create(); + Signals.postStateSwitch.dispatch(new StateSwitchSignalData(newState)); } /** @@ -112,8 +112,8 @@ public static void setScreen(FlixelScreen newScreen) { * } * * @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). + * 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 MASound playSound(String path) { @@ -131,9 +131,9 @@ public static MASound playSound(String path) { * Flixel.playSound(FlixelPaths.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 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. */ @@ -152,10 +152,10 @@ public static MASound playSound(String path, float volume) { * Flixel.playSound(FlixelPaths.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 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. */ @@ -175,13 +175,13 @@ public static MASound playSound(String path, float volume, boolean looping) { * Flixel.playSound(FlixelPaths.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 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 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) { @@ -202,13 +202,13 @@ public static MASound playSound(String path, float volume, boolean looping, MAGr * Flixel.playSound(FlixelPaths.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 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. */ @@ -234,8 +234,8 @@ public static MASound playSound(@NotNull String path, float volume, boolean loop * } * * @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). + * 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); @@ -252,9 +252,9 @@ public static void playMusic(String path) { * Flixel.playMusic(FlixelPaths.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 path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). * @param volume The volume to play the new music with. */ public static void playMusic(String path, float volume) { @@ -272,10 +272,10 @@ public static void playMusic(String path, float volume) { * Flixel.playMusic(FlixelPaths.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 path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new music with. * @param looping Should the new music loop indefinitely? */ public static void playMusic(String path, float volume, boolean looping) { @@ -295,11 +295,11 @@ public static void playMusic(String path, float volume, boolean looping) { * Flixel.playMusic(FlixelPaths.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 path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new music with. + * @param looping Should the new music loop indefinitely? * @param external Should this music be loaded externally? (This is only for mobile platforms!) */ public static void playMusic(String path, float volume, boolean looping, boolean external) { @@ -369,8 +369,8 @@ public static Stage getStage() { return game.stage; } - public static FlixelScreen getScreen() { - return screen; + public static FlixelState getState() { + return state; } public static MASound getMusic() { @@ -378,7 +378,7 @@ public static MASound getMusic() { } public static Vector2 getWindowSize() { - return game.windowSize; + return game.viewSize; } public static MiniAudio getAudioEngine() { @@ -397,7 +397,7 @@ public static MAGroup getSoundsGroup() { return soundsGroup; } - public static float getDelta() { + public static float getElapsed() { return Gdx.graphics.getDeltaTime(); } @@ -421,10 +421,12 @@ public static boolean isFullscreen() { */ public static final class Signals { - public static final FlixelSignal preRender = new FlixelSignal<>(); - public static final FlixelSignal postRender = new FlixelSignal<>(); - public static final FlixelSignal preScreenSwitch = new FlixelSignal<>(); - public static final FlixelSignal postScreenSwitch = new FlixelSignal<>(); + public static final FlixelSignal preUpdate = new FlixelSignal<>(); + public static final FlixelSignal postUpdate = new FlixelSignal<>(); + public static final FlixelSignal preDraw = new FlixelSignal<>(); + public static final FlixelSignal postDraw = new FlixelSignal<>(); + public static final FlixelSignal preStateSwitch = new FlixelSignal<>(); + public static final FlixelSignal postStateSwitch = new FlixelSignal<>(); public static final FlixelSignal preGameClose = new FlixelSignal<>(); public static final FlixelSignal postGameClose = new FlixelSignal<>(); public static final FlixelSignal windowFocused = new FlixelSignal<>(); diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java index faa04b5..5630cd8 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java @@ -12,13 +12,13 @@ import com.badlogic.gdx.utils.ScreenUtils; import com.badlogic.gdx.utils.SnapshotArray; import me.stringdotjar.flixelgdx.graphics.FlixelObject; -import me.stringdotjar.flixelgdx.graphics.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelState; import me.stringdotjar.flixelgdx.graphics.FlixelViewport; import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; import me.stringdotjar.flixelgdx.tween.FlixelTween; import me.stringdotjar.flixelgdx.util.FlixelRuntimeUtil; -import static me.stringdotjar.flixelgdx.signal.FlixelSignalData.RenderSignalData; +import static me.stringdotjar.flixelgdx.signal.FlixelSignalData.UpdateSignalData; /** * Flixel's enhanced game object used for containing the main loop and core elements of Flixel. @@ -32,10 +32,13 @@ public abstract class FlixelGame implements ApplicationListener { protected String title; /** The size of the game's starting window position and its viewport. */ + protected Vector2 viewSize; + + /** The current window size stored in a vector object. */ protected Vector2 windowSize; /** The entry point screen the game starts in (which becomes null after the game is done setting up!). */ - protected FlixelScreen initialScreen; + protected FlixelState initialScreen; /** Is the game's window currently focused? */ private boolean isFocused = true; @@ -64,8 +67,9 @@ public abstract class FlixelGame implements ApplicationListener { * @param height The starting height of the game's window and how tall the viewport should be. * @param initialScreen The initial screen to load when the game starts. */ - public FlixelGame(String title, int width, int height, FlixelScreen initialScreen) { + public FlixelGame(String title, int width, int height, FlixelState initialScreen) { this.title = title; + this.viewSize = new Vector2(width, height); this.windowSize = new Vector2(width, height); this.initialScreen = initialScreen; } @@ -76,7 +80,7 @@ public void create() { batch = new SpriteBatch(); viewports = new SnapshotArray<>(FlixelViewport.class); - viewports.add(new FlixelViewport((int) windowSize.x, (int) windowSize.y)); + viewports.add(new FlixelViewport((int) viewSize.x, (int) viewSize.y)); stage = new Stage(getViewport(), batch); @@ -86,7 +90,7 @@ public void create() { bgTexture = new Texture(pixmap); pixmap.dispose(); - Flixel.setScreen(initialScreen); + Flixel.switchState(initialScreen); initialScreen = null; } @@ -102,68 +106,94 @@ public void resize(int width, int height) { viewports.end(); } - @Override - public void render() { - float delta = Gdx.graphics.getDeltaTime(); - FlixelScreen screen = Flixel.getScreen(); + /** + * Updates the logic of the game's loop. + * + * @param elapsed The amount of time that occurred in the last frame. + */ + public void update(float elapsed) { + Flixel.Signals.preUpdate.dispatch(new UpdateSignalData(elapsed)); - Flixel.Signals.preRender.dispatch(new RenderSignalData(delta)); + stage.act(elapsed); + FlixelTween.getGlobalManager().update(elapsed); - stage.act(delta); - FlixelTween.getGlobalManager().update(delta); + FlixelState state = Flixel.getState(); + state.update(elapsed); - // Update and render the current screen that's active. + // Update all members contained in the current state. + SnapshotArray members = state.getMembers(); + FlixelObject[] mbrs = members.begin(); + for (int i = 0; i < members.size; i++) { + mbrs[i].update(elapsed); + } + members.end(); + + Flixel.Signals.postUpdate.dispatch(new UpdateSignalData(elapsed)); + } + + /** + * Updates the graphics and display of the game. + */ + public void draw() { + Flixel.Signals.preDraw.dispatch(); ScreenUtils.clear(Color.BLACK); - if (screen != null) { - screen.update(delta); + // Loop through all viewports and draw the current state's members onto their set viewports. + FlixelViewport[] vps = viewports.begin(); + FlixelState state = Flixel.getState(); + SnapshotArray members = state.getMembers(); + FlixelObject[] mbrs = members.begin(); + for (int i = 0; i < viewports.size; i++) { + FlixelViewport viewport = vps[i]; + if (viewport == null) { + continue; + } - // Loop through all viewports and draw the current screen's members onto their set viewports. - FlixelViewport[] vps = viewports.begin(); - SnapshotArray members = screen.getMembers(); - for (int i = 0; i < viewports.size; i++) { - FlixelViewport viewport = vps[i]; - if (viewport == null) { + var camera = viewport.getCamera(); + viewport.apply(); + camera.update(); + batch.setProjectionMatrix(camera.combined); + batch.begin(); + + // Draw the background color first. + batch.setColor(state.getBgColor()); + batch.draw(bgTexture, 0, 0, getViewport().getWorldWidth(), getViewport().getWorldHeight()); + batch.setColor(Color.WHITE); // Set the batch color back to white so display objects aren't affected. + + // Draw all the current screens members. + for (int j = 0; j < members.size; j++) { + FlixelObject member = mbrs[j]; + if (member == null) { continue; } - - viewport.apply(); - var camera = viewport.getCamera(); - camera.update(); - batch.setProjectionMatrix(camera.combined); - batch.begin(); - - // Draw the background color first. - batch.setColor(screen.getBgColor()); - batch.draw(bgTexture, 0, 0, getViewport().getWorldWidth(), getViewport().getWorldHeight()); - batch.setColor(Color.WHITE); // Set the batch color back to white so display objects aren't affected. - // Draw all the current screens members. - FlixelObject[] mbrs = members.begin(); - for (int j = 0; j < members.size; j++) { - FlixelObject member = mbrs[j]; - if (member == null) { - continue; - } - member.update(delta); - if (member instanceof FlixelSprite s) { - // Check if the current sprite is in the visible part of the viewport. - // If it cannot be seen, then we don't draw the sprite to save performance. - if (camera.frustum.boundsInFrustum(s.getX(), s.getY(), 0, s.getWidth(), s.getHeight(), 0)) { - s.draw(batch); - } - } else { - member.draw(batch); + if (member instanceof FlixelSprite s) { + // Check if the current sprite is in the visible part of the viewport. + // If it cannot be seen, then we don't draw the sprite to save performance. + if (camera.frustum.boundsInFrustum(s.getX(), s.getY(), 0, s.getWidth(), s.getHeight(), 0)) { + s.draw(batch); } + } else { + member.draw(batch); } - batch.end(); - members.end(); } - viewports.end(); - screen.draw(batch); + batch.end(); } + viewports.end(); + members.end(); + state.draw(batch); stage.draw(); + Flixel.Signals.postDraw.dispatch(); + } + + @Override + public void render() { + float delta = Gdx.graphics.getDeltaTime(); - Flixel.Signals.postRender.dispatch(new RenderSignalData(delta)); + windowSize.x = Gdx.graphics.getWidth(); + windowSize.y = Gdx.graphics.getHeight(); + + update(delta); + draw(); } @Override @@ -212,13 +242,10 @@ public void onWindowMinimized(boolean iconified) { */ public void setFullscreen(boolean enabled) { if (enabled) { - if (!Flixel.isFullscreen()) { - windowSize.set(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); - } Gdx.graphics.setFullscreenMode(Gdx.graphics.getDisplayMode()); Flixel.info("Entered fullscreen mode."); } else { - Gdx.graphics.setWindowedMode((int) windowSize.x, (int) windowSize.y); + Gdx.graphics.setWindowedMode((int) viewSize.x, (int) viewSize.y); Flixel.info("Exited fullscreen mode."); } } @@ -235,8 +262,8 @@ public void dispose() { Flixel.Signals.preGameClose.dispatch(); Flixel.info("Disposing the screen display..."); - Flixel.getScreen().hide(); - Flixel.getScreen().dispose(); + Flixel.getState().hide(); + Flixel.getState().dispose(); stage.dispose(); batch.dispose(); bgTexture.dispose(); @@ -272,6 +299,10 @@ public String getTitle() { return title; } + public Vector2 getViewSize() { + return viewSize; + } + public Vector2 getWindowSize() { return windowSize; } @@ -284,6 +315,12 @@ public Stage getStage() { return stage; } + /** + * Gets the first viewport that is part of the list. If the list is {@code null} or empty, then a new list (with a + * default viewport accordingly). + * + * @return The first viewport in the list. + */ public FlixelViewport getViewport() { Vector2 windowSize = Flixel.getWindowSize(); if (viewports == null) { @@ -298,8 +335,10 @@ public FlixelViewport getViewport() { } public void resetViewports() { + FlixelViewport viewport = new FlixelViewport((int) viewSize.x, (int) viewSize.y); + viewport.update((int) windowSize.x, (int) windowSize.y, true); viewports.clear(); - viewports.add(new FlixelViewport((int) windowSize.x, (int) windowSize.y)); + viewports.add(viewport); } public SnapshotArray getViewports() { @@ -318,9 +357,9 @@ public boolean isMinimized() { return isMinimized; } - public void setWindowSize(Vector2 windowSize) { - this.windowSize = windowSize; - Gdx.graphics.setWindowedMode((int) windowSize.x, (int) windowSize.y); - Flixel.info("Set window size. (WIDTH=" + windowSize.x + ", HEIGHT=" + windowSize.y + ")"); + public void setWindowSize(Vector2 newSize) { + viewSize = newSize; + Gdx.graphics.setWindowedMode((int) newSize.x, (int) newSize.y); + Flixel.info("Set window to new size. (WIDTH=" + newSize.x + ", HEIGHT=" + newSize.y + ")"); } } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java index 489ba9d..c806248 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java @@ -2,7 +2,7 @@ import com.badlogic.gdx.graphics.g2d.Batch; -/** An interface which allows any class that implements it to be added to a {@link FlixelScreen}. */ +/** An interface which allows any class that implements it to be added to a {@link FlixelState}. */ public interface FlixelObject { void update(float delta); void draw(Batch batch); diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelScreen.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java similarity index 89% rename from flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelScreen.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java index 6957ec6..d184750 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelScreen.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java @@ -10,7 +10,7 @@ * Base class for creating a better screen display with more functionality than the default {@link * com.badlogic.gdx.Screen} interface. */ -public abstract class FlixelScreen extends FlixelGroup implements Screen { +public abstract class FlixelState extends FlixelGroup implements Screen { /** Should {@code this} screen update its logic even when a substate is currently opened? */ public boolean persistentUpdate = false; @@ -21,7 +21,7 @@ public abstract class FlixelScreen extends FlixelGroup implements /** The background color of {@code this} current screen. */ protected Color bgColor; - public FlixelScreen() { + public FlixelState() { members = new SnapshotArray<>(FlixelObject.class); } @@ -46,16 +46,12 @@ public void update(float delta) { if (!persistentUpdate) { return; } - - forEachMember(object -> { - object.update(delta); - }); } /** * Draws its members onto the screen. * - * @param batch The batch that's used to draw the + * @param batch The batch that's used to draw {@code this} state's members. */ public void draw(Batch batch) { if (!persistentDraw) { diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java index 0da3e0d..0156008 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java @@ -1,7 +1,7 @@ package me.stringdotjar.flixelgdx.signal; import games.rednblack.miniaudio.MASound; -import me.stringdotjar.flixelgdx.graphics.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelState; import me.stringdotjar.flixelgdx.Flixel; /** @@ -10,9 +10,9 @@ */ public final class FlixelSignalData { - public record RenderSignalData(float delta) {} + public record UpdateSignalData(float delta) {} - public record ScreenSwitchSignalData(FlixelScreen screen) {} + public record StateSwitchSignalData(FlixelState screen) {} public record SoundPlayedSignalData(MASound sound) {} diff --git a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java index 62e2258..866df71 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java @@ -4,7 +4,7 @@ import me.stringdotjar.flixelgdx.Flixel; import me.stringdotjar.flixelgdx.FlixelGame; import me.stringdotjar.flixelgdx.backend.FlixelPaths; -import me.stringdotjar.flixelgdx.graphics.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelState; import me.stringdotjar.polyverse.Polyverse; import me.stringdotjar.polyverse.script.type.Script; import me.stringdotjar.polyverse.script.type.SystemScript; @@ -16,7 +16,7 @@ public class FunkinGame extends FlixelGame { private float lastVolume = 1.0f; - public FunkinGame(String title, int width, int height, FlixelScreen initialScreen) { + public FunkinGame(String title, int width, int height, FlixelState initialScreen) { super(title, width, height, initialScreen); } @@ -34,7 +34,7 @@ public void render() { toggleFullscreen(); } - Polyverse.forAllScripts(script -> script.onRender(Flixel.getDelta())); + Polyverse.forAllScripts(script -> script.onRender(Flixel.getElapsed())); } @Override diff --git a/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java b/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java deleted file mode 100644 index a9ccf3f..0000000 --- a/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java +++ /dev/null @@ -1,14 +0,0 @@ -package me.stringdotjar.funkin; - -import me.stringdotjar.flixelgdx.graphics.FlixelScreen; -import me.stringdotjar.flixelgdx.Flixel; -import me.stringdotjar.funkin.menus.TitleScreen; - -public class InitScreen extends FlixelScreen { - - @Override - public void create() { - super.create(); - Flixel.setScreen(new TitleScreen()); - } -} diff --git a/funkin/src/main/java/me/stringdotjar/funkin/InitState.java b/funkin/src/main/java/me/stringdotjar/funkin/InitState.java new file mode 100644 index 0000000..412adcc --- /dev/null +++ b/funkin/src/main/java/me/stringdotjar/funkin/InitState.java @@ -0,0 +1,14 @@ +package me.stringdotjar.funkin; + +import me.stringdotjar.flixelgdx.graphics.FlixelState; +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.funkin.menus.TitleState; + +public class InitState extends FlixelState { + + @Override + public void create() { + super.create(); + Flixel.switchState(new TitleState()); + } +} diff --git a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java similarity index 95% rename from funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java rename to funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java index 79fab0a..03e0778 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java @@ -3,7 +3,7 @@ import games.rednblack.miniaudio.MASound; import me.stringdotjar.flixelgdx.Flixel; import me.stringdotjar.flixelgdx.backend.FlixelPaths; -import me.stringdotjar.flixelgdx.graphics.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelState; import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; import me.stringdotjar.flixelgdx.input.FlixelKey; import me.stringdotjar.flixelgdx.tween.FlixelEase; @@ -11,7 +11,7 @@ import me.stringdotjar.flixelgdx.tween.settings.FlixelTweenSettings; import me.stringdotjar.flixelgdx.tween.settings.FlixelTweenType; -public class TitleScreen extends FlixelScreen { +public class TitleState extends FlixelState { private FlixelSprite logo; diff --git a/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java b/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java index 8fd5196..1981026 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java @@ -8,7 +8,7 @@ public final class FunkinConstants { /** * The default title for the game's window. */ - public static final String WINDOW_TITLE = "Friday Night Funkin': Java Edition"; + public static final String WINDOW_TITLE = "Polyverse Funkin'"; /** * How wide the window's viewport is in pixels. This also affects how wide the window is when diff --git a/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java b/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java index 7222c22..3ad634d 100644 --- a/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java +++ b/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java @@ -5,7 +5,7 @@ import com.badlogic.gdx.backends.lwjgl3.Lwjgl3WindowAdapter; import me.stringdotjar.flixelgdx.Flixel; import me.stringdotjar.funkin.FunkinGame; -import me.stringdotjar.funkin.InitScreen; +import me.stringdotjar.funkin.InitState; import me.stringdotjar.funkin.util.FunkinConstants; /** Launches the desktop (LWJGL3) application. */ @@ -23,7 +23,7 @@ private static void createApplication() { FunkinConstants.WINDOW_TITLE, FunkinConstants.WINDOW_WIDTH, FunkinConstants.WINDOW_HEIGHT, - new InitScreen() + new InitState() ); Flixel.initialize(game); // This is VERY important to do before creating the application! var size = game.getWindowSize(); From d2939f55cd201ae5cca7f35f82bd64050693863a Mon Sep 17 00:00:00 2001 From: String Date: Sun, 15 Feb 2026 23:23:24 -0600 Subject: [PATCH 7/9] Add functionality in places and rework some stuff --- .../me/stringdotjar/flixelgdx/FlixelGame.java | 9 +++ .../flixelgdx/graphics/FlixelState.java | 20 ++++++- .../graphics/sprite/FlixelSprite.java | 45 ++++++++++----- .../flixelgdx/group/FlixelGroup.java | 55 +++++++++++++++--- .../flixelgdx/group/FlixelGroupable.java | 9 +-- .../flixelgdx/group/FlixelSpriteGroup.java | 57 +++++++++++++++++++ .../flixelgdx/input/FlixelKey.java | 5 +- 7 files changed, 168 insertions(+), 32 deletions(-) create mode 100644 flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java index 5630cd8..0d30fc5 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java @@ -58,6 +58,9 @@ public abstract class FlixelGame implements ApplicationListener { /** Where all the global viewports are stored. */ protected SnapshotArray viewports; + /** Is the game currently closing? */ + private boolean isClosing = false; + /** * Creates a new game instance with the specified title, window width/height, and initial screen. This configures * the game's core parts, such as the viewport, stage, etc. @@ -257,6 +260,12 @@ public void toggleFullscreen() { @Override public void dispose() { + if (isClosing) { + Flixel.warn("Game is already closing. Skipping dispose..."); + return; + } + isClosing = true; + Flixel.warn("SHUTTING DOWN GAME AND DISPOSING ALL RESOURCES."); Flixel.Signals.preGameClose.dispatch(); diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java index d184750..bd8ea74 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java @@ -71,8 +71,26 @@ public void resume() {} @Override public void hide() {} + /** + * Disposes {@code this} state and all of its members. Called automatically when {@link + * me.stringdotjar.flixelgdx.Flixel#switchState(FlixelState)} is used, so that sprites and other + * {@link FlixelObject}s release its resources. + */ @Override - public void dispose() {} + public void dispose() { + if (members == null) { + return; + } + Object[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject obj = (FlixelObject) items[i]; + if (obj != null) { + obj.destroy(); + } + } + members.end(); + members.clear(); + } /** * Adds a new sprite to {@code this} screen. If it is {@code null}, it will not be added and diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java index 30b5946..adb8c31 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java @@ -7,6 +7,7 @@ import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Pool; @@ -147,7 +148,7 @@ public FlixelSprite loadSparrowFrames(FileHandle texture, FileHandle xmlFile) { public FlixelSprite 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<>(); + atlasRegions = new Array<>(AtlasRegion.class); for (XmlReader.Element subTexture : xmlFile.getChildrenByName("SubTexture")) { String name = subTexture.getAttribute("name"); @@ -325,23 +326,37 @@ public void reset() { 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(); + if (texture != null) { + texture.dispose(); + texture = null; } - 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(); + if (currentFrame != null) { + currentFrame.getTexture().dispose(); + currentFrame = null; + } + if (atlasRegions != null) { + for (int i = atlasRegions.size - 1; i >= 0; i--) { + var region = atlasRegions.items[i]; + if (region != null) { + region.getTexture().dispose(); + } + } + atlasRegions.setSize(0); + atlasRegions = null; + } + if (frames != null) { + for (int i = frames.length - 1; i >= 0; i--) { + var frame = frames[i]; + if (frame != null) { + for (TextureRegion region : frame) { + if (region != null) { + region.getTexture().dispose(); + } + } + } } + frames = null; } - frames = null; } public void changeX(float x) { diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java index 40eadec..1aac9ab 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java @@ -1,21 +1,24 @@ package me.stringdotjar.flixelgdx.group; +import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.utils.SnapshotArray; +import me.stringdotjar.flixelgdx.graphics.FlixelObject; + import java.util.function.Consumer; /** * Base class for creating groups with a list of members inside of it. */ -public abstract class FlixelGroup implements FlixelGroupable { +public abstract class FlixelGroup implements FlixelGroupable, FlixelObject { /** * The list of members that {@code this} group contains. */ - protected SnapshotArray members; + protected SnapshotArray members; public FlixelGroup() { - members = new SnapshotArray<>(); + members = new SnapshotArray<>(FlixelObject.class); } @Override @@ -23,30 +26,64 @@ public void add(T member) { members.add(member); } + @Override + public void update(float elapsed) { + FlixelObject[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = items[i]; + if (member == null) { + continue; + } + member.update(elapsed); + } + members.end(); + } + + @Override + public void draw(Batch batch) { + FlixelObject[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = items[i]; + if (member == null) { + continue; + } + member.draw(batch); + } + members.end(); + } + @Override public void remove(T member) { members.removeValue(member, true); } @Override - public void clear() { + public void destroy() { + members.forEach(FlixelObject::destroy); members.clear(); } @Override - public void forEachMember(Consumer callback) { - for (T member : members.begin()) { + public void clear() { + members.clear(); + } + + public void forEachMember(Consumer callback) { + FlixelObject[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = items[i]; if (member == null) { continue; } callback.accept(member); } + members.end(); } public void forEachMemberType(Class type, Consumer callback) { - T[] items = members.begin(); + FlixelObject[] items = members.begin(); for (int i = 0, n = members.size; i < n; i++) { - T member = items[i]; + FlixelObject member = items[i]; if (type.isInstance(member)) { callback.accept(type.cast(member)); } @@ -54,7 +91,7 @@ public void forEachMemberType(Class type, Consumer callback) { members.end(); } - public SnapshotArray getMembers() { + public SnapshotArray getMembers() { return members; } } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java index c464452..b6340a0 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java @@ -1,13 +1,10 @@ package me.stringdotjar.flixelgdx.group; -import java.util.function.Consumer; - /** * Interface for creating new groups with members inside of them. */ -public interface FlixelGroupable { - void add(T member); - void remove(T member); +public interface FlixelGroupable { + void add(FlixelObject member); + void remove(FlixelObject member); void clear(); - void forEachMember(Consumer callback); } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java new file mode 100644 index 0000000..b8a86ff --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java @@ -0,0 +1,57 @@ +package me.stringdotjar.flixelgdx.group; + +import com.badlogic.gdx.utils.Array; +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; + +/** + * A group of sprites that can be used to group and manage multiple sprites at once. + */ +public class FlixelSpriteGroup extends FlixelGroup { + + /** + * The list of sprites that {@code this} group contains. + */ + public Array members; + + public FlixelSpriteGroup() { + super(); + members = new Array<>(FlixelSprite.class); + } + + @Override + public void update(float elapsed) { + super.update(elapsed); + FlixelSprite[] items = members.items; + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite member = items[i]; + if (member == null) { + continue; + } + member.update(elapsed); + } + } + + @Override + public void add(FlixelSprite sprite) { + super.add(sprite); + members.add(sprite); + } + + @Override + public void remove(FlixelSprite sprite) { + super.remove(sprite); + members.removeValue(sprite, true); + } + + @Override + public void clear() { + members.forEach(FlixelSprite::destroy); + members.clear(); + } + + @Override + public void destroy() { + members.forEach(FlixelSprite::destroy); + members.clear(); + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java index 963a94b..3b522fc 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java @@ -5,4 +5,7 @@ /** * A simple extension of {@link Input.Keys} to simplify accessing key constants. */ -public class FlixelKey extends Input.Keys {} +public class FlixelKey extends Input.Keys { + + private FlixelKey() {} +} From 42ddfd8923a7740d9a64e7ec1d7ad4cc60711858 Mon Sep 17 00:00:00 2001 From: String Date: Mon, 16 Feb 2026 16:33:48 -0600 Subject: [PATCH 8/9] Add special sprite group rotation and Flixel feature replications --- .../flixelgdx/graphics/FlixelState.java | 2 +- .../graphics/sprite/FlixelSprite.java | 4 + .../flixelgdx/group/FlixelSpriteGroup.java | 537 +++++++++++++++++- .../stringdotjar/funkin/menus/TitleState.java | 83 ++- 4 files changed, 600 insertions(+), 26 deletions(-) diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java index bd8ea74..dde2799 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java @@ -49,7 +49,7 @@ public void update(float delta) { } /** - * Draws its members onto the screen. + * Draws {@code this} state's members onto the screen. * * @param batch The batch that's used to draw {@code this} state's members. */ diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java index adb8c31..5f5a58a 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java @@ -367,6 +367,10 @@ public void changeY(float y) { setY(getY() + y); } + public void changeRotation(float rotation) { + setRotation(getRotation() + rotation); + } + public Map> getAnimations() { return animations; } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java index b8a86ff..146a32c 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java @@ -1,57 +1,552 @@ package me.stringdotjar.flixelgdx.group; -import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.math.MathUtils; +import me.stringdotjar.flixelgdx.graphics.FlixelObject; import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; +import java.util.Comparator; +import java.util.Random; +import java.util.function.Consumer; + /** * A group of sprites that can be used to group and manage multiple sprites at once. + *

+ * Setting the group's position ({@link #setX}, {@link #setY}, {@link #setPosition}) moves all + * member sprites by the same delta, mirroring how HaxeFlixel propagates position changes + * (except in {@link RotationMode#WHEEL} mode where positions are set absolutely each frame). + * Sprites added to the group are automatically offset by the group's current position. + *

+ * Rotation behaviour is controlled by {@link #rotationMode}: + *

    + *
  • {@link RotationMode#INDIVIDUAL} (default) – {@link #setRotation}/{@link #changeRotation} + * apply the delta to each sprite's own rotation; no positional changes.
  • + *
  • {@link RotationMode#WHEEL} – sprites are arranged radially around the center + * like a wheel, repositioned absolutely each frame in {@link #update(float)}.
  • + *
  • {@link RotationMode#ORBIT} – sprites orbit around the group origin as a rigid body + * (like a rotating camera/TV screen); both position and individual rotation are adjusted + * by the delta.
  • + *
*/ public class FlixelSpriteGroup extends FlixelGroup { /** - * The list of sprites that {@code this} group contains. + * Controls how the group's {@link #rotation} affects member sprites. + */ + public enum RotationMode { + + /** + * Rotation is applied to each sprite's individual rotation by the delta. + * No positional changes occur from rotation. + */ + INDIVIDUAL, + + /** + * Sprites are arranged in a radial/wheel pattern around the group center + * ({@link #x}, {@link #y}). Each sprite is positioned at {@link #rotationRadius} from the + * center, spaced evenly around 360 degrees. Positions and rotations are set absolutely + * each frame in {@link #update(float)}. + */ + WHEEL, + + /** + * All sprites orbit around the group origin ({@link #x}, {@link #y}) as a rigid body, + * like a rotating camera or TV screen. When rotation changes, each sprite's position is + * rotated around the center by the delta and its individual rotation is also adjusted + * by the same amount. + */ + ORBIT + } + + /** + * Maximum number of members allowed. When {@code 0}, the group can grow without limit (default). + * When {@code > 0}, {@link #add(FlixelSprite)} will not add if at capacity. */ - public Array members; + private int maxSize = 0; + /** + * The X position of the group. When changed, all member sprites are moved by the delta + * (unless {@link #rotationMode} is {@link RotationMode#WHEEL}, in which case positions are + * set absolutely each frame). + */ + private float x; + + /** + * The Y position of the group. When changed, all member sprites are moved by the delta + * (unless {@link #rotationMode} is {@link RotationMode#WHEEL}, in which case positions are + * set absolutely each frame). + */ + private float y; + + /** + * The current rotation mode. See {@link RotationMode} for details. + */ + private RotationMode rotationMode = RotationMode.INDIVIDUAL; + + /** + * Distance of each sprite from the center when {@link #rotationMode} is + * {@link RotationMode#WHEEL}. + */ + private float rotationRadius = 100f; + + /** + * The group's rotation in degrees. How this value is used depends on {@link #rotationMode}: + *
    + *
  • {@link RotationMode#INDIVIDUAL} – delta is applied to each sprite's own rotation.
  • + *
  • {@link RotationMode#WHEEL} – base angle for the radial layout; sprite {@code i} + * gets angle {@code rotation + (360 / size) * i}.
  • + *
  • {@link RotationMode#ORBIT} – delta rotates all sprites around the group origin.
  • + *
+ */ + private float rotation; + + private static final Random RANDOM = new Random(); + + /** + * Creates a new FlixelSpriteGroup with default parameters. + */ public FlixelSpriteGroup() { + this(0, 100f, 0f); + } + + /** + * Creates a new FlixelSpriteGroup with the given max size. + * + * @param maxSize Maximum members allowed; 0 for unlimited. + */ + public FlixelSpriteGroup(int maxSize) { + this(maxSize, 100f, 0f); + } + + /** + * Creates a new FlixelSpriteGroup with the given parameters. + * + * @param maxSize Maximum members allowed; 0 for unlimited. + * @param rotationRadius Distance of each sprite from the center when in {@link RotationMode#WHEEL}. + * @param rotation Initial rotation in degrees. + */ + public FlixelSpriteGroup(int maxSize, float rotationRadius, float rotation) { super(); - members = new Array<>(FlixelSprite.class); + this.maxSize = Math.max(0, maxSize); + this.rotationRadius = rotationRadius; + this.rotation = rotation; } - @Override - public void update(float elapsed) { - super.update(elapsed); - FlixelSprite[] items = members.items; - for (int i = 0, n = members.size; i < n; i++) { - FlixelSprite member = items[i]; - if (member == null) { - continue; - } - member.update(elapsed); + public float getX() { + return x; + } + + /** + * Sets the group's X position. Every member sprite is moved by the delta ({@code newX - oldX}), + * unless the rotation mode is {@link RotationMode#WHEEL} (positions are set absolutely each frame). + */ + public void setX(float x) { + float dx = x - this.x; + this.x = x; + + if (rotationMode != RotationMode.WHEEL) { + transformMembersX(dx); + } + } + + /** + * Adds the given amount to the group's X position. Equivalent to {@code setX(getX() + x)}. + */ + public void changeX(float x) { + setX(this.x + x); + } + + public float getY() { + return y; + } + + /** + * Sets the group's Y position. Every member sprite is moved by the delta ({@code newY - oldY}), + * unless the rotation mode is {@link RotationMode#WHEEL} (positions are set absolutely each frame). + */ + public void setY(float y) { + float dy = y - this.y; + this.y = y; + + if (rotationMode != RotationMode.WHEEL) { + transformMembersY(dy); + } + } + + /** + * Adds the given amount to the group's Y position. Equivalent to {@code setY(getY() + y)}. + */ + public void changeY(float y) { + setY(this.y + y); + } + + /** + * Sets both X and Y in a single call, applying the deltas to all members in one pass + * to avoid iterating twice. + */ + public void setPosition(float x, float y) { + float dx = x - this.x; + float dy = y - this.y; + this.x = x; + this.y = y; + + if (rotationMode != RotationMode.WHEEL) { + transformMembersPosition(dx, dy); + } + } + + + /** + * Returns the current rotation mode. + */ + public RotationMode getRotationMode() { + return rotationMode; + } + + /** + * Sets the rotation mode. See {@link RotationMode} for the available options. + */ + public void setRotationMode(RotationMode rotationMode) { + this.rotationMode = rotationMode; + } + + public float getRotationRadius() { + return rotationRadius; + } + + public void setRotationRadius(float rotationRadius) { + this.rotationRadius = rotationRadius; + } + + /** + * Returns the group's rotation in degrees. + */ + public float getRotation() { + return rotation; + } + + /** + * Sets the group's rotation in degrees. The behaviour depends on {@link #rotationMode}: + *
    + *
  • {@link RotationMode#INDIVIDUAL} – the delta is applied to each sprite's + * own rotation (no positional change).
  • + *
  • {@link RotationMode#WHEEL} – the value is stored and applied during the + * next {@link #update(float)} call.
  • + *
  • {@link RotationMode#ORBIT} – each sprite's position is rotated around the + * group origin by the delta, and its individual rotation is also adjusted.
  • + *
+ */ + public void setRotation(float rotation) { + float delta = rotation - this.rotation; + this.rotation = rotation; + + switch (rotationMode) { + case INDIVIDUAL: + transformMembersIndividualRotation(delta); + break; + case ORBIT: + orbitMembersAroundCenter(delta); + break; + case WHEEL: + break; } } + /** + * Adds the given amount to the group's rotation. Equivalent to + * {@code setRotation(getRotation() + degrees)}. + */ + public void changeRotation(float degrees) { + setRotation(this.rotation + degrees); + } + + /** + * Number of members. Prefer this over {@code members.size} for consistency. + */ + public int getLength() { + return members.size; + } + + public int getMaxSize() { + return maxSize; + } + + public void setMaxSize(int maxSize) { + this.maxSize = Math.max(0, maxSize); + } + + + /** + * Adds a sprite to the group. The sprite's position is automatically offset by + * the group's current ({@link #x}, {@link #y}), matching HaxeFlixel's {@code preAdd} behaviour. + */ @Override public void add(FlixelSprite sprite) { + if (sprite == null) { + return; + } + + if (maxSize > 0 && members.size >= maxSize) { + return; + } + + preAdd(sprite); super.add(sprite); - members.add(sprite); } + /** + * Adds a sprite and returns it for chaining. + */ + public FlixelSprite addAndReturn(FlixelSprite sprite) { + add(sprite); + return sprite; + } + + /** + * Inserts a new sprite at the given index. The sprite is offset by the group's position. + */ + public void insert(int index, FlixelSprite sprite) { + if (sprite == null) { + return; + } + + if (maxSize > 0 && members.size >= maxSize) { + return; + } + + preAdd(sprite); + index = MathUtils.clamp(index, 0, members.size); + members.insert(index, sprite); + } + + /** + * Removes a sprite from the group. The group's position offset is subtracted from the + * sprite, restoring it to "local" coordinates, matching HaxeFlixel's {@code remove} behaviour. + */ @Override public void remove(FlixelSprite sprite) { + if (sprite == null) { + return; + } + super.remove(sprite); - members.removeValue(sprite, true); + sprite.setX(sprite.getX() - x); + sprite.setY(sprite.getY() - y); + } + + /** + * Replaces an existing sprite with a new one. The new sprite is offset by the group's position. + * + * @return the new sprite + */ + public FlixelSprite replace(FlixelSprite oldSprite, FlixelSprite newSprite) { + if (oldSprite == null || newSprite == null) { + return newSprite; + } + + int idx = members.indexOf(oldSprite, true); + if (idx < 0) { + add(newSprite); + return newSprite; + } + + preAdd(newSprite); + members.set(idx, newSprite); + return newSprite; + } + + /** + * Sorts members using the given comparator. + */ + public void sort(Comparator comparator) { + if (comparator == null) { + return; + } + + members.sort((a, b) -> comparator.compare((FlixelSprite) a, (FlixelSprite) b)); + } + + /** + * Returns a random member, or null if the group is empty. + */ + public FlixelSprite getRandom() { + return getRandom(0, members.size); + } + + /** + * Returns a random member from the range [startIndex, startIndex + length), or null if none. + */ + public FlixelSprite getRandom(int startIndex, int length) { + if (members.size == 0) { + return null; + } + + startIndex = MathUtils.clamp(startIndex, 0, members.size - 1); + if (length <= 0) { + length = members.size; + } + + int end = Math.min(startIndex + length, members.size); + int span = end - startIndex; + if (span <= 0) { + return null; + } + + return (FlixelSprite) members.get(startIndex + RANDOM.nextInt(span)); + } + + /** + * Applies a function to each member. + */ + public void forEach(Consumer function) { + if (function == null) { + return; + } + + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + function.accept(s); + } + } + } + + /** + * Returns the member at the given index, or null. + */ + public FlixelSprite get(int index) { + if (index < 0 || index >= members.size) { + return null; + } + + return (FlixelSprite) members.get(index); } @Override - public void clear() { - members.forEach(FlixelSprite::destroy); - members.clear(); + public void update(float elapsed) { + super.update(elapsed); + + if (rotationMode == RotationMode.WHEEL) { + applyWheelRotation(); + } } + @Override - public void destroy() { - members.forEach(FlixelSprite::destroy); + public void clear() { + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = members.get(i); + if (member != null) { + member.destroy(); + } + } + members.clear(); } + + /** + * Offsets a sprite by the group's current position before it is inserted into the group, + * matching HaxeFlixel's {@code preAdd} behaviour. + */ + private void preAdd(FlixelSprite sprite) { + sprite.setX(sprite.getX() + x); + sprite.setY(sprite.getY() + y); + } + + /** + * Moves every member's X by the given delta. + */ + private void transformMembersX(float dx) { + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + s.setX(s.getX() + dx); + } + } + } + + /** + * Moves every member's Y by the given delta. + */ + private void transformMembersY(float dy) { + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + s.setY(s.getY() + dy); + } + } + } + + /** + * Moves every member's position by the given deltas in a single pass. + */ + private void transformMembersPosition(float dx, float dy) { + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + s.setPosition(s.getX() + dx, s.getY() + dy); + } + } + } + + /** + * {@link RotationMode#INDIVIDUAL}: applies the rotation delta to each sprite's own rotation + * without changing any positions. + */ + private void transformMembersIndividualRotation(float delta) { + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + s.setRotation(s.getRotation() + delta); + } + } + } + + /** + * {@link RotationMode#ORBIT}: rotates every member's position around the group origin + * ({@link #x}, {@link #y}) by {@code angleDelta} degrees and adjusts each sprite's own + * rotation by the same amount, like a rotating camera/TV screen. + */ + private void orbitMembersAroundCenter(float angleDelta) { + float cos = MathUtils.cosDeg(angleDelta); + float sin = MathUtils.sinDeg(angleDelta); + + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s == null) { + continue; + } + + float localX = s.getX() - x; + float localY = s.getY() - y; + float rotatedX = localX * cos - localY * sin; + float rotatedY = localX * sin + localY * cos; + s.setPosition(x + rotatedX, y + rotatedY); + s.setRotation(s.getRotation() + angleDelta); + } + } + + /** + * {@link RotationMode#WHEEL}: positions and rotates each sprite consecutively around the + * group center in a radial/wheel pattern. Called each frame from {@link #update(float)}. + */ + private void applyWheelRotation() { + int n = members.size; + if (n == 0) { + return; + } + + float angleStep = 360f / n; + for (int i = 0; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s == null) { + continue; + } + + float angleDeg = rotation + angleStep * i; + float px = x + rotationRadius * MathUtils.cosDeg(angleDeg); + float py = y + rotationRadius * MathUtils.sinDeg(angleDeg); + s.setPosition(px, py); + s.setRotation(angleDeg); + } + } } diff --git a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java index 03e0778..e02411f 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java @@ -5,6 +5,7 @@ import me.stringdotjar.flixelgdx.backend.FlixelPaths; import me.stringdotjar.flixelgdx.graphics.FlixelState; import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; +import me.stringdotjar.flixelgdx.group.FlixelSpriteGroup; import me.stringdotjar.flixelgdx.input.FlixelKey; import me.stringdotjar.flixelgdx.tween.FlixelEase; import me.stringdotjar.flixelgdx.tween.FlixelTween; @@ -15,6 +16,12 @@ public class TitleState extends FlixelState { private FlixelSprite logo; + private FlixelSpriteGroup spriteGroup; + private FlixelSprite s1; + private FlixelSprite s2; + private FlixelSprite s3; + private FlixelSprite s4; + private FlixelTween tween; private MASound tickleFight; @@ -31,6 +38,26 @@ public void create() { tickleFight = Flixel.playSound("shared/sounds/tickleFight.ogg"); // Flixel.playMusic("preload/music/freakyMenu/freakyMenu.ogg", 0.5f); + spriteGroup = new FlixelSpriteGroup(0, 200f, 0f); + s1 = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset("transitionSwag/stickers-set-1/bfSticker1")); + s2 = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset("transitionSwag/stickers-set-1/bfSticker2")); + s3 = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset("transitionSwag/stickers-set-1/bfSticker3")); + s4 = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset("transitionSwag/stickers-set-1/gfSticker1")); + s1.setX(200); + s2.setX(-200); + s3.setX(200); + s4.setX(-200); + s1.setY(200); + s2.setY(200); + s3.setY(-200); + s4.setY(-200); + spriteGroup.add(s1); + spriteGroup.add(s2); + spriteGroup.add(s3); + spriteGroup.add(s4); + spriteGroup.setRotationMode(FlixelSpriteGroup.RotationMode.INDIVIDUAL); + add(spriteGroup); + FlixelTweenSettings settings = new FlixelTweenSettings() .addGoal("x", 600) .addGoal("y", 40) @@ -50,17 +77,65 @@ public void update(float elapsed) { super.update(elapsed); float speed = 500 * elapsed; + // if (Flixel.keyPressed(FlixelKey.W)) { + // logo.changeY(speed); + // } + // if (Flixel.keyPressed(FlixelKey.S)) { + // logo.changeY(-speed); + // } + // if (Flixel.keyPressed(FlixelKey.A)) { + // logo.changeX(-speed); + // } + // if (Flixel.keyPressed(FlixelKey.D)) { + // logo.changeX(speed); + // } if (Flixel.keyPressed(FlixelKey.W)) { - logo.changeY(speed); + spriteGroup.changeY(speed); } if (Flixel.keyPressed(FlixelKey.S)) { - logo.changeY(-speed); + spriteGroup.changeY(-speed); } if (Flixel.keyPressed(FlixelKey.A)) { - logo.changeX(-speed); + spriteGroup.changeX(-speed); } if (Flixel.keyPressed(FlixelKey.D)) { - logo.changeX(speed); + spriteGroup.changeX(speed); + } + + if (Flixel.keyPressed(FlixelKey.I)) { + s1.changeY(speed); + } + if (Flixel.keyPressed(FlixelKey.K)) { + s1.changeY(-speed); + } + if (Flixel.keyPressed(FlixelKey.J)) { + s1.changeX(-speed); + } + if (Flixel.keyPressed(FlixelKey.L)) { + s1.changeX(speed); + } + if (Flixel.keyPressed(FlixelKey.U)) { + s1.changeRotation(speed); + } + if (Flixel.keyPressed(FlixelKey.O)) { + s1.changeRotation(-speed); + } + + if (Flixel.keyJustPressed(FlixelKey.NUM_1)) { + spriteGroup.setRotationMode(FlixelSpriteGroup.RotationMode.INDIVIDUAL); + } + if (Flixel.keyJustPressed(FlixelKey.NUM_2)) { + spriteGroup.setRotationMode(FlixelSpriteGroup.RotationMode.WHEEL); + } + if (Flixel.keyJustPressed(FlixelKey.NUM_3)) { + spriteGroup.setRotationMode(FlixelSpriteGroup.RotationMode.ORBIT); + } + + if (Flixel.keyPressed(FlixelKey.LEFT)) { + spriteGroup.changeRotation(speed); + } + if (Flixel.keyPressed(FlixelKey.RIGHT)) { + spriteGroup.changeRotation(-speed); } if (Flixel.keyJustPressed(FlixelKey.SPACE)) { From 4007606ded0f06130e847749bdf27ab79f351a56 Mon Sep 17 00:00:00 2001 From: String Date: Mon, 16 Feb 2026 16:38:10 -0600 Subject: [PATCH 9/9] Move RotationMode enum to bottom for better readability --- .../flixelgdx/group/FlixelSpriteGroup.java | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java index 146a32c..324eb5a 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java @@ -29,34 +29,6 @@ */ public class FlixelSpriteGroup extends FlixelGroup { - /** - * Controls how the group's {@link #rotation} affects member sprites. - */ - public enum RotationMode { - - /** - * Rotation is applied to each sprite's individual rotation by the delta. - * No positional changes occur from rotation. - */ - INDIVIDUAL, - - /** - * Sprites are arranged in a radial/wheel pattern around the group center - * ({@link #x}, {@link #y}). Each sprite is positioned at {@link #rotationRadius} from the - * center, spaced evenly around 360 degrees. Positions and rotations are set absolutely - * each frame in {@link #update(float)}. - */ - WHEEL, - - /** - * All sprites orbit around the group origin ({@link #x}, {@link #y}) as a rigid body, - * like a rotating camera or TV screen. When rotation changes, each sprite's position is - * rotated around the center by the delta and its individual rotation is also adjusted - * by the same amount. - */ - ORBIT - } - /** * Maximum number of members allowed. When {@code 0}, the group can grow without limit (default). * When {@code > 0}, {@link #add(FlixelSprite)} will not add if at capacity. @@ -549,4 +521,35 @@ private void applyWheelRotation() { s.setRotation(angleDeg); } } + + /** + * Controls how a {@link FlixelSpriteGroup}'s {@link #rotation} affects its members. + */ + public enum RotationMode { + + /** + * Rotation is applied to each sprite's individual rotation by the delta. + * No positional changes occur from rotation. + */ + INDIVIDUAL, + + /** + * Sprites are arranged in a radial/wheel pattern around the group center + * ({@link #x}, {@link #y}). Each sprite is positioned at {@link #rotationRadius} from the + * center, spaced evenly around 360 degrees. Positions and rotations are set absolutely + * each frame in {@link #update(float)}. + *

+ * Note that you cannot change the rotation of any specific sprite of the group when in this mode, as it + * is locked to the group's rotation. + */ + WHEEL, + + /** + * All sprites orbit around the group origin ({@link #x}, {@link #y}) as a rigid body, + * like a rotating camera or TV screen. When rotation changes, each sprite's position is + * rotated around the center by the delta and its individual rotation is also adjusted + * by the same amount. + */ + ORBIT + } }