Added star fountains to main menu.

- Added MusicController methods to get measure progress (similar to beat progress).
- Workaround for inaccurate track positions after looping by not looping.
- Make sure star duration is positive.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2016-12-23 04:03:32 -05:00
parent 106891a259
commit 5704b8aa10
5 changed files with 177 additions and 16 deletions

View File

@ -139,7 +139,7 @@ public class Options {
private static String themeString = "theme.mp3,Rainbows,Kevin MacLeod,219350"; private static String themeString = "theme.mp3,Rainbows,Kevin MacLeod,219350";
/** The theme song timing point string (for computing beats to pulse the logo) . */ /** The theme song timing point string (for computing beats to pulse the logo) . */
private static String themeTimingPoint = "-1100,545.454545454545,4,1,0,100,0,0"; private static String themeTimingPoint = "-3300,545.454545454545,4,1,0,100,0,0";
/** /**
* Returns whether the XDG flag in the manifest (if any) is set to "true". * Returns whether the XDG flag in the manifest (if any) is set to "true".

View File

@ -195,13 +195,54 @@ public class MusicController {
/** /**
* Gets the progress of the current beat. * Gets the progress of the current beat.
* @return a beat progress value [0,1) where 0 marks the current beat and * @return a beat progress value [0,1) where 0 marks the current beat and
* 1 marks the next beat, or {@code null} if no beat information * 1 marks the next beat, or {@code null} if no timing information
* is available (e.g. music paused, no timing points) * is available (e.g. music paused, no timing points)
*/ */
public static Float getBeatProgress() { public static Float getBeatProgress() {
if (!updateTimingPoint())
return null;
// calculate beat progress
int trackPosition = Math.max(0, getPosition());
double beatLength = lastTimingPoint.getBeatLength() * 100.0;
int beatTime = lastTimingPoint.getTime();
return (float) ((((trackPosition - beatTime) * 100.0) % beatLength) / beatLength);
}
/**
* Gets the progress of the current measure.
* @return a measure progress value [0,1) where 0 marks the start of the measure and
* 1 marks the start of the next measure, or {@code null} if no timing information
* is available (e.g. music paused, no timing points)
*/
public static Float getMeasureProgress() { return getMeasureProgress(1); }
/**
* Gets the progress of the current measure.
* @param k the meter multiplier
* @return a measure progress value [0,1) where 0 marks the start of the measure and
* 1 marks the start of the next measure, or {@code null} if no timing information
* is available (e.g. music paused, no timing points)
*/
public static Float getMeasureProgress(int k) {
if (!updateTimingPoint())
return null;
// calculate measure progress
int trackPosition = Math.max(0, getPosition());
double measureLength = lastTimingPoint.getBeatLength() * lastTimingPoint.getMeter() * k * 100.0;
int beatTime = lastTimingPoint.getTime();
return (float) ((((trackPosition - beatTime) * 100.0) % measureLength) / measureLength);
}
/**
* Updates the timing point information for the current track position.
* @return {@code false} if timing point information is not available, {@code true} otherwise
*/
private static boolean updateTimingPoint() {
Beatmap map = getBeatmap(); Beatmap map = getBeatmap();
if (!isPlaying() || map == null || map.timingPoints == null || map.timingPoints.isEmpty()) if (!isPlaying() || map == null || map.timingPoints == null || map.timingPoints.isEmpty())
return null; return false;
// initialization // initialization
if (timingPointIndex == 0 && lastTimingPoint == null && !map.timingPoints.isEmpty()) { if (timingPointIndex == 0 && lastTimingPoint == null && !map.timingPoints.isEmpty()) {
@ -221,12 +262,9 @@ public class MusicController {
lastTimingPoint = timingPoint; lastTimingPoint = timingPoint;
} }
if (lastTimingPoint == null) if (lastTimingPoint == null)
return null; // no timing info return false; // no timing info
// calculate beat progress return true;
double beatLength = lastTimingPoint.getBeatLength() * 100.0;
int beatTime = lastTimingPoint.getTime();
return (float) ((((trackPosition - beatTime) * 100.0) % beatLength) / beatLength);
} }
/** /**
@ -401,7 +439,7 @@ public class MusicController {
public static void playThemeSong() { public static void playThemeSong() {
Beatmap beatmap = Options.getThemeBeatmap(); Beatmap beatmap = Options.getThemeBeatmap();
if (beatmap != null) { if (beatmap != null) {
play(beatmap, true, false); play(beatmap, false, false);
themePlaying = true; themePlaying = true;
} }
} }

View File

@ -35,6 +35,7 @@ import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.MenuButton.Expand; import itdelatrisu.opsu.ui.MenuButton.Expand;
import itdelatrisu.opsu.ui.StarFountain;
import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue; import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation; import itdelatrisu.opsu.ui.animations.AnimationEquation;
@ -53,8 +54,8 @@ import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException; import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState; import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame; import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.state.transition.FadeInTransition;
import org.newdawn.slick.state.transition.EasedFadeOutTransition; import org.newdawn.slick.state.transition.EasedFadeOutTransition;
import org.newdawn.slick.state.transition.FadeInTransition;
/** /**
* "Main Menu" state. * "Main Menu" state.
@ -116,6 +117,12 @@ public class MainMenu extends BasicGameState {
/** Music position bar coordinates and dimensions. */ /** Music position bar coordinates and dimensions. */
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight; private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
/** Last measure progress value. */
private float lastMeasureProgress = 0f;
/** The star fountain. */
private StarFountain starFountain;
// game-related variables // game-related variables
private GameContainer container; private GameContainer container;
private StateBasedGame game; private StateBasedGame game;
@ -214,6 +221,9 @@ public class MainMenu extends BasicGameState {
restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR); restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR);
restartButton.setHoverRotate(360); restartButton.setHoverRotate(360);
// initialize star fountain
starFountain = new StarFountain(width, height);
// logo animations // logo animations
float centerOffsetX = width / 5f; float centerOffsetX = width / 5f;
logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD); logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD);
@ -248,6 +258,9 @@ public class MainMenu extends BasicGameState {
g.fillRect(0, height * 8 / 9f, width, height / 9f); g.fillRect(0, height * 8 / 9f, width, height / 9f);
Colors.BLACK_ALPHA.a = oldAlpha; Colors.BLACK_ALPHA.a = oldAlpha;
// draw star fountain
starFountain.draw();
// draw downloads button // draw downloads button
downloadsButton.draw(); downloadsButton.draw();
@ -329,7 +342,7 @@ public class MainMenu extends BasicGameState {
throws SlickException { throws SlickException {
UI.update(delta); UI.update(delta);
if (MusicController.trackEnded()) if (MusicController.trackEnded())
nextTrack(); // end of track: go to next track nextTrack(false); // end of track: go to next track
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
logo.hoverUpdate(delta, mouseX, mouseY, 0.25f); logo.hoverUpdate(delta, mouseX, mouseY, 0.25f);
playButton.hoverUpdate(delta, mouseX, mouseY, 0.25f); playButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
@ -349,6 +362,7 @@ public class MainMenu extends BasicGameState {
noHoverUpdate |= contains; noHoverUpdate |= contains;
musicNext.hoverUpdate(delta, !noHoverUpdate && musicNext.contains(mouseX, mouseY)); musicNext.hoverUpdate(delta, !noHoverUpdate && musicNext.contains(mouseX, mouseY));
musicPrevious.hoverUpdate(delta, !noHoverUpdate && musicPrevious.contains(mouseX, mouseY)); musicPrevious.hoverUpdate(delta, !noHoverUpdate && musicPrevious.contains(mouseX, mouseY));
starFountain.update(delta);
// window focus change: increase/decrease theme song volume // window focus change: increase/decrease theme song volume
if (MusicController.isThemePlaying() && if (MusicController.isThemePlaying() &&
@ -360,6 +374,14 @@ public class MainMenu extends BasicGameState {
if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading())) if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
bgAlpha.update(delta); bgAlpha.update(delta);
// check measure progress
Float measureProgress = MusicController.getMeasureProgress(2);
if (measureProgress != null) {
if (measureProgress < lastMeasureProgress)
starFountain.burst(true);
lastMeasureProgress = measureProgress;
}
// buttons // buttons
int centerX = container.getWidth() / 2; int centerX = container.getWidth() / 2;
float currentLogoButtonAlpha; float currentLogoButtonAlpha;
@ -432,6 +454,10 @@ public class MainMenu extends BasicGameState {
} }
} }
// reset measure info
lastMeasureProgress = 0f;
starFountain.clear();
// reset button hover states if mouse is not currently hovering over the button // reset button hover states if mouse is not currently hovering over the button
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
if (!logo.contains(mouseX, mouseY, 0.25f)) if (!logo.contains(mouseX, mouseY, 0.25f))
@ -489,7 +515,7 @@ public class MainMenu extends BasicGameState {
} }
return; return;
} else if (musicNext.contains(x, y)) { } else if (musicNext.contains(x, y)) {
nextTrack(); nextTrack(true);
UI.sendBarNotification(">> Next"); UI.sendBarNotification(">> Next");
return; return;
} else if (musicPrevious.contains(x, y)) { } else if (musicPrevious.contains(x, y)) {
@ -598,7 +624,7 @@ public class MainMenu extends BasicGameState {
game.enterState(Opsu.STATE_DOWNLOADSMENU, new EasedFadeOutTransition(), new FadeInTransition()); game.enterState(Opsu.STATE_DOWNLOADSMENU, new EasedFadeOutTransition(), new FadeInTransition());
break; break;
case Input.KEY_R: case Input.KEY_R:
nextTrack(); nextTrack(true);
break; break;
case Input.KEY_UP: case Input.KEY_UP:
UI.changeVolume(1); UI.changeVolume(1);
@ -656,9 +682,16 @@ public class MainMenu extends BasicGameState {
/** /**
* Plays the next track, and adds the previous one to the stack. * Plays the next track, and adds the previous one to the stack.
* @param user {@code true} if this was user-initiated, false otherwise (track end)
*/ */
private void nextTrack() { private void nextTrack(boolean user) {
boolean isTheme = MusicController.isThemePlaying(); boolean isTheme = MusicController.isThemePlaying();
if (isTheme && !user) {
// theme was playing, restart
// NOTE: not looping due to inaccurate track positions after loop
MusicController.playAt(0, false);
return;
}
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
BeatmapSetNode node = menu.setFocus(BeatmapSetList.get().getRandomNode(), -1, true, false); BeatmapSetNode node = menu.setFocus(BeatmapSetList.get().getRandomNode(), -1, true, false);
boolean sameAudio = false; boolean sameAudio = false;

View File

@ -0,0 +1,85 @@
package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Image;
/**
* Star fountain consisting of two star streams.
*/
public class StarFountain {
/** The (approximate) number of stars in each burst. */
private static final int BURST_SIZE = 80;
/** Star streams. */
private final StarStream left, right;
/** Burst progress. */
private final AnimatedValue burstProgress = new AnimatedValue(800, 0, 1, AnimationEquation.LINEAR);
/**
* Initializes the star fountain.
* @param containerWidth the container width
* @param containerHeight the container height
*/
public StarFountain(int containerWidth, int containerHeight) {
Image img = GameImage.STAR2.getImage();
float xDir = containerWidth * 0.4f, yDir = containerHeight * 0.75f;
this.left = new StarStream(-img.getWidth(), containerHeight, xDir, -yDir, 0);
this.right = new StarStream(containerWidth, containerHeight, -xDir, -yDir, 0);
setStreamProperties(left);
setStreamProperties(right);
}
/**
* Sets attributes for the given star stream.
*/
private void setStreamProperties(StarStream stream) {
stream.setDirectionSpread(60f);
stream.setDurationSpread(1100, 200);
}
/**
* Draws the star fountain.
*/
public void draw() {
left.draw();
right.draw();
}
/**
* Updates the stars in the fountain by a delta interval.
* @param delta the delta interval since the last call
*/
public void update(int delta) {
left.update(delta);
right.update(delta);
if (burstProgress.update(delta)) {
int size = Math.round((float) delta / burstProgress.getDuration() * BURST_SIZE);
left.burst(size);
right.burst(size);
}
}
/**
* Creates a burst of stars to be processed during the next {@link #update(int)} call.
* @param wait if {@code true}, will not burst if a previous burst is in progress
*/
public void burst(boolean wait) {
if (wait && (burstProgress.getTime() < burstProgress.getDuration() || !left.isEmpty() || !right.isEmpty()))
return;
burstProgress.setTime(0);
}
/**
* Clears the stars currently in the fountain.
*/
public void clear() {
left.clear();
right.clear();
burstProgress.setTime(burstProgress.getDuration());
}
}

View File

@ -32,7 +32,7 @@ import java.util.Random;
import org.newdawn.slick.Image; import org.newdawn.slick.Image;
/** /**
* Horizontal star stream. * Star stream.
*/ */
public class StarStream { public class StarStream {
/** The origin of the star stream. */ /** The origin of the star stream. */
@ -196,7 +196,7 @@ public class StarStream {
Vec2f offset = position.cpy().add(direction.cpy().nor().normalize().scale((float) getGaussian(0, positionSpread))); Vec2f offset = position.cpy().add(direction.cpy().nor().normalize().scale((float) getGaussian(0, positionSpread)));
Vec2f dir = direction.cpy().scale(distanceRatio).add((float) getGaussian(0, directionSpread), (float) getGaussian(0, directionSpread)); Vec2f dir = direction.cpy().scale(distanceRatio).add((float) getGaussian(0, directionSpread), (float) getGaussian(0, directionSpread));
int angle = (int) getGaussian(0, 22.5); int angle = (int) getGaussian(0, 22.5);
int duration = (int) (distanceRatio * getGaussian(durationBase, durationSpread)); int duration = Math.max(0, (int) (distanceRatio * getGaussian(durationBase, durationSpread)));
AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD; AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD;
return new Star(offset, dir, angle, duration, eqn); return new Star(offset, dir, angle, duration, eqn);
@ -216,6 +216,11 @@ public class StarStream {
*/ */
public void clear() { stars.clear(); } public void clear() { stars.clear(); }
/**
* Returns whether there are any stars currently in this stream.
*/
public boolean isEmpty() { return stars.isEmpty(); }
/** /**
* Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value * Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value
* with the given mean and standard deviation. * with the given mean and standard deviation.