Merge pull request #61 from DarkTigrus/hitobject-stacking
Hit object stacking algorithm
This commit is contained in:
commit
f63be81490
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -183,4 +183,10 @@ public class Circle implements HitObject {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getEndTime() { return hitObject.getTime(); }
|
public int getEndTime() { return hitObject.getTime(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updatePosition() {
|
||||||
|
this.x = hitObject.getScaledX();
|
||||||
|
this.y = hitObject.getScaledY();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,12 @@ 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 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 }; }
|
||||||
|
|
||||||
|
|
|
@ -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,9 @@ public interface HitObject {
|
||||||
* @return the end time, in milliseconds
|
* @return the end time, in milliseconds
|
||||||
*/
|
*/
|
||||||
public int getEndTime();
|
public int getEndTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates position of hit object.
|
||||||
|
*/
|
||||||
|
public void updatePosition();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,17 @@ public class Slider implements HitObject {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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())
|
||||||
|
|
|
@ -263,6 +263,9 @@ public class Spinner implements HitObject {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updatePosition() {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float[] getPointAt(int trackPosition) {
|
public float[] getPointAt(int trackPosition) {
|
||||||
// get spinner time
|
// get spinner time
|
||||||
|
|
|
@ -80,12 +80,21 @@ 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;
|
||||||
|
|
||||||
|
/** Stack position offset modifier. */
|
||||||
|
private static final float STACK_OFFSET_MODIFIER = 0.05f;
|
||||||
|
|
||||||
/** The associated OsuFile object. */
|
/** The associated OsuFile object. */
|
||||||
private OsuFile osu;
|
private OsuFile osu;
|
||||||
|
|
||||||
|
@ -666,10 +675,7 @@ public class Game extends BasicGameState {
|
||||||
if (timingPointIndex < osu.timingPoints.size()) {
|
if (timingPointIndex < osu.timingPoints.size()) {
|
||||||
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
|
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
|
||||||
if (trackPosition >= timingPoint.getTime()) {
|
if (trackPosition >= timingPoint.getTime()) {
|
||||||
if (!timingPoint.isInherited())
|
setBeatLength(timingPoint);
|
||||||
beatLengthBase = beatLength = timingPoint.getBeatLength();
|
|
||||||
else
|
|
||||||
beatLength = beatLengthBase * timingPoint.getSliderMultiplier();
|
|
||||||
HitSound.setDefaultSampleSet(timingPoint.getSampleType());
|
HitSound.setDefaultSampleSet(timingPoint.getSampleType());
|
||||||
SoundController.setSampleVolume(timingPoint.getSampleVolume());
|
SoundController.setSampleVolume(timingPoint.getSampleVolume());
|
||||||
timingPointIndex++;
|
timingPointIndex++;
|
||||||
|
@ -1033,6 +1039,18 @@ 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) {
|
||||||
|
setBeatLength(timingPoint);
|
||||||
|
} 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);
|
||||||
|
@ -1049,6 +1067,73 @@ public class Game extends BasicGameState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stack calculation
|
||||||
|
// more info: https://gist.github.com/peppy/1167470
|
||||||
|
// @author peppy
|
||||||
|
for (int i = hitObjects.length - 1; i > 0; i--) {
|
||||||
|
OsuHitObject hitObjectI = osu.objects[i];
|
||||||
|
|
||||||
|
// 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 = osu.objects[n];
|
||||||
|
|
||||||
|
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 = osu.objects[j];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// not a special case. stack moves up left
|
||||||
|
float distance = Utils.distance(hitObjectI.getX(), hitObjectI.getY(),
|
||||||
|
hitObjectN.getX(), hitObjectN.getY());
|
||||||
|
if (distance < STACK_LENIENCE) {
|
||||||
|
hitObjectN.setStack(hitObjectI.getStack() + 1);
|
||||||
|
hitObjectI = hitObjectN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update hit objects positions
|
||||||
|
for (int i = 0; i < hitObjects.length; i++) {
|
||||||
|
if(osu.objects[i].getStack() != 0) {
|
||||||
|
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);
|
||||||
|
@ -1338,6 +1423,11 @@ 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
|
||||||
|
// StackOffset = HitObjectRadius / 10
|
||||||
|
int diameter = (int) (104 - (circleSize * 8));
|
||||||
|
OsuHitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
|
||||||
|
|
||||||
// initialize objects
|
// initialize objects
|
||||||
Circle.init(container, circleSize);
|
Circle.init(container, circleSize);
|
||||||
Slider.init(container, circleSize, osu);
|
Slider.init(container, circleSize, osu);
|
||||||
|
@ -1388,6 +1478,16 @@ public class Game extends BasicGameState {
|
||||||
*/
|
*/
|
||||||
public float getBeatLength() { return beatLength; }
|
public float getBeatLength() { return beatLength; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the beat length fields based on a given timing point.
|
||||||
|
*/
|
||||||
|
private void setBeatLength(OsuTimingPoint timingPoint) {
|
||||||
|
if (!timingPoint.isInherited())
|
||||||
|
beatLengthBase = beatLength = timingPoint.getBeatLength();
|
||||||
|
else
|
||||||
|
beatLength = beatLengthBase * timingPoint.getSliderMultiplier();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the slider multiplier given by the current timing point.
|
* Returns the slider multiplier given by the current timing point.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue
Block a user