1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 19:22:54 +08:00

Merge branch 'master' into profile.

This commit is contained in:
Huo Yaoyuan 2017-06-16 16:32:54 +08:00
commit af4ddf8fbd
32 changed files with 1164 additions and 347 deletions

@ -1 +1 @@
Subproject commit c80d5f53e740ffe63d9deca41749c5ba0573e744
Subproject commit e256557defe595032cbc3a9896797fa41d6ee8b6

View File

@ -6,14 +6,16 @@ using osu.Framework.Testing;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.UI;
using System;
using System.Collections.Generic;
using OpenTK;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Timing;
using osu.Framework.Configuration;
using OpenTK.Input;
using osu.Framework.Timing;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Linq;
using osu.Game.Rulesets.Mania.Timing;
using osu.Game.Rulesets.Timing;
namespace osu.Desktop.VisualTests.Tests
{
@ -30,7 +32,7 @@ namespace osu.Desktop.VisualTests.Tests
Action<int, SpecialColumnPosition> createPlayfield = (cols, pos) =>
{
Clear();
Add(new ManiaPlayfield(cols, new List<TimingChange>())
Add(new ManiaPlayfield(cols)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -39,37 +41,22 @@ namespace osu.Desktop.VisualTests.Tests
});
};
Action<int, SpecialColumnPosition> createPlayfieldWithNotes = (cols, pos) =>
const double start_time = 500;
const double duration = 500;
Func<double, bool, SpeedAdjustmentContainer> createTimingChange = (time, gravity) => new ManiaSpeedAdjustmentContainer(new MultiplierControlPoint(time)
{
TimingPoint = { BeatLength = 1000 }
}, gravity ? ScrollingAlgorithm.Gravity : ScrollingAlgorithm.Basic);
Action<bool> createPlayfieldWithNotes = gravity =>
{
Clear();
ManiaPlayfield playField;
Add(playField = new ManiaPlayfield(cols, new List<TimingChange> { new TimingChange { BeatLength = 200 } })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
SpecialColumnPosition = pos,
Scale = new Vector2(1, -1)
});
for (int i = 0; i < cols; i++)
{
playField.Add(new DrawableNote(new Note
{
StartTime = Time.Current + 1000,
Column = i
}));
}
};
Action createPlayfieldWithNotesAcceptingInput = () =>
{
Clear();
var rateAdjustClock = new StopwatchClock(true) { Rate = 0.5 };
var rateAdjustClock = new StopwatchClock(true) { Rate = 1 };
ManiaPlayfield playField;
Add(playField = new ManiaPlayfield(4, new List<TimingChange> { new TimingChange { BeatLength = 200 } })
Add(playField = new ManiaPlayfield(4)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -77,14 +64,23 @@ namespace osu.Desktop.VisualTests.Tests
Clock = new FramedClock(rateAdjustClock)
});
for (int t = 1000; t <= 2000; t += 100)
if (!gravity)
playField.Columns.ForEach(c => c.Add(createTimingChange(0, false)));
for (double t = start_time; t <= start_time + duration; t += 100)
{
if (gravity)
playField.Columns.ElementAt(0).Add(createTimingChange(t, true));
playField.Add(new DrawableNote(new Note
{
StartTime = t,
Column = 0
}, new Bindable<Key>(Key.D)));
if (gravity)
playField.Columns.ElementAt(3).Add(createTimingChange(t, true));
playField.Add(new DrawableNote(new Note
{
StartTime = t,
@ -92,17 +88,23 @@ namespace osu.Desktop.VisualTests.Tests
}, new Bindable<Key>(Key.K)));
}
playField.Add(new DrawableHoldNote(new HoldNote
{
StartTime = 1000,
Duration = 1000,
Column = 1
}, new Bindable<Key>(Key.F)));
if (gravity)
playField.Columns.ElementAt(1).Add(createTimingChange(start_time, true));
playField.Add(new DrawableHoldNote(new HoldNote
{
StartTime = 1000,
Duration = 1000,
StartTime = start_time,
Duration = duration,
Column = 1
}, new Bindable<Key>(Key.F)));
if (gravity)
playField.Columns.ElementAt(2).Add(createTimingChange(start_time, true));
playField.Add(new DrawableHoldNote(new HoldNote
{
StartTime = start_time,
Duration = duration,
Column = 2
}, new Bindable<Key>(Key.J)));
};
@ -116,16 +118,11 @@ namespace osu.Desktop.VisualTests.Tests
AddStep("Left special style", () => createPlayfield(8, SpecialColumnPosition.Left));
AddStep("Right special style", () => createPlayfield(8, SpecialColumnPosition.Right));
AddStep("Normal special style", () => createPlayfield(4, SpecialColumnPosition.Normal));
AddStep("Notes with input", () => createPlayfieldWithNotes(false));
AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction));
AddStep("Notes", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Normal));
AddWaitStep(10);
AddStep("Left special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Left));
AddWaitStep(10);
AddStep("Right special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Right));
AddWaitStep(10);
AddStep("Notes with input", () => createPlayfieldWithNotesAcceptingInput());
AddStep("Notes with gravity", () => createPlayfieldWithNotes(true));
AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction));
}
private void triggerKeyDown(Column column)

View File

@ -0,0 +1,220 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Timing;
namespace osu.Desktop.VisualTests.Tests
{
public class TestCaseScrollingHitObjects : TestCase
{
public override string Description => "SpeedAdjustmentContainer/DrawableTimingSection";
private SpeedAdjustmentCollection adjustmentCollection;
private BindableDouble timeRangeBindable;
private OsuSpriteText timeRangeText;
private OsuSpriteText bottomLabel;
private SpriteText topTime, bottomTime;
public override void Reset()
{
base.Reset();
timeRangeBindable = new BindableDouble(2000)
{
MinValue = 200,
MaxValue = 4000,
};
SliderBar<double> timeRange;
Add(timeRange = new BasicSliderBar<double>
{
Size = new Vector2(200, 20),
SelectionColor = Color4.Pink,
KeyboardStep = 100
});
Add(timeRangeText = new OsuSpriteText
{
X = 210,
TextSize = 16,
});
timeRange.Current.BindTo(timeRangeBindable);
timeRangeBindable.ValueChanged += v => timeRangeText.Text = $"Visible Range: {v:#,#.#}";
timeRangeBindable.ValueChanged += v => bottomLabel.Text = $"t minus {v:#,#}";
Add(new Drawable[]
{
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(100, 500),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.25f
},
adjustmentCollection = new SpeedAdjustmentCollection(Axes.Y)
{
RelativeSizeAxes = Axes.Both,
VisibleTimeRange = timeRangeBindable,
Masking = true,
},
new OsuSpriteText
{
Text = "t minus 0",
Margin = new MarginPadding(2),
TextSize = 14,
Anchor = Anchor.TopRight,
},
bottomLabel = new OsuSpriteText
{
Text = "t minus x",
Margin = new MarginPadding(2),
TextSize = 14,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomLeft,
},
topTime = new OsuSpriteText
{
Margin = new MarginPadding(2),
TextSize = 14,
Anchor = Anchor.TopLeft,
Origin = Anchor.TopRight,
},
bottomTime = new OsuSpriteText
{
Margin = new MarginPadding(2),
TextSize = 14,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomRight,
},
}
}
});
timeRangeBindable.TriggerChange();
adjustmentCollection.Add(new TestSpeedAdjustmentContainer(new MultiplierControlPoint()));
AddStep("Add hit object", () => adjustmentCollection.Add(new TestDrawableHitObject(new HitObject { StartTime = Time.Current + 2000 })));
}
protected override void Update()
{
base.Update();
topTime.Text = Time.Current.ToString("#,#");
bottomTime.Text = (Time.Current + timeRangeBindable.Value).ToString("#,#");
}
private class TestSpeedAdjustmentContainer : SpeedAdjustmentContainer
{
public TestSpeedAdjustmentContainer(MultiplierControlPoint controlPoint)
: base(controlPoint)
{
}
protected override DrawableTimingSection CreateTimingSection() => new TestDrawableTimingSection(ControlPoint);
private class TestDrawableTimingSection : DrawableTimingSection
{
private readonly MultiplierControlPoint controlPoint;
public TestDrawableTimingSection(MultiplierControlPoint controlPoint)
{
this.controlPoint = controlPoint;
}
protected override void Update()
{
base.Update();
Y = (float)(controlPoint.StartTime - Time.Current);
}
}
}
private class TestDrawableHitObject : DrawableHitObject
{
private readonly Box background;
private const float height = 14;
public TestDrawableHitObject(HitObject hitObject)
: base(hitObject)
{
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
RelativePositionAxes = Axes.Y;
Y = (float)hitObject.StartTime;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.X,
Height = height,
},
new Box
{
RelativeSizeAxes = Axes.X,
Colour = Color4.Cyan,
Height = 1,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Black,
TextSize = height,
Font = @"Exo2.0-BoldItalic",
Text = $"{hitObject.StartTime:#,#}"
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
FadeInFromZero(250, EasingTypes.OutQuint);
}
private bool hasExpired;
protected override void Update()
{
base.Update();
if (Time.Current >= HitObject.StartTime)
{
background.Colour = Color4.Red;
if (!hasExpired)
{
using (BeginDelayedSequence(200))
{
FadeOut(200);
Expire();
}
hasExpired = true;
}
}
}
}
}
}

View File

@ -207,6 +207,7 @@
<Compile Include="Tests\TestCaseReplay.cs" />
<Compile Include="Tests\TestCaseResults.cs" />
<Compile Include="Tests\TestCaseScoreCounter.cs" />
<Compile Include="Tests\TestCaseScrollingHitObjects.cs" />
<Compile Include="Tests\TestCaseSkipButton.cs" />
<Compile Include="Tests\TestCaseTabControl.cs" />
<Compile Include="Tests\TestCaseTaikoHitObjects.cs" />

View File

@ -96,6 +96,7 @@ namespace osu.Game.Rulesets.Mania
new ModCinema(),
},
},
new ManiaModGravity()
};
default:

View File

@ -0,0 +1,23 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.Mods
{
/// <summary>
/// A type of mod which generates speed adjustments that scroll the hit objects and bar lines.
/// </summary>
internal interface IGenerateSpeedAdjustments
{
/// <summary>
/// Applies this mod to a hit renderer.
/// </summary>
/// <param name="hitRenderer">The hit renderer to apply to.</param>
/// <param name="hitObjectTimingChanges">The per-column list of speed adjustments for hit objects.</param>
/// <param name="barlineTimingChanges">The list of speed adjustments for bar lines.</param>
void ApplyToHitRenderer(ManiaHitRenderer hitRenderer, ref List<SpeedAdjustmentContainer>[] hitObjectTimingChanges, ref List<SpeedAdjustmentContainer> barlineTimingChanges);
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Timing;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModGravity : Mod, IGenerateSpeedAdjustments
{
public override string Name => "Gravity";
public override double ScoreMultiplier => 0;
public override FontAwesome Icon => FontAwesome.fa_sort_desc;
public void ApplyToHitRenderer(ManiaHitRenderer hitRenderer, ref List<SpeedAdjustmentContainer>[] hitObjectTimingChanges, ref List<SpeedAdjustmentContainer> barlineTimingChanges)
{
// We have to generate one speed adjustment per hit object for gravity
foreach (ManiaHitObject obj in hitRenderer.Objects)
{
MultiplierControlPoint controlPoint = hitRenderer.CreateControlPointAt(obj.StartTime);
// Beat length has too large of an effect for gravity, so we'll force it to a constant value for now
controlPoint.TimingPoint.BeatLength = 1000;
hitObjectTimingChanges[obj.Column].Add(new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Gravity));
}
// Like with hit objects, we need to generate one speed adjustment per bar line
foreach (DrawableBarLine barLine in hitRenderer.BarLines)
{
var controlPoint = hitRenderer.CreateControlPointAt(barLine.HitObject.StartTime);
// Beat length has too large of an effect for gravity, so we'll force it to a constant value for now
controlPoint.TimingPoint.BeatLength = 1000;
barlineTimingChanges.Add(new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Gravity));
}
}
}
}

View File

@ -55,6 +55,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
tickContainer = new Container<DrawableHoldNoteTick>
{
RelativeSizeAxes = Axes.Both,
RelativeChildOffset = new Vector2(0, (float)HitObject.StartTime),
RelativeChildSize = new Vector2(1, (float)HitObject.Duration)
},
head = new DrawableHeadNote(this, key)
@ -76,9 +77,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
HoldStartTime = () => holdStartTime
};
// To make the ticks relative to ourselves we need to offset them backwards
drawableTick.Y -= (float)HitObject.StartTime;
tickContainer.Add(drawableTick);
AddNested(drawableTick);
}

View File

@ -6,6 +6,7 @@ using OpenTK.Input;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
@ -32,6 +33,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Y = (float)HitObject.StartTime;
}
protected override void LoadComplete()
{
base.LoadComplete();
LifetimeStart = HitObject.StartTime - ManiaPlayfield.TIME_SPAN_MAX;
}
public override Color4 AccentColour
{
get { return base.AccentColour; }

View File

@ -0,0 +1,27 @@
// 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.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.Timing
{
/// <summary>
/// A <see cref="DrawableTimingSection"/> which scrolls relative to the control point start time.
/// </summary>
internal class BasicScrollingDrawableTimingSection : DrawableTimingSection
{
private readonly MultiplierControlPoint controlPoint;
public BasicScrollingDrawableTimingSection(MultiplierControlPoint controlPoint)
{
this.controlPoint = controlPoint;
}
protected override void Update()
{
base.Update();
Y = (float)(controlPoint.StartTime - Time.Current);
}
}
}

View File

@ -1,156 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Timing
{
/// <summary>
/// A container in which added drawables are put into a relative coordinate space spanned by a length of time.
/// <para>
/// This container contains <see cref="ControlPoint"/>s which scroll inside this container.
/// Drawables added to this container are moved inside the relevant <see cref="ControlPoint"/>,
/// and as such, will scroll along with the <see cref="ControlPoint"/>s.
/// </para>
/// </summary>
public class ControlPointContainer : Container<Drawable>
{
/// <summary>
/// The amount of time which this container spans.
/// </summary>
public double TimeSpan { get; set; }
private readonly List<DrawableControlPoint> drawableControlPoints;
public ControlPointContainer(IEnumerable<TimingChange> timingChanges)
{
drawableControlPoints = timingChanges.Select(t => new DrawableControlPoint(t)).ToList();
Children = drawableControlPoints;
}
/// <summary>
/// Adds a drawable to this container. Note that the drawable added must have its Y-position be
/// an absolute unit of time that is _not_ relative to <see cref="TimeSpan"/>.
/// </summary>
/// <param name="drawable">The drawable to add.</param>
public override void Add(Drawable drawable)
{
// Always add timing sections to ourselves
if (drawable is DrawableControlPoint)
{
base.Add(drawable);
return;
}
var controlPoint = drawableControlPoints.LastOrDefault(t => t.CanContain(drawable)) ?? drawableControlPoints.FirstOrDefault();
if (controlPoint == null)
throw new InvalidOperationException("Could not find suitable timing section to add object to.");
controlPoint.Add(drawable);
}
/// <summary>
/// A container that contains drawables within the time span of a timing section.
/// <para>
/// The content of this container will scroll relative to the current time.
/// </para>
/// </summary>
private class DrawableControlPoint : Container
{
private readonly TimingChange timingChange;
protected override Container<Drawable> Content => content;
private readonly Container content;
/// <summary>
/// Creates a drawable control point. The height of this container will be proportional
/// to the beat length of the control point it is initialized with such that, e.g. a beat length
/// of 500ms results in this container being twice as high as its parent, which further means that
/// the content container will scroll at twice the normal rate.
/// </summary>
/// <param name="timingChange">The control point to create the drawable control point for.</param>
public DrawableControlPoint(TimingChange timingChange)
{
this.timingChange = timingChange;
RelativeSizeAxes = Axes.Both;
AddInternal(content = new AutoTimeRelativeContainer
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Y = (float)timingChange.Time
});
}
protected override void Update()
{
var parent = (ControlPointContainer)Parent;
// Adjust our height to account for the speed changes
Height = (float)(1000 / timingChange.BeatLength / timingChange.SpeedMultiplier);
RelativeChildSize = new Vector2(1, (float)parent.TimeSpan);
// Scroll the content
content.Y = (float)(timingChange.Time - Time.Current);
}
public override void Add(Drawable drawable)
{
// The previously relatively-positioned drawable will now become relative to content, but since the drawable has no knowledge of content,
// we need to offset it back by content's position position so that it becomes correctly relatively-positioned to content
// This can be removed if hit objects were stored such that either their StartTime or their "beat offset" was relative to the timing change
// they belonged to, but this requires a radical change to the beatmap format which we're not ready to do just yet
drawable.Y -= (float)timingChange.Time;
base.Add(drawable);
}
/// <summary>
/// Whether this control point can contain a drawable. This control point can contain a drawable if the drawable is positioned "after" this control point.
/// </summary>
/// <param name="drawable">The drawable to check.</param>
public bool CanContain(Drawable drawable) => content.Y <= drawable.Y;
/// <summary>
/// A container which always keeps its height and relative coordinate space "auto-sized" to its children.
/// <para>
/// This is used in the case where children are relatively positioned/sized to time values (e.g. notes/bar lines) to keep
/// such children wrapped inside a container, otherwise they would disappear due to container flattening.
/// </para>
/// </summary>
private class AutoTimeRelativeContainer : Container
{
protected override IComparer<Drawable> DepthComparer => new HitObjectReverseStartTimeComparer();
public override void InvalidateFromChild(Invalidation invalidation)
{
// We only want to re-compute our size when a child's size or position has changed
if ((invalidation & Invalidation.RequiredParentSizeToFit) == 0)
{
base.InvalidateFromChild(invalidation);
return;
}
if (!Children.Any())
return;
float height = Children.Select(child => child.Y + child.Height).Max();
Height = height;
RelativeChildSize = new Vector2(1, height);
base.InvalidateFromChild(invalidation);
}
}
}
}
}

View File

@ -0,0 +1,60 @@
// 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.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.Timing
{
/// <summary>
/// A <see cref="DrawableTimingSection"/> that emulates a form of gravity where hit objects speed up over time.
/// </summary>
internal class GravityScrollingDrawableTimingSection : DrawableTimingSection
{
private readonly MultiplierControlPoint controlPoint;
public GravityScrollingDrawableTimingSection(MultiplierControlPoint controlPoint)
{
this.controlPoint = controlPoint;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
// The gravity-adjusted start position
float startPos = (float)computeGravityTime(controlPoint.StartTime);
// The gravity-adjusted end position
float endPos = (float)computeGravityTime(controlPoint.StartTime + RelativeChildSize.Y);
Y = startPos;
Height = endPos - startPos;
}
/// <summary>
/// Applies gravity to a time value based on the current time.
/// </summary>
/// <param name="time">The time value gravity should be applied to.</param>
/// <returns>The time after gravity is applied to <paramref name="time"/>.</returns>
private double computeGravityTime(double time)
{
double relativeTime = relativeTimeAt(time);
// The sign of the relative time, this is used to apply backwards acceleration leading into startTime
double sign = relativeTime < 0 ? -1 : 1;
return VisibleTimeRange - acceleration * relativeTime * relativeTime * sign;
}
/// <summary>
/// The acceleration due to "gravity" of the content of this container.
/// </summary>
private double acceleration => 1 / VisibleTimeRange;
/// <summary>
/// Computes the current time relative to <paramref name="time"/>, accounting for <see cref="DrawableTimingSection.VisibleTimeRange"/>.
/// </summary>
/// <param name="time">The non-offset time.</param>
/// <returns>The current time relative to <paramref name="time"/> - <see cref="DrawableTimingSection.VisibleTimeRange"/>. </returns>
private double relativeTimeAt(double time) => Time.Current - time + VisibleTimeRange;
}
}

View File

@ -0,0 +1,30 @@
// 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.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.Timing
{
public class ManiaSpeedAdjustmentContainer : SpeedAdjustmentContainer
{
private readonly ScrollingAlgorithm scrollingAlgorithm;
public ManiaSpeedAdjustmentContainer(MultiplierControlPoint timingSection, ScrollingAlgorithm scrollingAlgorithm)
: base(timingSection)
{
this.scrollingAlgorithm = scrollingAlgorithm;
}
protected override DrawableTimingSection CreateTimingSection()
{
switch (scrollingAlgorithm)
{
default:
case ScrollingAlgorithm.Basic:
return new BasicScrollingDrawableTimingSection(ControlPoint);
case ScrollingAlgorithm.Gravity:
return new GravityScrollingDrawableTimingSection(ControlPoint);
}
}
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Timing
{
public enum ScrollingAlgorithm
{
/// <summary>
/// Basic scrolling algorithm based on the timing section time. This is the default algorithm.
/// </summary>
Basic,
/// <summary>
/// Emulating a form of gravity where hit objects speed up over time.
/// </summary>
Gravity
}
}

View File

@ -1,23 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Timing
{
public class TimingChange
{
/// <summary>
/// The time at which this timing change happened.
/// </summary>
public double Time;
/// <summary>
/// The beat length.
/// </summary>
public double BeatLength = 500;
/// <summary>
/// The speed multiplier.
/// </summary>
public double SpeedMultiplier = 1;
}
}

View File

@ -11,13 +11,10 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Colour;
using osu.Framework.Input;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Timing;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Judgements;
using System;
using osu.Framework.Configuration;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.UI
{
@ -33,6 +30,13 @@ namespace osu.Game.Rulesets.Mania.UI
private const float column_width = 45;
private const float special_column_width = 70;
private readonly BindableDouble visibleTimeRange = new BindableDouble();
public BindableDouble VisibleTimeRange
{
get { return visibleTimeRange; }
set { visibleTimeRange.BindTo(value); }
}
/// <summary>
/// The key that will trigger input actions for this column and hit objects contained inside it.
/// </summary>
@ -42,9 +46,9 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly Container hitTargetBar;
private readonly Container keyIcon;
public readonly ControlPointContainer ControlPointContainer;
private readonly SpeedAdjustmentCollection speedAdjustments;
public Column(IEnumerable<TimingChange> timingChanges)
public Column()
{
RelativeSizeAxes = Axes.Y;
Width = column_width;
@ -93,10 +97,11 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
},
ControlPointContainer = new ControlPointContainer(timingChanges)
speedAdjustments = new SpeedAdjustmentCollection(Axes.Y)
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
VisibleTimeRange = VisibleTimeRange
},
// For column lighting, we need to capture input events before the notes
new InputTarget
@ -187,10 +192,11 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
public void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> hitObject)
public void Add(SpeedAdjustmentContainer speedAdjustment) => speedAdjustments.Add(speedAdjustment);
public void Add(DrawableHitObject hitObject)
{
hitObject.AccentColour = AccentColour;
ControlPointContainer.Add(hitObject);
speedAdjustments.Add(hitObject);
}
private bool onKeyDown(InputState state, KeyDownEventArgs args)

View File

@ -8,6 +8,7 @@ using OpenTK;
using OpenTK.Input;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Lists;
using osu.Framework.MathUtils;
@ -16,6 +17,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Scoring;
@ -23,78 +25,44 @@ using osu.Game.Rulesets.Mania.Timing;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaHitRenderer : HitRenderer<ManiaHitObject, ManiaJudgement>
public class ManiaHitRenderer : SpeedAdjustedHitRenderer<ManiaHitObject, ManiaJudgement>
{
public int? Columns;
/// <summary>
/// Preferred column count. This will only have an effect during the initialization of the play field.
/// </summary>
public int PreferredColumns;
public IEnumerable<DrawableBarLine> BarLines;
/// <summary>
/// Per-column timing changes.
/// </summary>
private readonly List<SpeedAdjustmentContainer>[] hitObjectSpeedAdjustments;
/// <summary>
/// Bar line timing changes.
/// </summary>
private readonly List<SpeedAdjustmentContainer> barLineSpeedAdjustments = new List<SpeedAdjustmentContainer>();
public ManiaHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(beatmap, isForCurrentRuleset)
{
}
protected override Playfield<ManiaHitObject, ManiaJudgement> CreatePlayfield()
{
double lastSpeedMultiplier = 1;
double lastBeatLength = 500;
// Merge timing + difficulty points
var allPoints = new SortedList<ControlPoint>(Comparer<ControlPoint>.Default);
allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints);
allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints);
// Generate the timing points, making non-timing changes use the previous timing change
var timingChanges = allPoints.Select(c =>
{
var timingPoint = c as TimingControlPoint;
var difficultyPoint = c as DifficultyControlPoint;
if (timingPoint != null)
lastBeatLength = timingPoint.BeatLength;
if (difficultyPoint != null)
lastSpeedMultiplier = difficultyPoint.SpeedMultiplier;
return new TimingChange
{
Time = c.Time,
BeatLength = lastBeatLength,
SpeedMultiplier = lastSpeedMultiplier
};
});
double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
// Perform some post processing of the timing changes
timingChanges = timingChanges
// Collapse sections after the last hit object
.Where(s => s.Time <= lastObjectTime)
// Collapse sections with the same start time
.GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time)
// Collapse sections with the same beat length
.GroupBy(s => s.BeatLength * s.SpeedMultiplier).Select(g => g.First())
.ToList();
return new ManiaPlayfield(Columns ?? (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize), timingChanges)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
// Invert by default for now (should be moved to config/skin later)
Scale = new Vector2(1, -1)
};
}
[BackgroundDependencyLoader]
private void load()
{
var maniaPlayfield = (ManiaPlayfield)Playfield;
// Generate the speed adjustment container lists
hitObjectSpeedAdjustments = new List<SpeedAdjustmentContainer>[PreferredColumns];
for (int i = 0; i < PreferredColumns; i++)
hitObjectSpeedAdjustments[i] = new List<SpeedAdjustmentContainer>();
// Generate the bar lines
double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
SortedList<TimingControlPoint> timingPoints = Beatmap.ControlPointInfo.TimingPoints;
var barLines = new List<DrawableBarLine>();
for (int i = 0; i < timingPoints.Count; i++)
{
TimingControlPoint point = timingPoints[i];
@ -105,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI
int index = 0;
for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++)
{
maniaPlayfield.Add(new DrawableBarLine(new BarLine
barLines.Add(new DrawableBarLine(new BarLine
{
StartTime = t,
ControlPoint = point,
@ -113,17 +81,78 @@ namespace osu.Game.Rulesets.Mania.UI
}));
}
}
BarLines = barLines;
// Generate speed adjustments from mods first
bool useDefaultSpeedAdjustments = true;
if (Mods != null)
{
foreach (var speedAdjustmentMod in Mods.OfType<IGenerateSpeedAdjustments>())
{
useDefaultSpeedAdjustments = false;
speedAdjustmentMod.ApplyToHitRenderer(this, ref hitObjectSpeedAdjustments, ref barLineSpeedAdjustments);
}
}
// Generate the default speed adjustments
if (useDefaultSpeedAdjustments)
generateDefaultSpeedAdjustments();
}
[BackgroundDependencyLoader]
private void load()
{
var maniaPlayfield = (ManiaPlayfield)Playfield;
BarLines.ForEach(maniaPlayfield.Add);
}
protected override void ApplyBeatmap()
{
base.ApplyBeatmap();
PreferredColumns = (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize);
}
protected override void ApplySpeedAdjustments()
{
var maniaPlayfield = (ManiaPlayfield)Playfield;
for (int i = 0; i < PreferredColumns; i++)
foreach (var change in hitObjectSpeedAdjustments[i])
maniaPlayfield.Columns.ElementAt(i).Add(change);
foreach (var change in barLineSpeedAdjustments)
maniaPlayfield.Add(change);
}
private void generateDefaultSpeedAdjustments()
{
DefaultControlPoints.ForEach(c =>
{
foreach (List<SpeedAdjustmentContainer> t in hitObjectSpeedAdjustments)
t.Add(new ManiaSpeedAdjustmentContainer(c, ScrollingAlgorithm.Basic));
barLineSpeedAdjustments.Add(new ManiaSpeedAdjustmentContainer(c, ScrollingAlgorithm.Basic));
});
}
protected sealed override Playfield<ManiaHitObject, ManiaJudgement> CreatePlayfield() => new ManiaPlayfield(PreferredColumns)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
// Invert by default for now (should be moved to config/skin later)
Scale = new Vector2(1, -1)
};
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter() => new ManiaBeatmapConverter();
protected override DrawableHitObject<ManiaHitObject, ManiaJudgement> GetVisualRepresentation(ManiaHitObject h)
{
var maniaPlayfield = Playfield as ManiaPlayfield;
if (maniaPlayfield == null)
return null;
var maniaPlayfield = (ManiaPlayfield)Playfield;
Bindable<Key> key = maniaPlayfield.Columns.ElementAt(h.Column).Key;

View File

@ -15,13 +15,13 @@ using osu.Framework.Allocation;
using OpenTK.Input;
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Mania.Timing;
using osu.Framework.Input;
using osu.Framework.Graphics.Transforms;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Timing;
using osu.Framework.Configuration;
namespace osu.Game.Rulesets.Mania.UI
{
@ -29,10 +29,10 @@ namespace osu.Game.Rulesets.Mania.UI
{
public const float HIT_TARGET_POSITION = 50;
private const float time_span_default = 5000;
private const float time_span_min = 10;
private const float time_span_max = 50000;
private const float time_span_step = 200;
private const double time_span_default = 1500;
public const double TIME_SPAN_MIN = 50;
public const double TIME_SPAN_MAX = 10000;
private const double time_span_step = 50;
/// <summary>
/// Default column keys, expanding outwards from the middle as more column are added.
@ -58,14 +58,20 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly FlowContainer<Column> columns;
public IEnumerable<Column> Columns => columns.Children;
private readonly ControlPointContainer barLineContainer;
private readonly BindableDouble visibleTimeRange = new BindableDouble(time_span_default)
{
MinValue = TIME_SPAN_MIN,
MaxValue = TIME_SPAN_MAX
};
private readonly SpeedAdjustmentCollection barLineContainer;
private List<Color4> normalColumnColours = new List<Color4>();
private Color4 specialColumnColour;
private readonly int columnCount;
public ManiaPlayfield(int columnCount, IEnumerable<TimingChange> timingChanges)
public ManiaPlayfield(int columnCount)
{
this.columnCount = columnCount;
@ -116,12 +122,13 @@ namespace osu.Game.Rulesets.Mania.UI
Padding = new MarginPadding { Top = HIT_TARGET_POSITION },
Children = new[]
{
barLineContainer = new ControlPointContainer(timingChanges)
barLineContainer = new SpeedAdjustmentCollection(Axes.Y)
{
Name = "Bar lines",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y
RelativeSizeAxes = Axes.Y,
VisibleTimeRange = visibleTimeRange
// Width is set in the Update method
}
}
@ -131,9 +138,7 @@ namespace osu.Game.Rulesets.Mania.UI
};
for (int i = 0; i < columnCount; i++)
columns.Add(new Column(timingChanges));
TimeSpan = time_span_default;
columns.Add(new Column { VisibleTimeRange = visibleTimeRange });
}
[BackgroundDependencyLoader]
@ -208,6 +213,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> h) => Columns.ElementAt(h.HitObject.Column).Add(h);
public void Add(DrawableBarLine barline) => barLineContainer.Add(barline);
public void Add(SpeedAdjustmentContainer speedAdjustment) => barLineContainer.Add(speedAdjustment);
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
@ -216,10 +222,10 @@ namespace osu.Game.Rulesets.Mania.UI
switch (args.Key)
{
case Key.Minus:
transformTimeSpanTo(TimeSpan + time_span_step, 200, EasingTypes.OutQuint);
transformVisibleTimeRangeTo(visibleTimeRange + time_span_step, 200, EasingTypes.OutQuint);
break;
case Key.Plus:
transformTimeSpanTo(TimeSpan - time_span_step, 200, EasingTypes.OutQuint);
transformVisibleTimeRangeTo(visibleTimeRange - time_span_step, 200, EasingTypes.OutQuint);
break;
}
}
@ -227,29 +233,9 @@ namespace osu.Game.Rulesets.Mania.UI
return false;
}
private double timeSpan;
/// <summary>
/// The amount of time which the length of the playfield spans.
/// </summary>
public double TimeSpan
private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, EasingTypes easing = EasingTypes.None)
{
get { return timeSpan; }
set
{
if (timeSpan == value)
return;
timeSpan = value;
timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max);
barLineContainer.TimeSpan = value;
Columns.ForEach(c => c.ControlPointContainer.TimeSpan = value);
}
}
private void transformTimeSpanTo(double newTimeSpan, double duration = 0, EasingTypes easing = EasingTypes.None)
{
TransformTo(() => TimeSpan, newTimeSpan, duration, easing, new TransformTimeSpan());
TransformTo(() => visibleTimeRange.Value, newTimeRange, duration, easing, new TransformTimeSpan());
}
protected override void Update()
@ -278,7 +264,7 @@ namespace osu.Game.Rulesets.Mania.UI
base.Apply(d);
var p = (ManiaPlayfield)d;
p.TimeSpan = CurrentValue;
p.visibleTimeRange.Value = (float)CurrentValue;
}
}
}

View File

@ -63,6 +63,7 @@
<Compile Include="Judgements\ManiaHitResult.cs" />
<Compile Include="Judgements\ManiaJudgement.cs" />
<Compile Include="ManiaDifficultyCalculator.cs" />
<Compile Include="Mods\IGenerateSpeedAdjustments.cs" />
<Compile Include="Objects\Drawables\DrawableBarLine.cs" />
<Compile Include="Objects\Drawables\DrawableHoldNote.cs" />
<Compile Include="Objects\Drawables\DrawableHoldNoteTick.cs" />
@ -78,14 +79,17 @@
<Compile Include="Objects\ManiaHitObject.cs" />
<Compile Include="Objects\Note.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Timing\BasicScrollingDrawableTimingSection.cs" />
<Compile Include="Timing\GravityScrollingDrawableTimingSection.cs" />
<Compile Include="Timing\ScrollingAlgorithm.cs" />
<Compile Include="UI\Column.cs" />
<Compile Include="UI\ManiaHitRenderer.cs" />
<Compile Include="UI\ManiaPlayfield.cs" />
<Compile Include="ManiaRuleset.cs" />
<Compile Include="Mods\ManiaMod.cs" />
<Compile Include="Mods\ManiaModGravity.cs" />
<Compile Include="UI\SpecialColumnPosition.cs" />
<Compile Include="Timing\ControlPointContainer.cs" />
<Compile Include="Timing\TimingChange.cs" />
<Compile Include="Timing\ManiaSpeedAdjustmentContainer.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj">

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE

View File

@ -15,6 +15,6 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// The beat length at this control point.
/// </summary>
public double BeatLength = 500;
public double BeatLength = 1000;
}
}

View File

@ -10,7 +10,7 @@ namespace osu.Game.Graphics.Containers
{
public class ReverseDepthFillFlowContainer<T> : FillFlowContainer<T> where T : Drawable
{
protected override IComparer<Drawable> DepthComparer => new ReverseCreationOrderDepthComparer();
protected override IComparer<Drawable> DepthComparer => new ReverseCreationOrderDepthComparer();
protected override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.Reverse();
}
}

View File

@ -45,6 +45,7 @@ namespace osu.Game.Graphics.UserInterface
case Key.Up:
case Key.Down:
return false;
case Key.KeypadEnter:
case Key.Enter:
if (!AllowCommit) return false;
break;

View File

@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Dialog
{
if (args.Repeat) return false;
if (args.Key == Key.Enter)
if (args.Key == Key.Enter || args.Key == Key.KeypadEnter)
{
Buttons.OfType<PopupDialogOkButton>().FirstOrDefault()?.TriggerOnClick();
return true;

View File

@ -0,0 +1,111 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Caching;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.Timing
{
/// <summary>
/// A collection of hit objects which scrolls within a <see cref="SpeedAdjustmentContainer"/>.
///
/// <para>
/// This container handles the conversion between time and position through <see cref="Container{T}.RelativeChildSize"/> and
/// <see cref="Container{T}.RelativeChildOffset"/> such that hit objects added to this container should have time values set as their
/// positions/sizes to make proper use of this container.
/// </para>
///
/// <para>
/// This container will auto-size to the total duration of the contained hit objects along the desired auto-sizing axes such that the resulting size
/// of this container will be a value representing the total duration of all contained hit objects.
/// </para>
///
/// <para>
/// This container is and must always be relatively-sized and positioned to its such that the parent can utilise <see cref="Container{T}.RelativeChildSize"/>
/// and <see cref="Container{T}.RelativeChildOffset"/> to apply further time offsets to this collection of hit objects.
/// </para>
/// </summary>
public abstract class DrawableTimingSection : Container<DrawableHitObject>
{
private readonly BindableDouble visibleTimeRange = new BindableDouble();
/// <summary>
/// Gets or sets the range of time that is visible by the length of this container.
/// </summary>
public BindableDouble VisibleTimeRange
{
get { return visibleTimeRange; }
set { visibleTimeRange.BindTo(value); }
}
protected override IComparer<Drawable> DepthComparer => new HitObjectReverseStartTimeComparer();
/// <summary>
/// Axes through which this timing section scrolls. This is set from <see cref="SpeedAdjustmentContainer"/>.
/// </summary>
internal Axes ScrollingAxes;
private Cached layout = new Cached();
/// <summary>
/// Creates a new <see cref="DrawableTimingSection"/>.
/// </summary>
protected DrawableTimingSection()
{
RelativeSizeAxes = Axes.Both;
RelativePositionAxes = Axes.Both;
}
public override void InvalidateFromChild(Invalidation invalidation)
{
// We only want to re-compute our size when a child's size or position has changed
if ((invalidation & Invalidation.RequiredParentSizeToFit) == 0)
{
base.InvalidateFromChild(invalidation);
return;
}
layout.Invalidate();
base.InvalidateFromChild(invalidation);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (!layout.EnsureValid())
{
layout.Refresh(() =>
{
if (!Children.Any())
return;
//double maxDuration = Children.Select(c => (c.HitObject as IHasEndTime)?.EndTime ?? c.HitObject.StartTime).Max();
//float width = (float)maxDuration - RelativeChildOffset.X;
//float height = (float)maxDuration - RelativeChildOffset.Y;
// Auto-size to the total size of our children
// This ends up being the total duration of our children, however for now this is a more sure-fire way to calculate this
// than the above due to some undesired masking optimisations causing some hit objects to be culled...
// Todo: When this is investigated more we should use the above method as it is a little more exact
// Todo: This is not working correctly in the case that hit objects are absolutely-sized - needs a proper looking into in osu!framework
float width = Children.Select(child => child.X + child.Width).Max() - RelativeChildOffset.X;
float height = Children.Select(child => child.Y + child.Height).Max() - RelativeChildOffset.Y;
// Consider that width/height are time values. To have ourselves span these time values 1:1, we first need to set our size
Size = new Vector2((ScrollingAxes & Axes.X) > 0 ? width : Size.X, (ScrollingAxes & Axes.Y) > 0 ? height : Size.Y);
// Then to make our position-space be time values again, we need our relative child size to follow our size
RelativeChildSize = Size;
});
}
}
}
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO.Serialization;
namespace osu.Game.Rulesets.Timing
{
/// <summary>
/// A control point which adds an aggregated multiplier based on the provided <see cref="TimingPoint"/>'s BeatLength and <see cref="DifficultyPoint"/>'s SpeedMultiplier.
/// </summary>
public class MultiplierControlPoint : IJsonSerializable, IComparable<MultiplierControlPoint>
{
/// <summary>
/// The time in milliseconds at which this <see cref="MultiplierControlPoint"/> starts.
/// </summary>
public readonly double StartTime;
/// <summary>
/// The multiplier which this <see cref="MultiplierControlPoint"/> provides.
/// </summary>
public double Multiplier => 1000 / TimingPoint.BeatLength / DifficultyPoint.SpeedMultiplier;
/// <summary>
/// The <see cref="TimingControlPoint"/> that provides the timing information for this <see cref="MultiplierControlPoint"/>.
/// </summary>
public TimingControlPoint TimingPoint = new TimingControlPoint();
/// <summary>
/// The <see cref="DifficultyControlPoint"/> that provides additional difficulty information for this <see cref="MultiplierControlPoint"/>.
/// </summary>
public DifficultyControlPoint DifficultyPoint = new DifficultyControlPoint();
/// <summary>
/// Creates a <see cref="MultiplierControlPoint"/>. This is required for JSON serialization
/// </summary>
public MultiplierControlPoint()
{
}
/// <summary>
/// Creates a <see cref="MultiplierControlPoint"/>.
/// </summary>
/// <param name="startTime">The start time of this <see cref="MultiplierControlPoint"/>.</param>
public MultiplierControlPoint(double startTime)
{
StartTime = startTime;
}
/// <summary>
/// Creates a <see cref="MultiplierControlPoint"/> by copying another <see cref="MultiplierControlPoint"/>.
/// </summary>
/// <param name="startTime">The start time of this <see cref="MultiplierControlPoint"/>.</param>
/// <param name="other">The <see cref="MultiplierControlPoint"/> to copy.</param>
public MultiplierControlPoint(double startTime, MultiplierControlPoint other)
: this(startTime)
{
TimingPoint = other.TimingPoint;
DifficultyPoint = other.DifficultyPoint;
}
public int CompareTo(MultiplierControlPoint other) => StartTime.CompareTo(other?.StartTime);
}
}

View File

@ -0,0 +1,139 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Timing
{
/// <summary>
/// A collection of <see cref="SpeedAdjustmentContainer"/>s.
///
/// <para>
/// This container redirects any <see cref="DrawableHitObject"/>'s added to it to the <see cref="SpeedAdjustmentContainer"/>
/// which provides the speed adjustment active at the start time of the hit object. Furthermore, this container provides the
/// necessary <see cref="VisibleTimeRange"/> for the contained <see cref="SpeedAdjustmentContainer"/>s.
/// </para>
/// </summary>
public class SpeedAdjustmentCollection : Container<SpeedAdjustmentContainer>
{
private readonly BindableDouble visibleTimeRange = new BindableDouble();
/// <summary>
/// Gets or sets the range of time that is visible by the length of this container.
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="VisibleTimeRange"/> = 1000.
/// </summary>
public Bindable<double> VisibleTimeRange
{
get { return visibleTimeRange; }
set { visibleTimeRange.BindTo(value); }
}
protected override IComparer<Drawable> DepthComparer => new SpeedAdjustmentContainerReverseStartTimeComparer();
/// <summary>
/// Hit objects that are to be re-processed on the next update.
/// </summary>
private readonly Queue<DrawableHitObject> queuedHitObjects = new Queue<DrawableHitObject>();
private readonly Axes scrollingAxes;
/// <summary>
/// Creates a new <see cref="SpeedAdjustmentCollection"/>.
/// </summary>
/// <param name="scrollingAxes">The axes upon which hit objects should appear to scroll inside this container.</param>
public SpeedAdjustmentCollection(Axes scrollingAxes)
{
this.scrollingAxes = scrollingAxes;
}
/// <summary>
/// Adds a hit object to this <see cref="SpeedAdjustmentCollection"/>. The hit objects will be kept in a queue
/// and will be processed when new <see cref="SpeedAdjustmentContainer"/>s are added to this <see cref="SpeedAdjustmentCollection"/>.
/// </summary>
/// <param name="hitObject">The hit object to add.</param>
public void Add(DrawableHitObject hitObject)
{
queuedHitObjects.Enqueue(hitObject);
}
public override void Add(SpeedAdjustmentContainer speedAdjustment)
{
speedAdjustment.VisibleTimeRange.BindTo(VisibleTimeRange);
speedAdjustment.ScrollingAxes = scrollingAxes;
base.Add(speedAdjustment);
}
protected override void Update()
{
base.Update();
// Todo: At the moment this is going to re-process every single Update, however this will only be a null-op
// when there are no SpeedAdjustmentContainers available. This should probably error or something, but it's okay for now.
// An external count is kept because hit objects that can't be added are re-queued
int count = queuedHitObjects.Count;
while (count-- > 0)
{
var hitObject = queuedHitObjects.Dequeue();
var target = adjustmentContainerFor(hitObject);
if (target == null)
{
// We can't add this hit object to a speed adjustment container yet, so re-queue it
// for re-processing when the layout next invalidated
queuedHitObjects.Enqueue(hitObject);
continue;
}
if (hitObject.RelativePositionAxes != target.ScrollingAxes)
throw new InvalidOperationException($"Make sure to set all {nameof(DrawableHitObject)}'s {nameof(RelativePositionAxes)} are equal to the correct axes of scrolling ({target.ScrollingAxes}).");
target.Add(hitObject);
}
}
/// <summary>
/// Finds the <see cref="SpeedAdjustmentContainer"/> which provides the speed adjustment active at the start time
/// of a hit object. If there is no <see cref="SpeedAdjustmentContainer"/> active at the start time of the hit object,
/// then the first (time-wise) speed adjustment is returned.
/// </summary>
/// <param name="hitObject">The hit object to find the active <see cref="SpeedAdjustmentContainer"/> for.</param>
/// <returns>The <see cref="SpeedAdjustmentContainer"/> active at <paramref name="hitObject"/>'s start time. Null if there are no speed adjustments.</returns>
private SpeedAdjustmentContainer adjustmentContainerFor(DrawableHitObject hitObject) => Children.FirstOrDefault(c => c.CanContain(hitObject)) ?? Children.LastOrDefault();
/// <summary>
/// Finds the <see cref="SpeedAdjustmentContainer"/> which provides the speed adjustment active at a time.
/// If there is no <see cref="SpeedAdjustmentContainer"/> active at the time, then the first (time-wise) speed adjustment is returned.
/// </summary>
/// <param name="time">The time to find the active <see cref="SpeedAdjustmentContainer"/> at.</param>
/// <returns>The <see cref="SpeedAdjustmentContainer"/> active at <paramref name="time"/>. Null if there are no speed adjustments.</returns>
private SpeedAdjustmentContainer adjustmentContainerAt(double time) => Children.FirstOrDefault(c => c.CanContain(time)) ?? Children.LastOrDefault();
/// <summary>
/// Compares two speed adjustment containers by their control point start time, falling back to creation order
// if their control point start time is equal. This will compare the two speed adjustment containers in reverse order.
/// </summary>
private class SpeedAdjustmentContainerReverseStartTimeComparer : ReverseCreationOrderDepthComparer
{
public override int Compare(Drawable x, Drawable y)
{
var speedAdjustmentX = x as SpeedAdjustmentContainer;
var speedAdjustmentY = y as SpeedAdjustmentContainer;
// If either of the two drawables are not hit objects, fall back to the base comparer
if (speedAdjustmentX?.ControlPoint == null || speedAdjustmentY?.ControlPoint == null)
return base.Compare(x, y);
// Compare by start time
int i = speedAdjustmentY.ControlPoint.StartTime.CompareTo(speedAdjustmentX.ControlPoint.StartTime);
return i != 0 ? i : base.Compare(x, y);
}
}
}
}

View File

@ -0,0 +1,93 @@
// 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.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.Timing
{
/// <summary>
/// A container for hit objects which applies applies the speed adjustments defined by the properties of a <see cref="Timing.MultiplierControlPoint"/>
/// to affect the scroll speed of the contained <see cref="DrawableTimingSection"/>.
///
/// <para>
/// This container must always be relatively-sized to its parent to provide the speed adjustments. This container will provide the speed adjustments
/// by modifying its size while maintaining a constant <see cref="Container{T}.RelativeChildSize"/> for its children
/// </para>
/// </summary>
public abstract class SpeedAdjustmentContainer : Container<DrawableHitObject>
{
private readonly Bindable<double> visibleTimeRange = new Bindable<double>();
/// <summary>
/// Gets or sets the range of time that is visible by the length of this container.
/// </summary>
public Bindable<double> VisibleTimeRange
{
get { return visibleTimeRange; }
set { visibleTimeRange.BindTo(value); }
}
protected override Container<DrawableHitObject> Content => content;
private Container<DrawableHitObject> content;
/// <summary>
/// Axes which the content of this container will scroll through.
/// </summary>
/// <returns></returns>
public Axes ScrollingAxes { get; internal set; }
public readonly MultiplierControlPoint ControlPoint;
/// <summary>
/// Creates a new <see cref="SpeedAdjustmentContainer"/>.
/// </summary>
/// <param name="controlPoint">The <see cref="MultiplierControlPoint"/> which provides the speed adjustments for this container.</param>
protected SpeedAdjustmentContainer(MultiplierControlPoint controlPoint)
{
ControlPoint = controlPoint;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
DrawableTimingSection timingSection = CreateTimingSection();
timingSection.ScrollingAxes = ScrollingAxes;
timingSection.VisibleTimeRange.BindTo(VisibleTimeRange);
timingSection.RelativeChildOffset = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)ControlPoint.StartTime : 0, (ScrollingAxes & Axes.Y) > 0 ? (float)ControlPoint.StartTime : 0);
AddInternal(content = timingSection);
}
protected override void Update()
{
float multiplier = (float)ControlPoint.Multiplier;
// The speed adjustment happens by modifying our size by the multiplier while maintaining the visible time range as the relatve size for our children
Size = new Vector2((ScrollingAxes & Axes.X) > 0 ? multiplier : 1, (ScrollingAxes & Axes.Y) > 0 ? multiplier : 1);
RelativeChildSize = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)VisibleTimeRange : 1, (ScrollingAxes & Axes.Y) > 0 ? (float)VisibleTimeRange : 1);
}
/// <summary>
/// Whether this speed adjustment can contain a hit object. This is true if the hit object occurs after this speed adjustment with respect to time.
/// </summary>
public bool CanContain(DrawableHitObject hitObject) => CanContain(hitObject.HitObject.StartTime);
/// <summary>
/// Whether this speed adjustment can contain an object placed at a time value. This is true if the time occurs after this speed adjustment.
/// </summary>
public bool CanContain(double startTime) => ControlPoint.StartTime <= startTime;
/// <summary>
/// Creates the container which handles the movement of a collection of hit objects.
/// </summary>
/// <returns>The <see cref="DrawableTimingSection"/>.</returns>
protected abstract DrawableTimingSection CreateTimingSection();
}
}

View File

@ -120,6 +120,16 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public Beatmap<TObject> Beatmap;
/// <summary>
/// All the converted hit objects contained by this hit renderer.
/// </summary>
public override IEnumerable<HitObject> Objects => Beatmap.HitObjects;
/// <summary>
/// The mods which are to be applied.
/// </summary>
protected IEnumerable<Mod> Mods;
/// <summary>
/// Creates a hit renderer for a beatmap.
/// </summary>
@ -129,6 +139,8 @@ namespace osu.Game.Rulesets.UI
{
Debug.Assert(beatmap != null, "HitRenderer initialized with a null beatmap.");
Mods = beatmap.Mods.Value;
RelativeSizeAxes = Axes.Both;
BeatmapConverter<TObject> converter = CreateBeatmapConverter();
@ -148,6 +160,8 @@ namespace osu.Game.Rulesets.UI
// Post-process the beatmap
processor.PostProcess(Beatmap);
ApplyBeatmap();
// Add mods, should always be the last thing applied to give full control to mods
applyMods(beatmap.Mods.Value);
}
@ -165,6 +179,11 @@ namespace osu.Game.Rulesets.UI
mod.ApplyToHitRenderer(this);
}
/// <summary>
/// Called when the beatmap of this hit renderer has been set. Used to apply any default values from the beatmap.
/// </summary>
protected virtual void ApplyBeatmap() { }
/// <summary>
/// Creates a processor to perform post-processing operations
/// on HitObjects in converted Beatmaps.
@ -192,7 +211,10 @@ namespace osu.Game.Rulesets.UI
public sealed override bool ProvidingUserCursor => !HasReplayLoaded && Playfield.ProvidingUserCursor;
public override IEnumerable<HitObject> Objects => Beatmap.HitObjects;
/// <summary>
/// All the converted hit objects contained by this hit renderer.
/// </summary>
public new IEnumerable<TObject> Objects => Beatmap.HitObjects;
protected override bool AllObjectsJudged => drawableObjects.All(h => h.Judged);
@ -234,6 +256,9 @@ namespace osu.Game.Rulesets.UI
InputManager.ReplayInputHandler.ToScreenSpace = Playfield.ScaledContent.ToScreenSpace;
}
/// <summary>
/// Creates and adds drawable representations of hit objects to the play field.
/// </summary>
private void loadObjects()
{
drawableObjects.Capacity = Beatmap.HitObjects.Count;

View File

@ -0,0 +1,104 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Lists;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// A type of <see cref="HitRenderer{TObject, TJudgement}"/> that supports speed adjustments in some capacity.
/// </summary>
public abstract class SpeedAdjustedHitRenderer<TObject, TJudgement> : HitRenderer<TObject, TJudgement>
where TObject : HitObject
where TJudgement : Judgement
{
protected readonly SortedList<MultiplierControlPoint> DefaultControlPoints = new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default);
protected SpeedAdjustedHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(beatmap, isForCurrentRuleset)
{
}
[BackgroundDependencyLoader]
private void load()
{
ApplySpeedAdjustments();
}
protected override void ApplyBeatmap()
{
// Calculate default multiplier control points
var lastTimingPoint = new TimingControlPoint();
var lastDifficultyPoint = new DifficultyControlPoint();
// Merge timing + difficulty points
var allPoints = new SortedList<ControlPoint>(Comparer<ControlPoint>.Default);
allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints);
allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints);
// Generate the timing points, making non-timing changes use the previous timing change
var timingChanges = allPoints.Select(c =>
{
var timingPoint = c as TimingControlPoint;
var difficultyPoint = c as DifficultyControlPoint;
if (timingPoint != null)
lastTimingPoint = timingPoint;
if (difficultyPoint != null)
lastDifficultyPoint = difficultyPoint;
return new MultiplierControlPoint(c.Time)
{
TimingPoint = lastTimingPoint,
DifficultyPoint = lastDifficultyPoint
};
});
double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
// Perform some post processing of the timing changes
timingChanges = timingChanges
// Collapse sections after the last hit object
.Where(s => s.StartTime <= lastObjectTime)
// Collapse sections with the same start time
.GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime)
// Collapse sections with the same beat length
.GroupBy(s => s.TimingPoint.BeatLength * s.DifficultyPoint.SpeedMultiplier).Select(g => g.First());
DefaultControlPoints.AddRange(timingChanges);
}
/// <summary>
/// Generates a control point with the default timing change/difficulty change from the beatmap at a time.
/// </summary>
/// <param name="time">The time to create the control point at.</param>
/// <returns>The <see cref="MultiplierControlPoint"/> at <paramref name="time"/>.</returns>
public MultiplierControlPoint CreateControlPointAt(double time)
{
if (DefaultControlPoints.Count == 0)
return new MultiplierControlPoint(time);
int index = DefaultControlPoints.BinarySearch(new MultiplierControlPoint(time));
if (index < 0)
return new MultiplierControlPoint(time);
return new MultiplierControlPoint(time, DefaultControlPoints[index].DeepClone());
}
/// <summary>
/// Applies speed changes to the playfield.
/// </summary>
protected abstract void ApplySpeedAdjustments();
}
}

View File

@ -371,6 +371,7 @@ namespace osu.Game.Screens.Select
switch (args.Key)
{
case Key.KeypadEnter:
case Key.Enter:
raiseSelect();
return true;

View File

@ -215,6 +215,10 @@
<Compile Include="Database\RulesetDatabase.cs" />
<Compile Include="Rulesets\Scoring\Score.cs" />
<Compile Include="Rulesets\Scoring\ScoreProcessor.cs" />
<Compile Include="Rulesets\Timing\SpeedAdjustmentContainer.cs" />
<Compile Include="Rulesets\Timing\DrawableTimingSection.cs" />
<Compile Include="Rulesets\Timing\MultiplierControlPoint.cs" />
<Compile Include="Rulesets\Timing\SpeedAdjustmentCollection.cs" />
<Compile Include="Screens\Menu\MenuSideFlashes.cs" />
<Compile Include="Screens\Play\HUD\HealthDisplay.cs" />
<Compile Include="Screens\Play\HUDOverlay.cs" />
@ -324,6 +328,7 @@
<Compile Include="Screens\Select\SongSelect.cs" />
<Compile Include="Rulesets\UI\HitRenderer.cs" />
<Compile Include="Rulesets\UI\Playfield.cs" />
<Compile Include="Rulesets\UI\SpeedAdjustedHitRenderer.cs" />
<Compile Include="Screens\Select\EditSongSelect.cs" />
<Compile Include="Screens\Play\HUD\ComboCounter.cs" />
<Compile Include="Screens\Play\HUD\ComboResultCounter.cs" />