Merge branch 'animations' into slider-improvements

This commit is contained in:
yugecin 2016-12-18 16:48:03 +01:00
commit e16cae73d1
2 changed files with 186 additions and 121 deletions

View File

@ -26,7 +26,6 @@ import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.Colors;
@ -151,7 +150,8 @@ public class GameData {
HIT_SLIDER10 = 7, HIT_SLIDER10 = 7,
HIT_SLIDER30 = 8, HIT_SLIDER30 = 8,
HIT_MAX = 9, // not a hit result HIT_MAX = 9, // not a hit result
HIT_SLIDER_REPEAT = 10; // not a hit result HIT_SLIDER_REPEAT = 10, // not a hit result
HIT_ANIMATION_RESULT = 11; // not a hit result
/** Hit result-related images (indexed by HIT_* constants). */ /** Hit result-related images (indexed by HIT_* constants). */
private Image[] hitResults; private Image[] hitResults;
@ -232,7 +232,7 @@ public class GameData {
private LinkedBlockingDeque<HitErrorInfo> hitErrorList; private LinkedBlockingDeque<HitErrorInfo> hitErrorList;
/** Hit object types, used for drawing results. */ /** Hit object types, used for drawing results. */
public enum HitObjectType { CIRCLE, SLIDERTICK, SLIDER_FIRST, SLIDER_LAST, SPINNER } public enum HitObjectType { CIRCLE, SLIDERTICK, SLIDER_FIRST, SLIDER_CURVE, SLIDER_LAST, SPINNER }
/** Hit result helper class. */ /** Hit result helper class. */
private class HitObjectResult { private class HitObjectResult {
@ -892,50 +892,9 @@ public class GameData {
lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color); lighting.drawCentered(hitResult.x, hitResult.y, hitResult.color);
} }
// hit animation // hit animations, only draw when the 'hidden' mod is not enabled
if (hitResult.result != HIT_MISS && ( if (!GameMod.HIDDEN.isActive()) {
hitResult.hitResultType == HitObjectType.CIRCLE || drawHitAnimations(hitResult, trackPosition);
hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
hitResult.hitResultType == HitObjectType.SLIDER_LAST)) {
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
float alpha = 1f - progress;
if (hitResult.result == HIT_SLIDER_REPEAT) {
// repeats
Image scaledRepeat = GameImage.REVERSEARROW.getImage().getScaledCopy(scale);
scaledRepeat.setAlpha(alpha);
float ang;
if (hitResult.hitResultType == HitObjectType.SLIDER_FIRST) {
ang = hitResult.curve.getStartAngle();
} else {
ang = hitResult.curve.getEndAngle();
}
scaledRepeat.rotate(ang);
scaledRepeat.drawCentered(hitResult.x, hitResult.y, hitResult.color);
}
// "hidden" mod: circle and slider animations not drawn
else if (!GameMod.HIDDEN.isActive()) {
// slider curve
if (hitResult.curve != null) {
float oldWhiteAlpha = Colors.WHITE_FADE.a;
float oldColorAlpha = hitResult.color.a;
Colors.WHITE_FADE.a = alpha;
hitResult.color.a = alpha;
hitResult.curve.draw(hitResult.color);
Colors.WHITE_FADE.a = oldWhiteAlpha;
hitResult.color.a = oldColorAlpha;
}
// hit circles
Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
scaledHitCircle.setAlpha(alpha);
scaledHitCircleOverlay.setAlpha(alpha);
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
}
} }
// hit result // hit result
@ -963,6 +922,66 @@ public class GameData {
} }
} }
/**
* Draw the hit animations: circles, reversearrows, slider curves fading out and/or expanding
* @param hitResult the hitresult which holds information about the kind of animation to draw
* @param trackPosition the current track position (in ms)
*/
private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) {
if (hitResult.hitResultType == HitObjectType.SLIDER_CURVE && hitResult.curve != null) {
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
float alpha = 1f - progress;
// slider curve
float oldWhiteAlpha = Colors.WHITE_FADE.a;
float oldColorAlpha = hitResult.color.a;
Colors.WHITE_FADE.a = alpha;
hitResult.color.a = alpha;
hitResult.curve.draw(hitResult.color);
Colors.WHITE_FADE.a = oldWhiteAlpha;
hitResult.color.a = oldColorAlpha;
return;
}
if (hitResult.result == HIT_MISS) {
return;
}
if (hitResult.hitResultType != HitObjectType.CIRCLE
&& hitResult.hitResultType != HitObjectType.SLIDER_FIRST
&& hitResult.hitResultType != HitObjectType.SLIDER_LAST) {
return;
}
// circles
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
float alpha = 1f - progress;
if (hitResult.result == HIT_SLIDER_REPEAT) {
// repeats
Image scaledRepeat = GameImage.REVERSEARROW.getImage().getScaledCopy(scale);
scaledRepeat.setAlpha(alpha);
float ang;
if (hitResult.hitResultType == HitObjectType.SLIDER_FIRST) {
ang = hitResult.curve.getStartAngle();
} else {
ang = hitResult.curve.getEndAngle();
}
scaledRepeat.rotate(ang);
scaledRepeat.drawCentered(hitResult.x, hitResult.y, hitResult.color);
}
// hit circles
Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
scaledHitCircle.setAlpha(alpha);
scaledHitCircleOverlay.setAlpha(alpha);
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
}
/** /**
* Changes health by a given percentage, modified by drainRate. * Changes health by a given percentage, modified by drainRate.
* @param percent the health percentage * @param percent the health percentage
@ -1195,6 +1214,14 @@ public class GameData {
hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, x, y, color, type, curve, true, true)); hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, x, y, color, type, curve, true, true));
} }
public void sendSliderCurveResult(int time, Color color, Curve curve) {
hitResultList.add(new HitObjectResult(time, HIT_SLIDER_REPEAT, 0f, 0f, color, HitObjectType.SLIDER_CURVE, curve, true, true));
}
public void sendAnimationResult(int time, float x, float y, Color color, boolean expand) {
hitResultList.add(new HitObjectResult(time, HIT_ANIMATION_RESULT, x, y, color, HitObjectType.CIRCLE, null, expand, true));
}
/** /**
* Handles a slider tick result. * Handles a slider tick result.
* @param time the tick start time * @param time the tick start time
@ -1396,14 +1423,6 @@ public class GameData {
boolean hideResult = (hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled(); boolean hideResult = (hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled();
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand, hideResult)); hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand, hideResult));
// sliders: add the other curve endpoint for the hit animation
if (curve != null) {
boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
Vec2f p = curve.pointAt((isFirst) ? 1f : 0f);
HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
hitResultList.add(new HitObjectResult(time, hitResult, p.x, p.y, color, type, null, expand, hideResult));
}
} }
/** /**

View File

@ -180,6 +180,7 @@ public class Slider implements GameObject {
@Override @Override
public void draw(Graphics g, int trackPosition) { public void draw(Graphics g, int trackPosition) {
int timeDiff = hitObject.getTime() - trackPosition; int timeDiff = hitObject.getTime() - trackPosition;
final int repeats = hitObject.getRepeatCount();
final int approachTime = game.getApproachTime(); final int approachTime = game.getApproachTime();
final int fadeInTime = game.getFadeInTime(); final int fadeInTime = game.getFadeInTime();
float scale = timeDiff / (float) approachTime; float scale = timeDiff / (float) approachTime;
@ -205,15 +206,38 @@ public class Slider implements GameObject {
float curveInterval = Options.isSliderSnaking() ? alpha : 1f; float curveInterval = Options.isSliderSnaking() ? alpha : 1f;
curve.draw(color,curveInterval); curve.draw(color,curveInterval);
// end circle // end circle, only draw if ball still has to go there
if (curveInterval == 1f && currentRepeats < repeats - (repeats % 2 == 0 ? 1 : 0)) {
Color circleColor = new Color(color);
Color overlayColor = new Color(Colors.WHITE_FADE);
if (currentRepeats == 0) {
if (Options.isSliderSnaking()) {
// fade in end circle using decorationsAlpha when snaking sliders are enabled
circleColor.a = overlayColor.a = sliderAlpha * decorationsAlpha;
}
} else {
// fade in end circle after repeats
circleColor.a = overlayColor.a = sliderAlpha * getCircleAlphaAfterRepeat(trackPosition, true);
}
Vec2f endCircPos = curve.pointAt(curveInterval); Vec2f endCircPos = curve.pointAt(curveInterval);
hitCircle.drawCentered(endCircPos.x, endCircPos.y, color); hitCircle.drawCentered(endCircPos.x, endCircPos.y, circleColor);
hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, Colors.WHITE_FADE); hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, overlayColor);
}
// start circle // set first circle colors to fade in after repeats
hitCircle.drawCentered(x, y, color); Color firstCircleColor = new Color(color);
if (!overlayAboveNumber) Color startCircleOverlayColor = new Color(Colors.WHITE_FADE);
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE); if (sliderClickedInitial) {
// fade in first circle after repeats
firstCircleColor.a = startCircleOverlayColor.a = sliderAlpha * getCircleAlphaAfterRepeat(trackPosition, false);
}
// start circle, only draw if ball still has to go there
if (!sliderClickedInitial || currentRepeats < repeats - (repeats % 2 == 1 ? 1 : 0)) {
hitCircle.drawCentered(x, y, firstCircleColor);
if (!overlayAboveNumber || sliderClickedInitial)
hitCircleOverlay.drawCentered(x, y, startCircleOverlayColor);
}
color.a = alpha; color.a = alpha;
@ -231,35 +255,34 @@ public class Slider implements GameObject {
alpha = Math.min(alpha, hiddenAlpha); alpha = Math.min(alpha, hiddenAlpha);
} }
} }
if (sliderClickedInitial)
; // don't draw current combo number if already clicked // draw combonumber and overlay if not initially clicked
else if (!sliderClickedInitial) {
data.drawSymbolNumber(hitObject.getComboNumber(), x, y, data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha); hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
if (overlayAboveNumber) { if (overlayAboveNumber) {
oldWhiteFadeAlpha = Colors.WHITE_FADE.a; startCircleOverlayColor.a = sliderAlpha;
Colors.WHITE_FADE.a = sliderAlpha; hitCircleOverlay.drawCentered(x, y, startCircleOverlayColor);
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE); }
Colors.WHITE_FADE.a = oldWhiteFadeAlpha;
} }
// repeats // repeats
if (curveInterval == 1.0f) { if (curveInterval == 1.0f) {
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1 && tcurRepeat < repeats - 1; tcurRepeat++) {
if (hitObject.getRepeatCount() - 1 > tcurRepeat) {
Image arrow = GameImage.REVERSEARROW.getImage(); Image arrow = GameImage.REVERSEARROW.getImage();
// bouncing animation // bouncing animation
//arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292)); //arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292));
float colorLuminance = 0.299f*color.r + 0.587f*color.g + 0.114f*color.b; float colorLuminance = 0.299f*color.r + 0.587f*color.g + 0.114f*color.b;
Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black; Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black;
if (tcurRepeat != currentRepeats) { if (tcurRepeat == 0) {
if (sliderTime == 0)
continue;
float t = Math.max(getT(trackPosition, true), 0);
arrow.setAlpha((float) (t - Math.floor(t)));
} else
arrow.setAlpha(Options.isSliderSnaking() ? decorationsAlpha : 1f); arrow.setAlpha(Options.isSliderSnaking() ? decorationsAlpha : 1f);
} else {
if (!sliderClickedInitial) {
continue;
}
arrow.setAlpha(getCircleAlphaAfterRepeat(trackPosition, tcurRepeat % 2 == 0));
}
if (tcurRepeat % 2 == 0) { if (tcurRepeat % 2 == 0) {
// last circle // last circle
arrow.setRotation(curve.getEndAngle()); arrow.setRotation(curve.getEndAngle());
@ -271,7 +294,6 @@ public class Slider implements GameObject {
} }
} }
} }
}
if (timeDiff >= 0) { if (timeDiff >= 0) {
// approach circle // approach circle
@ -363,6 +385,24 @@ public class Slider implements GameObject {
} }
} }
/**
* Get the alpha level used to fade in circles & reversearrows after repeat
* @param trackPosition current trackposition, in ms
* @param endCircle request alpha for end circle (true) or start circle (false)?
* @return alpha level as float in interval [0, 1]
*/
private float getCircleAlphaAfterRepeat(int trackPosition, boolean endCircle) {
int ticksN = ticksT == null ? 0 : ticksT.length;
float t = getT(trackPosition, false);
if (endCircle) {
t = 1f - t;
}
if (currentRepeats % 2 == (endCircle ? 0 : 1)) {
t = 1f;
}
return Utils.clamp(t * (ticksN + 1), 0f, 1f);
}
/** /**
* Calculates the slider hit result. * Calculates the slider hit result.
* @return the hit result (GameData.HIT_* constants) * @return the hit result (GameData.HIT_* constants)
@ -441,7 +481,7 @@ public class Slider implements GameObject {
} }
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result, data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
cx, cy, color, comboEnd, hitObject, type, sliderHeldToEnd, cx, cy, color, comboEnd, hitObject, type, sliderHeldToEnd,
currentRepeats + 1, curve, sliderHeldToEnd); currentRepeats + 1, null, sliderHeldToEnd);
return result; return result;
} }
@ -460,8 +500,11 @@ public class Slider implements GameObject {
if (timeDiff < hitResultOffset[GameData.HIT_50]) { if (timeDiff < hitResultOffset[GameData.HIT_50]) {
result = GameData.HIT_SLIDER30; result = GameData.HIT_SLIDER30;
ticksHit++; ticksHit++;
} else if (timeDiff < hitResultOffset[GameData.HIT_MISS]) data.sendAnimationResult(trackPosition, this.x, this.y, color, true);
} else if (timeDiff < hitResultOffset[GameData.HIT_MISS]) {
result = GameData.HIT_MISS; result = GameData.HIT_MISS;
data.sendAnimationResult(trackPosition, this.x, this.y, color, false);
}
//else not a hit //else not a hit
if (result > -1) { if (result > -1) {
@ -489,8 +532,11 @@ public class Slider implements GameObject {
if (isAutoMod) { // "auto" mod: catch any missed notes due to lag if (isAutoMod) { // "auto" mod: catch any missed notes due to lag
ticksHit++; ticksHit++;
data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats); data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
} else data.sendAnimationResult(time, x, y, color, true);
} else {
data.sliderTickResult(time, GameData.HIT_MISS, x, y, hitObject, currentRepeats); data.sliderTickResult(time, GameData.HIT_MISS, x, y, hitObject, currentRepeats);
data.sendAnimationResult(trackPosition, x, y, color, false);
}
} }
// "auto" mod: send a perfect hit result // "auto" mod: send a perfect hit result
@ -499,6 +545,7 @@ public class Slider implements GameObject {
ticksHit++; ticksHit++;
sliderClickedInitial = true; sliderClickedInitial = true;
data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats); data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
data.sendAnimationResult(time, x, y, color, true);
} }
} }
@ -530,6 +577,9 @@ public class Slider implements GameObject {
// calculate and send slider result // calculate and send slider result
hitResult(); hitResult();
// send 'curve fade out' hit result
data.sendSliderCurveResult(getEndTime(), color, curve);
return true; return true;
} }
@ -549,23 +599,6 @@ public class Slider implements GameObject {
tickIndex = 0; tickIndex = 0;
isNewRepeat = true; isNewRepeat = true;
tickExpandTime = TICK_EXPAND_TIME; tickExpandTime = TICK_EXPAND_TIME;
// send hit result, to fade out reversearrow
HitObjectType type;
float posX, posY;
if (currentRepeats % 2 == 1) {
type = HitObjectType.SLIDER_LAST;
Vec2f endPos = curve.pointAt(1);
posX = endPos.x;
posY = endPos.y;
} else {
type = HitObjectType.SLIDER_FIRST;
posX = this.x;
posY = this.y;
}
float colorLuminance = 0.299f*color.r + 0.587f*color.g + 0.114f*color.b;
Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black;
data.sendRepeatSliderResult(trackPosition, posX, posY, arrowColor, curve, type);
} }
} }
@ -593,13 +626,26 @@ public class Slider implements GameObject {
// held during new repeat // held during new repeat
if (isNewRepeat) { if (isNewRepeat) {
ticksHit++; ticksHit++;
HitObjectType type;
float posX, posY;
if (currentRepeats % 2 > 0) { // last circle if (currentRepeats % 2 > 0) { // last circle
int lastIndex = hitObject.getSliderX().length; type = HitObjectType.SLIDER_LAST;
Vec2f endPos = curve.pointAt(1f);
posX = endPos.x;
posY = endPos.y;
} else { // first circle
type = HitObjectType.SLIDER_FIRST;
posX = this.x;
posY = this.y;
}
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30, data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats); posX, posY, hitObject, currentRepeats);
} else // first circle
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30, // send hit result, to fade out reversearrow
c.x, c.y, hitObject, currentRepeats); float colorLuminance = 0.299f*color.r + 0.587f*color.g + 0.114f*color.b;
Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black;
data.sendRepeatSliderResult(trackPosition, posX, posY, arrowColor, curve, type);
} }
// held during new tick // held during new tick