From dea7e942a395bb9ee27248e95744a83e750e7124 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 5 Jul 2014 21:00:52 -0400 Subject: [PATCH] Created a splash screen state for loading resources. - Game now boots to splash screen, which displays parser progress. - Added option "LoadVerbose" to disable rendering the progress text. - Main Menu backgrounds now fade in regardless of DynamicBackground setting (as a transition from the splash screen). Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Opsu.java | 29 +++-- src/itdelatrisu/opsu/OsuParser.java | 47 +++++++- src/itdelatrisu/opsu/objects/Slider.java | 3 +- src/itdelatrisu/opsu/states/MainMenu.java | 29 ++--- src/itdelatrisu/opsu/states/Options.java | 31 +++++- src/itdelatrisu/opsu/states/SongMenu.java | 9 +- src/itdelatrisu/opsu/states/Splash.java | 125 ++++++++++++++++++++++ 7 files changed, 230 insertions(+), 43 deletions(-) create mode 100644 src/itdelatrisu/opsu/states/Splash.java diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 0aedf827..f835d0d7 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -25,6 +25,7 @@ import itdelatrisu.opsu.states.MainMenu; import itdelatrisu.opsu.states.MainMenuExit; import itdelatrisu.opsu.states.Options; import itdelatrisu.opsu.states.SongMenu; +import itdelatrisu.opsu.states.Splash; import java.io.File; import java.io.FileNotFoundException; @@ -61,6 +62,7 @@ public class Opsu extends StateBasedGame { * Game states. */ public static final int + STATE_SPLASH = 0, STATE_MAINMENU = 1, STATE_MAINMENUEXIT = 2, STATE_SONGMENU = 3, @@ -80,6 +82,7 @@ public class Opsu extends StateBasedGame { @Override public void initStatesList(GameContainer container) throws SlickException { + addState(new Splash(STATE_SPLASH)); addState(new MainMenu(STATE_MAINMENU)); addState(new MainMenuExit(STATE_MAINMENUEXIT)); addState(new SongMenu(STATE_SONGMENU)); @@ -125,10 +128,17 @@ public class Opsu extends StateBasedGame { ResourceLoader.addResourceLocation(new FileSystemLocation(new File("."))); ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/"))); + // clear the cache + if (!Options.TMP_DIR.mkdir()) { + for (File tmp : Options.TMP_DIR.listFiles()) + tmp.delete(); + } + Options.TMP_DIR.deleteOnExit(); + // start the game - Opsu osuGame = new Opsu("opsu!"); + Opsu opsu = new Opsu("opsu!"); try { - AppGameContainer app = new AppGameContainer(osuGame); + AppGameContainer app = new AppGameContainer(opsu); // basic game settings int[] containerSize = Options.getContainerSize(); @@ -136,21 +146,6 @@ public class Opsu extends StateBasedGame { String[] icons = { "icon16.png", "icon32.png" }; app.setIcons(icons); - // parse song directory - OsuParser.parseAllFiles(Options.getBeatmapDir(), - app.getWidth(), app.getHeight() - ); - - // clear the cache - if (!Options.TMP_DIR.mkdir()) { - for (File tmp : Options.TMP_DIR.listFiles()) - tmp.delete(); - } - Options.TMP_DIR.deleteOnExit(); - - // load sounds - SoundController.init(); - app.start(); } catch (SlickException e) { Log.error("Error while creating game container.", e); diff --git a/src/itdelatrisu/opsu/OsuParser.java b/src/itdelatrisu/opsu/OsuParser.java index fc01f17f..791ae43e 100644 --- a/src/itdelatrisu/opsu/OsuParser.java +++ b/src/itdelatrisu/opsu/OsuParser.java @@ -34,6 +34,21 @@ import org.newdawn.slick.util.Log; * Parser for OSU files. */ public class OsuParser { + /** + * The current file being parsed. + */ + private static File currentFile; + + /** + * The current directory number while parsing. + */ + private static int currentDirectoryIndex = -1; + + /** + * The total number of directories to parse. + */ + private static int totalDirectories = -1; + /** * The x and y multipliers for hit object coordinates. */ @@ -62,7 +77,13 @@ public class OsuParser { xOffset = width / 5; yOffset = height / 5; - for (File folder : root.listFiles()) { + // progress tracking + File[] folders = root.listFiles(); + currentDirectoryIndex = 0; + totalDirectories = folders.length; + + for (File folder : folders) { + currentDirectoryIndex++; if (!folder.isDirectory()) continue; File[] files = folder.listFiles(new FilenameFilter() { @@ -77,6 +98,8 @@ public class OsuParser { // create a new group entry ArrayList osuFiles = new ArrayList(); for (File file : files) { + currentFile = file; + // Parse hit objects only when needed to save time/memory. // Change boolean to 'true' to parse them immediately. parseFile(file, osuFiles, false); @@ -86,6 +109,10 @@ public class OsuParser { Opsu.groups.addSongGroup(osuFiles); } } + + currentFile = null; + currentDirectoryIndex = -1; + totalDirectories = -1; } /** @@ -554,4 +581,22 @@ public class OsuParser { int i = file.lastIndexOf('.'); return (i != -1) ? file.substring(i + 1).toLowerCase() : ""; } + + /** + * Returns the name of the current file being parsed, or null if none. + */ + public static String getCurrentFileName() { + return (currentFile != null) ? currentFile.getName() : null; + } + + /** + * Returns the progress of file parsing, or -1 if not parsing. + * @return the completion percent [0, 100] or -1 + */ + public static int getParserProgress() { + if (currentDirectoryIndex == -1 || totalDirectories == -1) + return -1; + + return currentDirectoryIndex * 100 / totalDirectories; + } } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index c4dd9ea2..87cc64ab 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -521,7 +521,6 @@ public class Slider { } } - // end of slider if (overlap || trackPosition > hitObject.time + sliderTimeTotal) { tickIntervals++; @@ -537,7 +536,7 @@ public class Slider { if (distance < followCircleRadius) ticksHit++; } - + // calculate and send slider result hitResult(); return true; diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index ca9d65bd..63fc7b9a 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -96,9 +96,9 @@ public class MainMenu extends BasicGameState { private Image backgroundImage; /** - * Alpha level for fade-in effect for dynamic backgrounds. + * Background alpha level (for fade-in effect). */ - private float dynamicBackgroundAlpha = 0f; + private float bgAlpha = 0f; // game-related variables private StateBasedGame game; @@ -111,8 +111,6 @@ public class MainMenu extends BasicGameState { @Override public void init(GameContainer container, StateBasedGame game) throws SlickException { - Utils.init(container, game); - this.game = game; osuStartTime = System.currentTimeMillis(); @@ -165,11 +163,12 @@ public class MainMenu extends BasicGameState { // draw background OsuFile osu = MusicController.getOsuFile(); if (Options.isDynamicBackgroundEnabled() && - osu != null && osu.drawBG(width, height, dynamicBackgroundAlpha)) + osu != null && osu.drawBG(width, height, bgAlpha)) ; - else if (backgroundImage != null) + else if (backgroundImage != null) { + backgroundImage.setAlpha(bgAlpha); backgroundImage.draw(); - else + } else g.setBackground(Utils.COLOR_BLUE_BACKGROUND); // draw buttons @@ -224,11 +223,11 @@ public class MainMenu extends BasicGameState { @Override public void update(GameContainer container, StateBasedGame game, int delta) throws SlickException { - // dynamic backgrounds - if (Options.isDynamicBackgroundEnabled() && dynamicBackgroundAlpha < 0.9f) { - dynamicBackgroundAlpha += delta / 1000f; - if (dynamicBackgroundAlpha > 0.9f) - dynamicBackgroundAlpha = 0.9f; + // fade in background + if (bgAlpha < 0.9f) { + bgAlpha += delta / 1000f; + if (bgAlpha > 0.9f) + bgAlpha = 0.9f; } // buttons @@ -296,12 +295,14 @@ public class MainMenu extends BasicGameState { OsuGroupNode node = menu.setFocus(Opsu.groups.getRandomNode(), -1, true); if (node != null) previous.add(node.index); - dynamicBackgroundAlpha = 0f; + if (Options.isDynamicBackgroundEnabled()) + bgAlpha = 0f; } else if (musicPrevious.contains(x, y)) { if (!previous.isEmpty()) { SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); menu.setFocus(Opsu.groups.getBaseNode(previous.pop()), -1, true); - dynamicBackgroundAlpha = 0f; + if (Options.isDynamicBackgroundEnabled()) + bgAlpha = 0f; } else MusicController.setPosition(0); } diff --git a/src/itdelatrisu/opsu/states/Options.java b/src/itdelatrisu/opsu/states/Options.java index a5d324ae..e412ed42 100644 --- a/src/itdelatrisu/opsu/states/Options.java +++ b/src/itdelatrisu/opsu/states/Options.java @@ -134,7 +134,8 @@ public class Options extends BasicGameState { FIXED_CS, FIXED_HP, FIXED_AR, - FIXED_OD; + FIXED_OD, + LOAD_VERBOSE; }; /** @@ -177,7 +178,8 @@ public class Options extends BasicGameState { GameOption.SHOW_FPS, GameOption.SCREENSHOT_FORMAT, GameOption.NEW_CURSOR, - GameOption.DYNAMIC_BACKGROUND + GameOption.DYNAMIC_BACKGROUND, + GameOption.LOAD_VERBOSE }; /** @@ -341,6 +343,11 @@ public class Options extends BasicGameState { fixedCS = 0f, fixedHP = 0f, fixedAR = 0f, fixedOD = 0f; + /** + * Whether or not to render loading text in the splash screen. + */ + private static boolean loadVerbose = true; + /** * Game option coordinate modifiers (for drawing). */ @@ -584,6 +591,9 @@ public class Options extends BasicGameState { case IGNORE_BEATMAP_SKINS: ignoreBeatmapSkins = !ignoreBeatmapSkins; break; + case LOAD_VERBOSE: + loadVerbose = !loadVerbose; + break; default: break; } @@ -750,6 +760,12 @@ public class Options extends BasicGameState { "The song background will be used as the main menu background." ); break; + case LOAD_VERBOSE: + drawOption(pos, "Display Loading Text", + loadVerbose ? "Yes" : "No", + "Display loading progress in the splash screen." + ); + break; case MUSIC_VOLUME: drawOption(pos, "Music Volume", String.format("%d%%", musicVolume), @@ -1048,6 +1064,12 @@ public class Options extends BasicGameState { */ public static float getFixedOD() { return fixedOD; } + /** + * Returns whether or not to render loading text in the splash screen. + * @return true if enabled + */ + public static boolean isLoadVerbose() { return loadVerbose; } + /** * Returns the current beatmap directory. * If invalid, this will attempt to search for the directory, @@ -1144,6 +1166,9 @@ public class Options extends BasicGameState { case "DynamicBackground": dynamicBackground = Boolean.parseBoolean(value); break; + case "LoadVerbose": + loadVerbose = Boolean.parseBoolean(value); + break; case "VolumeMusic": i = Integer.parseInt(value); if (i >= 0 && i <= 100) @@ -1243,6 +1268,8 @@ public class Options extends BasicGameState { writer.newLine(); writer.write(String.format("DynamicBackground = %b", dynamicBackground)); writer.newLine(); + writer.write(String.format("LoadVerbose = %b", loadVerbose)); + writer.newLine(); writer.write(String.format("VolumeMusic = %d", musicVolume)); writer.newLine(); writer.write(String.format("VolumeEffect = %d", effectVolume)); diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index db123aee..cda4068b 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -85,7 +85,7 @@ public class SongMenu extends BasicGameState { /** * The current sort order (SORT_* constant). */ - private byte currentSort; + private byte currentSort = OsuGroupList.SORT_TITLE; /** * The options button (to enter the "Game Options" menu). @@ -135,11 +135,6 @@ public class SongMenu extends BasicGameState { this.game = game; this.input = container.getInput(); - // initialize song list - currentSort = OsuGroupList.SORT_TITLE; - Opsu.groups.init(currentSort); - setFocus(Opsu.groups.getRandomNode(), -1, true); - int width = container.getWidth(); int height = container.getHeight(); @@ -414,7 +409,7 @@ public class SongMenu extends BasicGameState { startNode = Opsu.groups.getBaseNode(startNode.index); } setFocus(node, -1, false); - + break; } } diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java new file mode 100644 index 00000000..08a3c1e5 --- /dev/null +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -0,0 +1,125 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.states; + +import itdelatrisu.opsu.Opsu; +import itdelatrisu.opsu.OsuGroupList; +import itdelatrisu.opsu.OsuParser; +import itdelatrisu.opsu.SoundController; +import itdelatrisu.opsu.Utils; + +import org.newdawn.slick.Color; +import org.newdawn.slick.GameContainer; +import org.newdawn.slick.Graphics; +import org.newdawn.slick.Image; +import org.newdawn.slick.SlickException; +import org.newdawn.slick.state.BasicGameState; +import org.newdawn.slick.state.StateBasedGame; + +public class Splash extends BasicGameState { + /** + * Logo image. + */ + private Image logo; + + /** + * Whether or not loading has completed. + */ + private boolean finished = false; + + // game-related variables + private int state; + + public Splash(int state) { + this.state = state; + } + + @Override + public void init(GameContainer container, StateBasedGame game) + throws SlickException { + final int width = container.getWidth(); + final int height = container.getHeight(); + + logo = new Image("logo.png"); + logo = logo.getScaledCopy((height / 1.2f) / logo.getHeight()); + logo.setAlpha(0f); + + // load Utils class first (needed in other 'init' methods) + Utils.init(container, game); + + // load other resources in a new thread + final SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); + new Thread() { + @Override + public void run() { + // parse song directory + OsuParser.parseAllFiles(Options.getBeatmapDir(), width, height); + + // initialize song list + Opsu.groups.init(OsuGroupList.SORT_TITLE); + menu.setFocus(Opsu.groups.getRandomNode(), -1, true); + + // load sounds + SoundController.init(); + + finished = true; + } + }.start(); + } + + @Override + public void render(GameContainer container, StateBasedGame game, Graphics g) + throws SlickException { + g.setBackground(Color.black); + + int width = container.getWidth(); + int height = container.getHeight(); + + logo.drawCentered(width / 2, height / 2); + + // progress tracking + String currentFile = OsuParser.getCurrentFileName(); + if (currentFile != null && Options.isLoadVerbose()) { + g.setColor(Color.white); + g.setFont(Utils.FONT_MEDIUM); + int lineHeight = Utils.FONT_MEDIUM.getLineHeight(); + g.drawString( + String.format("Loading... (%s%%)", OsuParser.getParserProgress()), + 25, height - 25 - (lineHeight * 2) + ); + g.drawString(currentFile, 25, height - 25 - lineHeight); + } + } + + @Override + public void update(GameContainer container, StateBasedGame game, int delta) + throws SlickException { + // fade in logo + float alpha = logo.getAlpha(); + if (alpha < 1f) + logo.setAlpha(alpha + (delta / 400f)); + + // change states when loading complete + if (finished && alpha >= 1f) + game.enterState(Opsu.STATE_MAINMENU); + } + + @Override + public int getID() { return state; } +}