Update replay frames in a new thread.

This drops less frames, but is still pretty bad.  See #42.
- Changed some LinkedList classes to LinkedBlockingDeques and added some synchronized methods.
- Slight modifications to OpenALStreamPlayer (may or may not be slightly more accurate).

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2015-03-10 00:48:04 -04:00
parent 39caf30770
commit 1f8c150e6c
3 changed files with 93 additions and 41 deletions

View File

@ -26,6 +26,7 @@ import itdelatrisu.opsu.audio.SoundEffect;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.concurrent.LinkedBlockingDeque;
import org.newdawn.slick.Animation; import org.newdawn.slick.Animation;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
@ -165,7 +166,7 @@ public class GameData {
private int[] hitResultOffset; private int[] hitResultOffset;
/** List of hit result objects associated with hit objects. */ /** List of hit result objects associated with hit objects. */
private LinkedList<OsuHitObjectResult> hitResultList; private LinkedBlockingDeque<OsuHitObjectResult> hitResultList;
/** /**
* Class to store hit error information. * Class to store hit error information.
@ -198,7 +199,7 @@ public class GameData {
} }
/** List containing recent hit error information. */ /** List containing recent hit error information. */
private LinkedList<HitErrorInfo> hitErrorList; private LinkedBlockingDeque<HitErrorInfo> hitErrorList;
/** /**
* Hit result helper class. * Hit result helper class.
@ -332,8 +333,8 @@ public class GameData {
health = 100f; health = 100f;
healthDisplay = 100f; healthDisplay = 100f;
hitResultCount = new int[HIT_MAX]; hitResultCount = new int[HIT_MAX];
hitResultList = new LinkedList<OsuHitObjectResult>(); hitResultList = new LinkedBlockingDeque<OsuHitObjectResult>();
hitErrorList = new LinkedList<HitErrorInfo>(); hitErrorList = new LinkedBlockingDeque<HitErrorInfo>();
fullObjectCount = 0; fullObjectCount = 0;
combo = 0; combo = 0;
comboMax = 0; comboMax = 0;

View File

@ -148,6 +148,9 @@ public class Game extends BasicGameState {
/** Number of retries. */ /** Number of retries. */
private int retries = 0; private int retries = 0;
/** Whether or not this game is a replay. */
private boolean isReplay = false;
/** The replay, if any. */ /** The replay, if any. */
private Replay replay; private Replay replay;
@ -163,6 +166,12 @@ public class Game extends BasicGameState {
/** The replay skip time, or -1 if none. */ /** The replay skip time, or -1 if none. */
private int replaySkipTime = -1; private int replaySkipTime = -1;
/** The thread updating the replay frames. */
private Thread replayThread;
/** Whether or not the replay thread should continue running. */
private boolean replayThreadRunning;
/** The previous game mod state (before the replay). */ /** The previous game mod state (before the replay). */
private int previousMods = 0; private int previousMods = 0;
@ -385,7 +394,7 @@ public class Game extends BasicGameState {
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY); cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
} }
if (replay == null) if (!isReplay)
UI.draw(g); UI.draw(g);
else else
UI.draw(g, replayX, replayY, replayKeyPressed); UI.draw(g, replayX, replayY, replayKeyPressed);
@ -396,7 +405,7 @@ public class Game extends BasicGameState {
throws SlickException { throws SlickException {
UI.update(delta); UI.update(delta);
int mouseX, mouseY; int mouseX, mouseY;
if (replay == null) { if (!isReplay) {
mouseX = input.getMouseX(); mouseX = input.getMouseX();
mouseY = input.getMouseY(); mouseY = input.getMouseY();
} else { } else {
@ -460,7 +469,7 @@ public class Game extends BasicGameState {
else { // go to ranking screen else { // go to ranking screen
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data); ((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
ScoreData score = data.getScoreData(osu); ScoreData score = data.getScoreData(osu);
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && replay == null) if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay)
ScoreDB.addScore(score); ScoreDB.addScore(score);
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} }
@ -484,29 +493,12 @@ public class Game extends BasicGameState {
} }
// replays // replays
if (replay != null) { if (isReplay) {
// skip intro // skip intro
if (replaySkipTime > 0 && trackPosition > replaySkipTime) { if (replaySkipTime > 0 && trackPosition > replaySkipTime) {
skipIntro(); skipIntro();
replaySkipTime = -1; replaySkipTime = -1;
} }
// load next frame(s)
int replayKey = ReplayFrame.KEY_NONE;
while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) {
ReplayFrame frame = replay.frames[replayIndex];
replayX = frame.getX();
replayY = frame.getY();
replayKeyPressed = frame.isKeyPressed();
if (replayKeyPressed)
replayKey = frame.getKeys();
replayIndex++;
}
// send a key press
if (replayKey != ReplayFrame.KEY_NONE)
gameKeyPressed(((replayKey & ReplayFrame.KEY_M1) > 0) ?
Input.MOUSE_LEFT_BUTTON : Input.MOUSE_RIGHT_BUTTON, replayX, replayY);
} }
// song beginning // song beginning
@ -534,7 +526,7 @@ public class Game extends BasicGameState {
} }
// pause game if focus lost // pause game if focus lost
if (!container.hasFocus() && !GameMod.AUTO.isActive() && replay == null) { if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
if (pauseTime < 0) { if (pauseTime < 0) {
pausedMouseX = mouseX; pausedMouseX = mouseX;
pausedMouseY = mouseY; pausedMouseY = mouseY;
@ -559,14 +551,14 @@ public class Game extends BasicGameState {
} }
// game over, force a restart // game over, force a restart
if (replay == null) { if (!isReplay) {
restart = Restart.LOSE; restart = Restart.LOSE;
game.enterState(Opsu.STATE_GAMEPAUSEMENU); game.enterState(Opsu.STATE_GAMEPAUSEMENU);
} }
} }
// update objects (loop in unlikely event of any skipped indexes) // update objects (loop in unlikely event of any skipped indexes)
boolean keyPressed = ((replay != null && replayKeyPressed) || Utils.isGameKeyPressed()); boolean keyPressed = ((isReplay && replayKeyPressed) || Utils.isGameKeyPressed());
while (objectIndex < hitObjects.length && trackPosition > osu.objects[objectIndex].getTime()) { while (objectIndex < hitObjects.length && trackPosition > osu.objects[objectIndex].getTime()) {
// check if we've already passed the next object's start time // check if we've already passed the next object's start time
boolean overlap = (objectIndex + 1 < hitObjects.length && boolean overlap = (objectIndex + 1 < hitObjects.length &&
@ -588,7 +580,7 @@ public class Game extends BasicGameState {
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
// game keys // game keys
if (!Keyboard.isRepeatEvent() && replay == null) { if (!Keyboard.isRepeatEvent() && !isReplay) {
if (key == Options.getGameKeyLeft()) if (key == Options.getGameKeyLeft())
gameKeyPressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY()); gameKeyPressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY());
else if (key == Options.getGameKeyRight()) else if (key == Options.getGameKeyRight())
@ -598,7 +590,7 @@ public class Game extends BasicGameState {
switch (key) { switch (key) {
case Input.KEY_ESCAPE: case Input.KEY_ESCAPE:
// "auto" mod or watching replay: go back to song menu // "auto" mod or watching replay: go back to song menu
if (GameMod.AUTO.isActive() || replay != null) { if (GameMod.AUTO.isActive() || isReplay) {
game.closeRequested(); game.closeRequested();
break; break;
} }
@ -696,7 +688,7 @@ public class Game extends BasicGameState {
return; return;
// watching replay // watching replay
if (replay != null) { if (isReplay) {
// only allow skip button // only allow skip button
if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y)) if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y))
skipIntro(); skipIntro();
@ -837,7 +829,7 @@ public class Game extends BasicGameState {
} }
// load replay frames // load replay frames
if (replay != null) { if (isReplay) {
// unhide cursor // unhide cursor
UI.showCursor(); UI.showCursor();
@ -862,6 +854,44 @@ public class Game extends BasicGameState {
} else } else
break; break;
} }
// run frame updates in another thread
killReplayThread();
replayThread = new Thread() {
@Override
public void run() {
while (replayThreadRunning) {
// update frames
int trackPosition = MusicController.getPosition();
int keys = ReplayFrame.KEY_NONE;
while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) {
ReplayFrame frame = replay.frames[replayIndex];
replayX = frame.getX();
replayY = frame.getY();
replayKeyPressed = frame.isKeyPressed();
if (replayKeyPressed)
keys = frame.getKeys();
replayIndex++;
}
// send a key press
if (replayKeyPressed && keys != ReplayFrame.KEY_NONE)
gameKeyPressed(((keys & ReplayFrame.KEY_M1) > 0) ?
Input.MOUSE_LEFT_BUTTON : Input.MOUSE_RIGHT_BUTTON, replayX, replayY);
// out of frames
if (replayIndex >= replay.frames.length)
break;
// sleep execution
try {
Thread.sleep(0, 512000);
} catch (InterruptedException e) {}
}
}
};
replayThreadRunning = true;
replayThread.start();
} }
leadInTime = osu.audioLeadIn + approachTime; leadInTime = osu.audioLeadIn + approachTime;
@ -876,10 +906,11 @@ public class Game extends BasicGameState {
throws SlickException { throws SlickException {
// container.setMouseGrabbed(false); // container.setMouseGrabbed(false);
// reset previous mod state and re-hide cursor // replays
if (replay != null) { if (isReplay) {
GameMod.loadModState(previousMods); GameMod.loadModState(previousMods);
UI.hideCursor(); UI.hideCursor();
killReplayThread();
} }
} }
@ -914,7 +945,7 @@ public class Game extends BasicGameState {
* Skips the beginning of a track. * Skips the beginning of a track.
* @return true if skipped, false otherwise * @return true if skipped, false otherwise
*/ */
private boolean skipIntro() { private synchronized boolean skipIntro() {
int firstObjectTime = osu.objects[0].getTime(); int firstObjectTime = osu.objects[0].getTime();
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
if (objectIndex == 0 && trackPosition < firstObjectTime - SKIP_OFFSET) { if (objectIndex == 0 && trackPosition < firstObjectTime - SKIP_OFFSET) {
@ -923,6 +954,8 @@ public class Game extends BasicGameState {
MusicController.resume(); MusicController.resume();
} }
replaySkipTime = -1; replaySkipTime = -1;
if (replayThread != null && replayThread.isAlive())
replayThread.interrupt();
MusicController.setPosition(firstObjectTime - SKIP_OFFSET); MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
SoundController.playSound(SoundEffect.MENUHIT); SoundController.playSound(SoundEffect.MENUHIT);
return true; return true;
@ -1052,9 +1085,23 @@ public class Game extends BasicGameState {
*/ */
public float getTimingPointMultiplier() { return beatLength / beatLengthBase; } public float getTimingPointMultiplier() { return beatLength / beatLengthBase; }
/**
* Kills the running replay updating thread, if any.
*/
private void killReplayThread() {
if (replayThread != null && replayThread.isAlive()) {
replayThreadRunning = false;
replayThread.interrupt();
}
replayThread = null;
}
/** /**
* Sets a replay to view. * Sets a replay to view.
* @param replay the replay * @param replay the replay
*/ */
public void setReplay(Replay replay) { this.replay = replay; } public void setReplay(Replay replay) {
this.isReplay = true;
this.replay = replay;
}
} }

View File

@ -239,6 +239,8 @@ public class OpenALStreamPlayer {
return; return;
} }
long playedPos_ = playedPos;
long lastUpdateTime_ = lastUpdateTime;
int processed = AL10.alGetSourcei(source, AL10.AL_BUFFERS_PROCESSED); int processed = AL10.alGetSourcei(source, AL10.AL_BUFFERS_PROCESSED);
while (processed > 0) { while (processed > 0) {
unqueued.clear(); unqueued.clear();
@ -248,11 +250,11 @@ public class OpenALStreamPlayer {
int bufferLength = AL10.alGetBufferi(bufferIndex, AL10.AL_SIZE); int bufferLength = AL10.alGetBufferi(bufferIndex, AL10.AL_SIZE);
playedPos += bufferLength; playedPos_ += bufferLength;
lastUpdateTime = getTime(); lastUpdateTime_ = getTime();
if (musicLength > 0 && playedPos > musicLength) if (musicLength > 0 && playedPos_ > musicLength)
playedPos -= musicLength; playedPos_ -= musicLength;
if (stream(bufferIndex)) { if (stream(bufferIndex)) {
AL10.alSourceQueueBuffers(source, unqueued); AL10.alSourceQueueBuffers(source, unqueued);
@ -264,6 +266,8 @@ public class OpenALStreamPlayer {
} }
processed--; processed--;
} }
playedPos = playedPos_;
lastUpdateTime = lastUpdateTime_;
int state = AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE); int state = AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE);
@ -382,7 +386,7 @@ public class OpenALStreamPlayer {
* @return The current position in seconds. * @return The current position in seconds.
*/ */
public float getPosition() { public float getPosition() {
float playedTime = ((float) playedPos / (float) sampleSize) / sampleRate; float playedTime = ((float) playedPos / sampleSize) / sampleRate;
float timePosition = playedTime + (getTime() - lastUpdateTime) / 1000f; float timePosition = playedTime + (getTime() - lastUpdateTime) / 1000f;
// + AL10.alGetSourcef(source, AL11.AL_SEC_OFFSET); // + AL10.alGetSourcef(source, AL11.AL_SEC_OFFSET);
return timePosition; return timePosition;