1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-21 00:12:59 +08:00

Merge pull request #336 from Damnae/hitobject_stacking

Add hit object stacking.
This commit is contained in:
Dean Herbert 2017-02-09 17:01:52 +09:00 committed by GitHub
commit 60e206e587
19 changed files with 212 additions and 32 deletions

View File

@ -58,28 +58,28 @@ namespace osu.Desktop.VisualTests.Tests
{
new OsuHitRenderer
{
Objects = beatmap.HitObjects,
Beatmap = beatmap,
Scale = new Vector2(0.5f),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft
},
new TaikoHitRenderer
{
Objects = beatmap.HitObjects,
Beatmap = beatmap,
Scale = new Vector2(0.5f),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
},
new CatchHitRenderer
{
Objects = beatmap.HitObjects,
Beatmap = beatmap,
Scale = new Vector2(0.5f),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft
},
new ManiaHitRenderer
{
Objects = beatmap.HitObjects,
Beatmap = beatmap,
Scale = new Vector2(0.5f),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight

View File

@ -8,6 +8,7 @@ using osu.Game.Modes.Objects;
using osu.Game.Modes.Osu.Objects;
using osu.Game.Modes.Osu.UI;
using osu.Game.Modes.UI;
using osu.Game.Beatmaps;
namespace osu.Game.Modes.Catch
{
@ -15,7 +16,7 @@ namespace osu.Game.Modes.Catch
{
public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay();
public override HitRenderer CreateHitRendererWith(List<HitObject> objects) => new CatchHitRenderer { Objects = objects };
public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new CatchHitRenderer { Beatmap = beatmap };
protected override PlayMode PlayMode => PlayMode.Catch;

View File

@ -4,16 +4,17 @@
using System.Collections.Generic;
using osu.Game.Modes.Objects;
using osu.Game.Modes.Osu.Objects;
using osu.Game.Beatmaps;
namespace osu.Game.Modes.Catch.Objects
{
class CatchConverter : HitObjectConverter<CatchBaseHit>
{
public override List<CatchBaseHit> Convert(List<HitObject> input)
public override List<CatchBaseHit> Convert(Beatmap beatmap)
{
List<CatchBaseHit> output = new List<CatchBaseHit>();
foreach (HitObject i in input)
foreach (HitObject i in beatmap.HitObjects)
{
CatchBaseHit h = i as CatchBaseHit;

View File

@ -9,6 +9,7 @@ using osu.Game.Modes.Osu;
using osu.Game.Modes.Osu.Objects;
using osu.Game.Modes.Osu.UI;
using osu.Game.Modes.UI;
using osu.Game.Beatmaps;
namespace osu.Game.Modes.Mania
{
@ -16,7 +17,7 @@ namespace osu.Game.Modes.Mania
{
public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay();
public override HitRenderer CreateHitRendererWith(List<HitObject> objects) => new ManiaHitRenderer { Objects = objects };
public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new ManiaHitRenderer { Beatmap = beatmap };
protected override PlayMode PlayMode => PlayMode.Mania;

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using osu.Game.Modes.Objects;
using osu.Game.Modes.Osu.Objects;
using osu.Game.Beatmaps;
namespace osu.Game.Modes.Mania.Objects
{
@ -17,11 +18,11 @@ namespace osu.Game.Modes.Mania.Objects
this.columns = columns;
}
public override List<ManiaBaseHit> Convert(List<HitObject> input)
public override List<ManiaBaseHit> Convert(Beatmap beatmap)
{
List<ManiaBaseHit> output = new List<ManiaBaseHit>();
foreach (HitObject i in input)
foreach (HitObject i in beatmap.HitObjects)
{
ManiaBaseHit h = i as ManiaBaseHit;

View File

@ -28,7 +28,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
osuObject = h;
Origin = Anchor.Centre;
Position = osuObject.Position;
Position = osuObject.StackedPosition;
Scale = new Vector2(osuObject.Scale);
Children = new Drawable[]

View File

@ -31,7 +31,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
{
body = new SliderBody(s)
{
Position = s.Position,
Position = s.StackedPosition,
PathWidth = s.Scale * 64,
},
bouncer1 = new SliderBouncer(s, false)
@ -41,7 +41,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
},
bouncer2 = new SliderBouncer(s, true)
{
Position = s.Position,
Position = s.StackedPosition,
Scale = new Vector2(s.Scale),
},
ball = new SliderBall(s)
@ -51,7 +51,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
initialCircle = new DrawableHitCircle(new HitCircle
{
StartTime = s.StartTime,
Position = s.Position,
Position = s.StackedPosition,
Scale = s.Scale,
Colour = s.Colour,
Sample = s.Sample,

View File

@ -26,7 +26,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
Direction = FlowDirection.VerticalOnly;
Spacing = new Vector2(0, 2);
Position = (h?.EndPosition ?? Vector2.Zero) + judgement.PositionOffset;
Position = (h?.StackedEndPosition ?? Vector2.Zero) + judgement.PositionOffset;
Children = new Drawable[]
{

View File

@ -13,10 +13,18 @@ namespace osu.Game.Modes.Osu.Objects
{
public Vector2 Position { get; set; }
public float Scale { get; set; } = 1;
public Vector2 StackedPosition => Position + StackOffset;
public virtual Vector2 EndPosition => Position;
public Vector2 StackedEndPosition => EndPosition + StackOffset;
public virtual int StackHeight { get; set; }
public Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f);
public float Scale { get; set; } = 1;
public override void SetDefaultsFromBeatmap(Beatmap beatmap)
{
base.SetDefaultsFromBeatmap(beatmap);

View File

@ -3,19 +3,168 @@
using System.Collections.Generic;
using osu.Game.Modes.Objects;
using osu.Game.Beatmaps;
using osu.Game.Modes.Osu.Objects.Drawables;
using OpenTK;
namespace osu.Game.Modes.Osu.Objects
{
public class OsuHitObjectConverter : HitObjectConverter<OsuHitObject>
{
public override List<OsuHitObject> Convert(List<HitObject> input)
public override List<OsuHitObject> Convert(Beatmap beatmap)
{
List<OsuHitObject> output = new List<OsuHitObject>();
foreach (HitObject h in input)
foreach (HitObject h in beatmap.HitObjects)
output.Add(h as OsuHitObject);
UpdateStacking(output, beatmap.BeatmapInfo?.StackLeniency ?? 0.7f);
return output;
}
public static void UpdateStacking(List<OsuHitObject> hitObjects, float stackLeniency, int startIndex = 0, int endIndex = -1)
{
if (endIndex == -1)
endIndex = hitObjects.Count - 1;
int stackDistance = 3;
float stackThreshold = DrawableOsuHitObject.TIME_PREEMPT * stackLeniency;
// Reset stacking inside the update range
for (int i = startIndex; i <= endIndex; i++)
hitObjects[i].StackHeight = 0;
// Extend the end index to include objects they are stacked on
int extendedEndIndex = endIndex;
for (int i = endIndex; i >= startIndex; i--)
{
int stackBaseIndex = i;
for (int n = stackBaseIndex + 1; n < hitObjects.Count; n++)
{
OsuHitObject stackBaseObject = hitObjects[stackBaseIndex];
if (stackBaseObject is Spinner) break;
OsuHitObject objectN = hitObjects[n];
if (objectN is Spinner) continue;
if (objectN.StartTime - stackBaseObject.EndTime > stackThreshold)
//We are no longer within stacking range of the next object.
break;
if (Vector2.Distance(stackBaseObject.Position, objectN.Position) < stackDistance ||
(stackBaseObject is Slider && Vector2.Distance(stackBaseObject.EndPosition, objectN.Position) < stackDistance))
{
stackBaseIndex = n;
// HitObjects after the specified update range haven't been reset yet
objectN.StackHeight = 0;
}
}
if (stackBaseIndex > extendedEndIndex)
{
extendedEndIndex = stackBaseIndex;
if (extendedEndIndex == hitObjects.Count - 1)
break;
}
}
//Reverse pass for stack calculation.
int extendedStartIndex = startIndex;
for (int i = extendedEndIndex; i > startIndex; i--)
{
int n = i;
/* We should check every note which has not yet got a stack.
* Consider the case we have two interwound stacks and this will make sense.
*
* o <-1 o <-2
* o <-3 o <-4
*
* We first process starting from 4 and handle 2,
* then we come backwards on the i loop iteration until we reach 3 and handle 1.
* 2 and 1 will be ignored in the i loop because they already have a stack value.
*/
OsuHitObject objectI = hitObjects[i];
if (objectI.StackHeight != 0 || objectI is Spinner) continue;
/* If this object is a hitcircle, then we enter this "special" case.
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
* Any other case is handled by the "is Slider" code below this.
*/
if (objectI is HitCircle)
{
while (--n >= 0)
{
OsuHitObject objectN = hitObjects[n];
if (objectN is Spinner) continue;
if (objectI.StartTime - objectN.EndTime > stackThreshold)
//We are no longer within stacking range of the previous object.
break;
// HitObjects before the specified update range haven't been reset yet
if (n < extendedStartIndex)
{
objectN.StackHeight = 0;
extendedStartIndex = n;
}
/* This is a special case where hticircles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern.
* o==o <- slider is at original location
* o <- hitCircle has stack of -1
* o <- hitCircle has stack of -2
*/
if (objectN is Slider && Vector2.Distance(objectN.EndPosition, objectI.Position) < stackDistance)
{
int offset = objectI.StackHeight - objectN.StackHeight + 1;
for (int j = n + 1; j <= i; j++)
{
//For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
OsuHitObject objectJ = hitObjects[j];
if (Vector2.Distance(objectN.EndPosition, objectJ.Position) < stackDistance)
objectJ.StackHeight -= offset;
}
//We have hit a slider. We should restart calculation using this as the new base.
//Breaking here will mean that the slider still has StackCount of 0, so will be handled in the i-outer-loop.
break;
}
if (Vector2.Distance(objectN.Position, objectI.Position) < stackDistance)
{
//Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
//NOTE: Sliders with start positions stacking are a special case that is also handled here.
objectN.StackHeight = objectI.StackHeight + 1;
objectI = objectN;
}
}
}
else if (objectI is Slider)
{
/* We have hit the first slider in a possible stack.
* From this point on, we ALWAYS stack positive regardless.
*/
while (--n >= startIndex)
{
OsuHitObject objectN = hitObjects[n];
if (objectN is Spinner) continue;
if (objectI.StartTime - objectN.StartTime > stackThreshold)
//We are no longer within stacking range of the previous object.
break;
if (Vector2.Distance(objectN.EndPosition, objectI.Position) < stackDistance)
{
objectN.StackHeight = objectI.StackHeight + 1;
objectI = objectN;
}
}
}
}
}
}
}

View File

@ -12,6 +12,18 @@ namespace osu.Game.Modes.Osu.Objects
public override Vector2 EndPosition => RepeatCount % 2 == 0 ? Position : Curve.PositionAt(1);
private int stackHeight;
public override int StackHeight
{
get { return stackHeight; }
set
{
stackHeight = value;
if (Curve != null)
Curve.Offset = StackOffset;
}
}
public double Velocity;
public override void SetDefaultsFromBeatmap(Beatmap beatmap)

View File

@ -17,6 +17,8 @@ namespace osu.Game.Modes.Osu.Objects
public CurveTypes CurveType;
public Vector2 Offset;
private List<Vector2> calculatedPath = new List<Vector2>();
private List<double> cumulativeLength = new List<double>();
@ -166,7 +168,8 @@ namespace osu.Game.Modes.Osu.Objects
/// to 1 (end of the slider) and stores the generated path in the given list.
/// </summary>
/// <param name="path">The list to be filled with the computed curve.</param>
/// <param name="progress">Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
/// <param name="p0">Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
public void GetPathToProgress(List<Vector2> path, double p0, double p1)
{
double d0 = progressToDistance(p0);
@ -177,12 +180,12 @@ namespace osu.Game.Modes.Osu.Objects
int i = 0;
for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i);
path.Add(interpolateVertices(i, d0));
path.Add(interpolateVertices(i, d0) + Offset);
for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i)
path.Add(calculatedPath[i]);
path.Add(calculatedPath[i] + Offset);
path.Add(interpolateVertices(i, d1));
path.Add(interpolateVertices(i, d1) + Offset);
}
/// <summary>
@ -194,7 +197,7 @@ namespace osu.Game.Modes.Osu.Objects
public Vector2 PositionAt(double progress)
{
double d = progressToDistance(progress);
return interpolateVertices(indexOfDistance(d), d);
return interpolateVertices(indexOfDistance(d), d) + Offset;
}
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Modes.Osu
{
public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay();
public override HitRenderer CreateHitRendererWith(List<HitObject> objects) => new OsuHitRenderer { Objects = objects };
public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new OsuHitRenderer { Beatmap = beatmap };
public override IEnumerable<BeatmapStatistic> GetBeatmapStatistics(WorkingBeatmap beatmap) => new[]
{

View File

@ -4,16 +4,17 @@
using System.Collections.Generic;
using osu.Game.Modes.Objects;
using osu.Game.Modes.Osu.Objects;
using osu.Game.Beatmaps;
namespace osu.Game.Modes.Taiko.Objects
{
class TaikoConverter : HitObjectConverter<TaikoBaseHit>
{
public override List<TaikoBaseHit> Convert(List<HitObject> input)
public override List<TaikoBaseHit> Convert(Beatmap beatmap)
{
List<TaikoBaseHit> output = new List<TaikoBaseHit>();
foreach (HitObject i in input)
foreach (HitObject i in beatmap.HitObjects)
{
TaikoBaseHit h = i as TaikoBaseHit;

View File

@ -9,6 +9,7 @@ using osu.Game.Modes.Osu.Objects;
using osu.Game.Modes.Osu.UI;
using osu.Game.Modes.Taiko.UI;
using osu.Game.Modes.UI;
using osu.Game.Beatmaps;
namespace osu.Game.Modes.Taiko
{
@ -16,7 +17,7 @@ namespace osu.Game.Modes.Taiko
{
public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay();
public override HitRenderer CreateHitRendererWith(List<HitObject> objects) => new TaikoHitRenderer { Objects = objects };
public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new TaikoHitRenderer { Beatmap = beatmap };
protected override PlayMode PlayMode => PlayMode.Taiko;

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using System;
using System.Collections.Generic;
@ -9,7 +10,7 @@ namespace osu.Game.Modes.Objects
public abstract class HitObjectConverter<T>
where T : HitObject
{
public abstract List<T> Convert(List<HitObject> input);
public abstract List<T> Convert(Beatmap beatmap);
}
public class HitObjectConvertException : Exception

View File

@ -28,7 +28,7 @@ namespace osu.Game.Modes
public abstract ScoreProcessor CreateScoreProcessor(int hitObjectCount);
public abstract HitRenderer CreateHitRendererWith(List<HitObject> objects);
public abstract HitRenderer CreateHitRendererWith(Beatmap beatmap);
public abstract HitObjectParser CreateHitObjectParser();

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Modes.Objects;
using osu.Game.Modes.Objects.Drawables;
using osu.Game.Beatmaps;
namespace osu.Game.Modes.UI
{
@ -37,7 +38,7 @@ namespace osu.Game.Modes.UI
{
private List<T> objects;
public List<HitObject> Objects
public Beatmap Beatmap
{
set
{
@ -51,7 +52,7 @@ namespace osu.Game.Modes.UI
protected abstract HitObjectConverter<T> Converter { get; }
protected virtual List<T> Convert(List<HitObject> objects) => Converter.Convert(objects);
protected virtual List<T> Convert(Beatmap beatmap) => Converter.Convert(beatmap);
public HitRenderer()
{

View File

@ -125,7 +125,7 @@ namespace osu.Game.Screens.Play
OnQuit = Exit
};
hitRenderer = ruleset.CreateHitRendererWith(beatmap.HitObjects);
hitRenderer = ruleset.CreateHitRendererWith(beatmap);
//bind HitRenderer to ScoreProcessor and ourselves (for a pass situation)
hitRenderer.OnJudgement += scoreProcessor.AddJudgement;