Implement hit object stacking algorithm.

This commit is contained in:
Pavel Kolchev 2015-03-28 15:11:43 +03:00
parent 16ec6c5e23
commit fbe87559c9
8 changed files with 207 additions and 26 deletions

View File

@ -72,9 +72,27 @@ public class OsuHitObject {
/** The container height. */ /** The container height. */
private static int containerHeight; private static int containerHeight;
/**
* Return stack position modifier in pixels.
* @return stack position modifier.
*/
public static float getStackOffset() { return stackOffset; }
/**
* Sets stack position modifier in pixels
* @param offset position modifier.
*/
public static void setStackOffset(float offset) { stackOffset = offset; }
/** The offset per stack. */
private static float stackOffset;
/** Starting coordinates. */ /** Starting coordinates. */
private float x, y; private float x, y;
/** Hit object index in current stack. */
private int stack;
/** Start time (in ms). */ /** Start time (in ms). */
private int time; private int time;
@ -249,18 +267,30 @@ public class OsuHitObject {
/** /**
* Returns the scaled starting x coordinate. * Returns the scaled starting x coordinate.
*/ */
public float getScaledX() { return x * xMultiplier + xOffset; } public float getScaledX() { return (x - stack * stackOffset) * xMultiplier + xOffset; }
/** /**
* Returns the scaled starting y coordinate. * Returns the scaled starting y coordinate.
*/ */
public float getScaledY() { public float getScaledY() {
if (GameMod.HARD_ROCK.isActive()) if (GameMod.HARD_ROCK.isActive())
return containerHeight - (y * yMultiplier + yOffset); return containerHeight - ((y - stack * stackOffset) * yMultiplier + yOffset);
else else
return y * yMultiplier + yOffset; return (y - stack * stackOffset) * yMultiplier + yOffset;
} }
/**
* Sets the hit object index in current stack.
* @param stack index in stack
*/
public void setStack(int stack) { this.stack = stack; }
/**
* Returns hit object index in current stack.
* @return index in stack
*/
public int getStack() { return stack; }
/** /**
* Returns the start time. * Returns the start time.
* @return the start time (in ms) * @return the start time (in ms)
@ -331,7 +361,7 @@ public class OsuHitObject {
float[] x = new float[sliderX.length]; float[] x = new float[sliderX.length];
for (int i = 0; i < x.length; i++) for (int i = 0; i < x.length; i++)
x[i] = sliderX[i] * xMultiplier + xOffset; x[i] = (sliderX[i] - stack * stackOffset) * xMultiplier + xOffset;
return x; return x;
} }
@ -346,10 +376,10 @@ public class OsuHitObject {
float[] y = new float[sliderY.length]; float[] y = new float[sliderY.length];
if (GameMod.HARD_ROCK.isActive()) { if (GameMod.HARD_ROCK.isActive()) {
for (int i = 0; i < y.length; i++) for (int i = 0; i < y.length; i++)
y[i] = containerHeight - (sliderY[i] * yMultiplier + yOffset); y[i] = containerHeight - ((sliderY[i] - stack * stackOffset) * yMultiplier + yOffset);
} else { } else {
for (int i = 0; i < y.length; i++) for (int i = 0; i < y.length; i++)
y[i] = sliderY[i] * yMultiplier + yOffset; y[i] = (sliderY[i] - stack * stackOffset) * yMultiplier + yOffset;
} }
return y; return y;
} }

View File

@ -250,6 +250,20 @@ public class Utils {
return val; return val;
} }
/**
*
* @param x1 The x-component of the first point
* @param y1 The y-component of the first point
* @param x2 The x-component of the second point
* @param y2 The y-component of the second point
* @return Euclidean distance between points (x1,y1) and (x2,y2)
*/
public static float distance(float x1, float y1, float x2, float y2) {
float v1 = Math.abs(x1 - x2);
float v2 = Math.abs(y1 - y2);
return (float) Math.sqrt((v1 * v1) + (v2 * v2));
}
/** /**
* Returns true if a game input key is pressed (mouse/keyboard left/right). * Returns true if a game input key is pressed (mouse/keyboard left/right).
* @return true if pressed * @return true if pressed

View File

@ -183,4 +183,13 @@ public class Circle implements HitObject {
@Override @Override
public int getEndTime() { return hitObject.getTime(); } public int getEndTime() { return hitObject.getTime(); }
@Override
public OsuHitObject getHitObject() { return hitObject; }
@Override
public void updatePosition() {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
}
} }

View File

@ -53,6 +53,15 @@ public class DummyObject implements HitObject {
@Override @Override
public boolean mousePressed(int x, int y, int trackPosition) { return false; } public boolean mousePressed(int x, int y, int trackPosition) { return false; }
@Override
public OsuHitObject getHitObject() { return hitObject; }
@Override
public void updatePosition() {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
}
@Override @Override
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; } public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }

View File

@ -18,6 +18,7 @@
package itdelatrisu.opsu.objects; package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.OsuHitObject;
import org.newdawn.slick.Graphics; import org.newdawn.slick.Graphics;
/** /**
@ -64,4 +65,15 @@ public interface HitObject {
* @return the end time, in milliseconds * @return the end time, in milliseconds
*/ */
public int getEndTime(); public int getEndTime();
/**
* Return associated OsuHitObject.
* @return hit object as OsuHitObject
*/
public OsuHitObject getHitObject();
/**
* Updates position of hit object.
*/
public void updatePosition();
} }

View File

@ -146,6 +146,21 @@ public class Slider implements HitObject {
this.curve = new CircumscribedCircle(hitObject, color); this.curve = new CircumscribedCircle(hitObject, color);
else else
this.curve = new LinearBezier(hitObject, color); this.curve = new LinearBezier(hitObject, color);
// slider time calculations
this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f;
this.sliderTimeTotal = sliderTime * hitObject.getRepeatCount();
// ticks
float tickLengthDiv = 100f * sliderMultiplier / sliderTickRate / game.getTimingPointMultiplier();
int tickCount = (int) Math.ceil(hitObject.getPixelLength() / tickLengthDiv) - 1;
if (tickCount > 0) {
this.ticksT = new float[tickCount];
float tickTOffset = 1f / (tickCount + 1);
float t = tickTOffset;
for (int i = 0; i < tickCount; i++, t += tickTOffset)
ticksT[i] = t;
}
} }
@Override @Override
@ -313,25 +328,6 @@ public class Slider implements HitObject {
@Override @Override
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) { public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) {
int repeatCount = hitObject.getRepeatCount(); int repeatCount = hitObject.getRepeatCount();
// slider time and tick calculations
if (sliderTimeTotal == 0f) {
// slider time
this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f;
this.sliderTimeTotal = sliderTime * repeatCount;
// ticks
float tickLengthDiv = 100f * sliderMultiplier / sliderTickRate / game.getTimingPointMultiplier();
int tickCount = (int) Math.ceil(hitObject.getPixelLength() / tickLengthDiv) - 1;
if (tickCount > 0) {
this.ticksT = new float[tickCount];
float tickTOffset = 1f / (tickCount + 1);
float t = tickTOffset;
for (int i = 0; i < tickCount; i++, t += tickTOffset)
ticksT[i] = t;
}
}
int[] hitResultOffset = game.getHitResultOffsets(); int[] hitResultOffset = game.getHitResultOffsets();
boolean isAutoMod = GameMod.AUTO.isActive(); boolean isAutoMod = GameMod.AUTO.isActive();
@ -455,6 +451,20 @@ public class Slider implements HitObject {
return false; return false;
} }
@Override
public OsuHitObject getHitObject() { return hitObject; }
@Override
public void updatePosition() {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
if (hitObject.getSliderType() == OsuHitObject.SLIDER_PASSTHROUGH && hitObject.getSliderX().length == 2)
this.curve = new CircumscribedCircle(hitObject, color);
else
this.curve = new LinearBezier(hitObject, color);
}
@Override @Override
public float[] getPointAt(int trackPosition) { public float[] getPointAt(int trackPosition) {
if (trackPosition <= hitObject.getTime()) if (trackPosition <= hitObject.getTime())

View File

@ -263,6 +263,12 @@ public class Spinner implements HitObject {
return false; return false;
} }
@Override
public OsuHitObject getHitObject() { return hitObject; }
@Override
public void updatePosition() {}
@Override @Override
public float[] getPointAt(int trackPosition) { public float[] getPointAt(int trackPosition) {
// get spinner time // get spinner time

View File

@ -80,12 +80,18 @@ public class Game extends BasicGameState {
/** Replay. */ /** Replay. */
REPLAY, REPLAY,
/** Health is zero: no-continue/force restart. */ /** Health is zero: no-continue/force restart. */
LOSE; LOSE
} }
/** Minimum time before start of song, in milliseconds, to process skip-related actions. */ /** Minimum time before start of song, in milliseconds, to process skip-related actions. */
private static final int SKIP_OFFSET = 2000; private static final int SKIP_OFFSET = 2000;
/** Tolerance in case if hit object is not snapped to the grid */
private static final float STACK_LENIENCE = 3f;
/** Stack time window of the previous object in ms. */
private static final int STACK_TIMEOUT = 1000;
/** The associated OsuFile object. */ /** The associated OsuFile object. */
private OsuFile osu; private OsuFile osu;
@ -1027,6 +1033,21 @@ public class Game extends BasicGameState {
Color color = osu.combo[hitObject.getComboIndex()]; Color color = osu.combo[hitObject.getComboIndex()];
// pass beatLength to hit objects
int hitObjectTime = hitObject.getTime();
int timingPointIndex = 0;
while (timingPointIndex < osu.timingPoints.size()) {
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
if (timingPoint.getTime() <= hitObjectTime) {
if (!timingPoint.isInherited())
beatLengthBase = beatLength = timingPoint.getBeatLength();
else
beatLength = beatLengthBase * timingPoint.getSliderMultiplier();
} else
break;
timingPointIndex++;
}
try { try {
if (hitObject.isCircle()) if (hitObject.isCircle())
hitObjects[i] = new Circle(hitObject, this, data, color, comboEnd); hitObjects[i] = new Circle(hitObject, this, data, color, comboEnd);
@ -1043,6 +1064,72 @@ public class Game extends BasicGameState {
} }
} }
// stack calculation
for (int i = hitObjects.length - 1; i > 0; i--) {
OsuHitObject hitObjectI = hitObjects[i].getHitObject();
// already calculated
if (hitObjectI.getStack() != 0 || hitObjectI.isSpinner())
continue;
// search for hit objects in stack
for (int n = i -1; n >= 0; n--) {
OsuHitObject hitObjectN = hitObjects[n].getHitObject();
if (hitObjectN.isSpinner())
continue;
// check if in range stack calculation
float timeI = hitObjectI.getTime() - (STACK_TIMEOUT * osu.stackLeniency);
float timeN = hitObjectN.isSlider() ? hitObjects[n].getEndTime() : hitObjectN.getTime();
if (timeI > timeN)
break;
if (hitObjectN.isSlider()) {
// possible special case. if slider end in the stack, all next hit objects in stack moves right down
Slider slider = (Slider) hitObjects[n];
float x1 = hitObjects[i].getPointAt(hitObjectI.getTime())[0];
float y1 = hitObjects[i].getPointAt(hitObjectI.getTime())[1];
float x2 = slider.getPointAt(slider.getEndTime())[0];
float y2 = slider.getPointAt(slider.getEndTime())[1];
float distance = Utils.distance(x1, y1, x2, y2);
// check if hit object part of this stack
if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) {
int offset = hitObjectI.getStack() - hitObjectN.getStack() + 1;
for (int j = n + 1; j <= i; j++) {
OsuHitObject hitObjectJ = hitObjects[j].getHitObject();
x1 = hitObjects[j].getPointAt(hitObjectJ.getTime())[0];
y1 = hitObjects[j].getPointAt(hitObjectJ.getTime())[1];
distance = Utils.distance(x1, y1, x2, y2);
if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) {
// hit object below slider end
hitObjectJ.setStack(hitObjectJ.getStack() - offset);
}
}
// slider end always start of the stack. reset calculation
break;
}
}
// no special case. stack moves up left
float x1 = hitObjectI.getX();
float y1 = hitObjectI.getY();
float x2 = hitObjectN.getX();
float y2 = hitObjectN.getY();
float distance = Utils.distance(x1, y1, x2, y2);
if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) {
hitObjectN.setStack(hitObjectI.getStack() + 1);
hitObjectI = hitObjectN;
}
}
}
// update hit objects positions
for (int i = 0; i < hitObjects.length; i++) {
hitObjects[i].updatePosition();
}
// load the first timingPoint // load the first timingPoint
if (!osu.timingPoints.isEmpty()) { if (!osu.timingPoints.isEmpty()) {
OsuTimingPoint timingPoint = osu.timingPoints.get(0); OsuTimingPoint timingPoint = osu.timingPoints.get(0);
@ -1331,6 +1418,10 @@ public class Game extends BasicGameState {
if (Options.getFixedHP() > 0f) if (Options.getFixedHP() > 0f)
HPDrainRate = Options.getFixedHP(); HPDrainRate = Options.getFixedHP();
// Stack modifier scales with hit object size
int diameter = (int) (104 - (circleSize * 8));
OsuHitObject.setStackOffset(diameter * 0.05f);
// initialize objects // initialize objects
Circle.init(container, circleSize); Circle.init(container, circleSize);
Slider.init(container, circleSize, osu); Slider.init(container, circleSize, osu);