Merge branch 'master' into fix-skipbutton-ordering

This commit is contained in:
Dean Herbert
2018-01-15 15:54:18 +09:00
committed by GitHub
121 changed files with 1876 additions and 1582 deletions

View File

@ -0,0 +1,19 @@
// Copyright (c) 2007-2018 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.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.UI
{
public class HitObjectContainer : CompositeDrawable
{
public virtual IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>();
public virtual IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>();
public virtual void Add(DrawableHitObject hitObject) => AddInternal(hitObject);
public virtual bool Remove(DrawableHitObject hitObject) => RemoveInternal(hitObject);
}
}

View File

@ -8,8 +8,6 @@ using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
using osu.Game.Rulesets.Judgements;
using osu.Framework.Allocation;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Rulesets.UI
{
@ -18,7 +16,7 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// The HitObjects contained in this Playfield.
/// </summary>
public HitObjectContainer HitObjects { get; protected set; }
public HitObjectContainer HitObjects { get; private set; }
public Container<Drawable> ScaledContent;
@ -46,16 +44,14 @@ namespace osu.Game.Rulesets.UI
}
}
});
HitObjects = new HitObjectContainer
{
RelativeSizeAxes = Axes.Both,
};
}
[BackgroundDependencyLoader]
private void load()
{
HitObjects = CreateHitObjectContainer();
HitObjects.RelativeSizeAxes = Axes.Both;
Add(HitObjects);
}
@ -89,12 +85,10 @@ namespace osu.Game.Rulesets.UI
/// <param name="judgement">The <see cref="Judgement"/> that occurred.</param>
public virtual void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) { }
public class HitObjectContainer : CompositeDrawable
{
public virtual IEnumerable<DrawableHitObject> Objects => InternalChildren.OfType<DrawableHitObject>();
public virtual void Add(DrawableHitObject hitObject) => AddInternal(hitObject);
public virtual bool Remove(DrawableHitObject hitObject) => RemoveInternal(hitObject);
}
/// <summary>
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
/// </summary>
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
private class ScaledContainer : Container
{

View File

@ -0,0 +1,25 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.UI.Scrolling
{
public enum ScrollingDirection
{
/// <summary>
/// Hitobjects will scroll vertically from the bottom of the hitobject container.
/// </summary>
Up,
/// <summary>
/// Hitobjects will scroll vertically from the top of the hitobject container.
/// </summary>
Down,
/// <summary>
/// Hitobjects will scroll horizontally from the right of the hitobject container.
/// </summary>
Left,
/// <summary>
/// Hitobjects will scroll horizontally from the left of the hitobject container.
/// </summary>
Right
}
}

View File

@ -0,0 +1,116 @@
// Copyright (c) 2007-2018 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.Caching;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Lists;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI.Scrolling.Visualisers;
namespace osu.Game.Rulesets.UI.Scrolling
{
public class ScrollingHitObjectContainer : HitObjectContainer
{
/// <summary>
/// The duration required to scroll through one length of the <see cref="ScrollingHitObjectContainer"/> before any control point adjustments.
/// </summary>
public readonly BindableDouble TimeRange = new BindableDouble
{
MinValue = 0,
MaxValue = double.MaxValue
};
/// <summary>
/// The control points that adjust the scrolling speed.
/// </summary>
protected readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>();
private readonly ScrollingDirection direction;
private Cached initialStateCache = new Cached();
public ScrollingHitObjectContainer(ScrollingDirection direction)
{
this.direction = direction;
RelativeSizeAxes = Axes.Both;
TimeRange.ValueChanged += v => initialStateCache.Invalidate();
}
private ISpeedChangeVisualiser speedChangeVisualiser;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
switch (config.Get<SpeedChangeVisualisationMethod>(OsuSetting.SpeedChangeVisualisation))
{
case SpeedChangeVisualisationMethod.Sequential:
speedChangeVisualiser = new SequentialSpeedChangeVisualiser(ControlPoints);
break;
case SpeedChangeVisualisationMethod.Overlapping:
speedChangeVisualiser = new OverlappingSpeedChangeVisualiser(ControlPoints);
break;
}
}
public override void Add(DrawableHitObject hitObject)
{
initialStateCache.Invalidate();
base.Add(hitObject);
}
public override bool Remove(DrawableHitObject hitObject)
{
var result = base.Remove(hitObject);
if (result)
initialStateCache.Invalidate();
return result;
}
public void AddControlPoint(MultiplierControlPoint controlPoint)
{
ControlPoints.Add(controlPoint);
initialStateCache.Invalidate();
}
public bool RemoveControlPoint(MultiplierControlPoint controlPoint)
{
var result = ControlPoints.Remove(controlPoint);
if (result)
initialStateCache.Invalidate();
return result;
}
public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
{
if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo)) > 0)
initialStateCache.Invalidate();
return base.Invalidate(invalidation, source, shallPropagate);
}
protected override void Update()
{
base.Update();
if (!initialStateCache.IsValid)
{
speedChangeVisualiser.ComputeInitialStates(Objects, direction, TimeRange, DrawSize);
initialStateCache.Validate();
}
}
protected override void UpdateAfterChildrenLife()
{
base.UpdateAfterChildrenLife();
// We need to calculate this as soon as possible after lifetimes so that hitobjects get the final say in their positions
speedChangeVisualiser.ComputePositions(AliveObjects, direction, Time.Current, TimeRange, DrawSize);
}
}
}

View File

@ -0,0 +1,142 @@
// Copyright (c) 2007-2018 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.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK.Input;
namespace osu.Game.Rulesets.UI.Scrolling
{
/// <summary>
/// A type of <see cref="Playfield"/> specialized towards scrolling <see cref="DrawableHitObject"/>s.
/// </summary>
public abstract class ScrollingPlayfield : Playfield
{
/// <summary>
/// The default span of time visible by the length of the scrolling axes.
/// This is clamped between <see cref="time_span_min"/> and <see cref="time_span_max"/>.
/// </summary>
private const double time_span_default = 1500;
/// <summary>
/// The minimum span of time that may be visible by the length of the scrolling axes.
/// </summary>
private const double time_span_min = 50;
/// <summary>
/// The maximum span of time that may be visible by the length of the scrolling axes.
/// </summary>
private const double time_span_max = 10000;
/// <summary>
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
/// </summary>
private const double time_span_step = 50;
/// <summary>
/// The span of time that is visible by the length of the scrolling axes.
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="VisibleTimeRange"/> = 1000.
/// </summary>
public readonly BindableDouble VisibleTimeRange = new BindableDouble(time_span_default)
{
Default = time_span_default,
MinValue = time_span_min,
MaxValue = time_span_max
};
/// <summary>
/// Whether the player can change <see cref="VisibleTimeRange"/>.
/// </summary>
protected virtual bool UserScrollSpeedAdjustment => true;
/// <summary>
/// The container that contains the <see cref="SpeedAdjustmentContainer"/>s and <see cref="DrawableHitObject"/>s.
/// </summary>
public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects;
private readonly ScrollingDirection direction;
/// <summary>
/// Creates a new <see cref="ScrollingPlayfield"/>.
/// </summary>
/// <param name="scrollingAxes">The axes on which <see cref="DrawableHitObject"/>s in this container should scroll.</param>
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width</param>
protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null)
: base(customWidth)
{
this.direction = direction;
}
[BackgroundDependencyLoader]
private void load()
{
HitObjects.TimeRange.BindTo(VisibleTimeRange);
}
private List<ScrollingPlayfield> nestedPlayfields;
/// <summary>
/// All the <see cref="ScrollingPlayfield"/>s nested inside this playfield.
/// </summary>
public IEnumerable<ScrollingPlayfield> NestedPlayfields => nestedPlayfields;
/// <summary>
/// Adds a <see cref="ScrollingPlayfield"/> to this playfield. The nested <see cref="ScrollingPlayfield"/>
/// will be given all of the same speed adjustments as this playfield.
/// </summary>
/// <param name="otherPlayfield">The <see cref="ScrollingPlayfield"/> to add.</param>
protected void AddNested(ScrollingPlayfield otherPlayfield)
{
if (nestedPlayfields == null)
nestedPlayfields = new List<ScrollingPlayfield>();
nestedPlayfields.Add(otherPlayfield);
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (!UserScrollSpeedAdjustment)
return false;
if (state.Keyboard.ControlPressed)
{
switch (args.Key)
{
case Key.Minus:
transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
break;
case Key.Plus:
transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
break;
}
}
return false;
}
private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None)
{
this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing));
}
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(direction);
private class TransformVisibleTimeRange : Transform<double, ScrollingPlayfield>
{
private double valueAt(double time)
{
if (time < StartTime) return StartValue;
if (time >= EndTime) return EndValue;
return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
}
public override string TargetMember => "VisibleTimeRange.Value";
protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time);
protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value;
}
}
}

View File

@ -13,7 +13,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI
namespace osu.Game.Rulesets.UI.Scrolling
{
/// <summary>
/// A type of <see cref="RulesetContainer{TPlayfield,TObject}"/> that supports a <see cref="ScrollingPlayfield"/>.
@ -70,12 +70,10 @@ namespace osu.Game.Rulesets.UI
// 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());
// 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);
DefaultControlPoints.AddRange(timingChanges);
@ -88,7 +86,7 @@ namespace osu.Game.Rulesets.UI
private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield playfield)
{
playfield.HitObjects.AddSpeedAdjustment(CreateSpeedAdjustmentContainer(controlPoint));
playfield.HitObjects.AddControlPoint(controlPoint);
playfield.NestedPlayfields.ForEach(p => applySpeedAdjustment(controlPoint, p));
}
@ -108,12 +106,5 @@ namespace osu.Game.Rulesets.UI
return new MultiplierControlPoint(time, DefaultControlPoints[index].DeepClone());
}
/// <summary>
/// Creates a <see cref="SpeedAdjustmentContainer"/> that facilitates the movement of hit objects.
/// </summary>
/// <param name="controlPoint">The <see cref="MultiplierControlPoint"/> that provides the speed adjustments for the hitobjects.</param>
/// <returns>The <see cref="SpeedAdjustmentContainer"/>.</returns>
protected virtual SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new SpeedAdjustmentContainer(controlPoint);
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) 2007-2018 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.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
{
public interface ISpeedChangeVisualiser
{
/// <summary>
/// Computes the states of <see cref="DrawableHitObject"/>s that are constant, such as lifetime and spatial length.
/// This is invoked once whenever <paramref name="timeRange"/> or <paramref name="length"/> changes.
/// </summary>
/// <param name="hitObjects">The <see cref="DrawableHitObject"/>s whose states should be computed.</param>
/// <param name="direction">The scrolling direction.</param>
/// <param name="timeRange">The duration required to scroll through one length of the screen before any control point adjustments.</param>
/// <param name="length">The length of the screen that is scrolled through.</param>
void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length);
/// <summary>
/// Computes the states of <see cref="DrawableHitObject"/>s that change depending on <paramref name="currentTime"/>, such as position.
/// This is invoked once per frame.
/// </summary>
/// <param name="hitObjects">The <see cref="DrawableHitObject"/>s whose states should be computed.</param>
/// <param name="direction">The scrolling direction.</param>
/// <param name="currentTime">The current time.</param>
/// <param name="timeRange">The duration required to scroll through one length of the screen before any control point adjustments.</param>
/// <param name="length">The length of the screen that is scrolled through.</param>
void ComputePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length);
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) 2007-2018 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.Framework.Lists;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Timing;
using OpenTK;
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
{
public class OverlappingSpeedChangeVisualiser : ISpeedChangeVisualiser
{
private readonly SortedList<MultiplierControlPoint> controlPoints;
public OverlappingSpeedChangeVisualiser(SortedList<MultiplierControlPoint> controlPoints)
{
this.controlPoints = controlPoints;
}
public void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length)
{
foreach (var obj in hitObjects)
{
var controlPoint = controlPointAt(obj.HitObject.StartTime);
obj.LifetimeStart = obj.HitObject.StartTime - timeRange / controlPoint.Multiplier;
if (obj.NestedHitObjects != null)
{
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);
}
}
}
public void ComputePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length)
{
foreach (var obj in hitObjects)
{
var controlPoint = controlPointAt(obj.HitObject.StartTime);
var position = (obj.HitObject.StartTime - currentTime) * controlPoint.Multiplier / timeRange;
switch (direction)
{
case ScrollingDirection.Up:
obj.Y = (float)(position * length.Y);
break;
case ScrollingDirection.Down:
obj.Y = (float)(-position * length.Y);
break;
case ScrollingDirection.Left:
obj.X = (float)(position * length.X);
break;
case ScrollingDirection.Right:
obj.X = (float)(-position * length.X);
break;
}
}
}
private readonly MultiplierControlPoint searchPoint = new MultiplierControlPoint();
private MultiplierControlPoint controlPointAt(double time)
{
if (controlPoints.Count == 0)
return new MultiplierControlPoint(double.NegativeInfinity);
if (time < controlPoints[0].StartTime)
return controlPoints[0];
searchPoint.StartTime = time;
int index = controlPoints.BinarySearch(searchPoint);
if (index < 0)
index = ~index - 1;
return controlPoints[index];
}
}
}

View File

@ -0,0 +1,103 @@
// Copyright (c) 2007-2018 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 osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Timing;
using OpenTK;
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
{
public class SequentialSpeedChangeVisualiser : ISpeedChangeVisualiser
{
private readonly Dictionary<DrawableHitObject, double> hitObjectPositions = new Dictionary<DrawableHitObject, double>();
private readonly IReadOnlyList<MultiplierControlPoint> controlPoints;
public SequentialSpeedChangeVisualiser(IReadOnlyList<MultiplierControlPoint> controlPoints)
{
this.controlPoints = controlPoints;
}
public void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length)
{
foreach (var obj in hitObjects)
{
var startPosition = hitObjectPositions[obj] = positionAt(obj.HitObject.StartTime, timeRange);
obj.LifetimeStart = obj.HitObject.StartTime - timeRange - 1000;
if (obj.HitObject is IHasEndTime endTime)
{
var diff = positionAt(endTime.EndTime, timeRange) - startPosition;
switch (direction)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
obj.Height = (float)(diff * length.Y);
break;
case ScrollingDirection.Left:
case ScrollingDirection.Right:
obj.Width = (float)(diff * length.X);
break;
}
}
if (obj.NestedHitObjects != null)
{
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);
}
}
}
public void ComputePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length)
{
var timelinePosition = positionAt(currentTime, timeRange);
foreach (var obj in hitObjects)
{
var finalPosition = hitObjectPositions[obj] - timelinePosition;
switch (direction)
{
case ScrollingDirection.Up:
obj.Y = (float)(finalPosition * length.Y);
break;
case ScrollingDirection.Down:
obj.Y = (float)(-finalPosition * length.Y);
break;
case ScrollingDirection.Left:
obj.X = (float)(finalPosition * length.X);
break;
case ScrollingDirection.Right:
obj.X = (float)(-finalPosition * length.X);
break;
}
}
}
private double positionAt(double time, double timeRange)
{
double length = 0;
for (int i = 0; i < controlPoints.Count; i++)
{
var current = controlPoints[i];
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null;
if (i > 0 && current.StartTime > time)
continue;
// Duration of the current control point
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
length += Math.Min(currentDuration, time - current.StartTime) * current.Multiplier / timeRange;
}
return length;
}
}
}

View File

@ -1,266 +0,0 @@
// Copyright (c) 2007-2018 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 OpenTK.Input;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// A type of <see cref="Playfield"/> specialized towards scrolling <see cref="DrawableHitObject"/>s.
/// </summary>
public class ScrollingPlayfield : Playfield
{
/// <summary>
/// The default span of time visible by the length of the scrolling axes.
/// This is clamped between <see cref="time_span_min"/> and <see cref="time_span_max"/>.
/// </summary>
private const double time_span_default = 1500;
/// <summary>
/// The minimum span of time that may be visible by the length of the scrolling axes.
/// </summary>
private const double time_span_min = 50;
/// <summary>
/// The maximum span of time that may be visible by the length of the scrolling axes.
/// </summary>
private const double time_span_max = 10000;
/// <summary>
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
/// </summary>
private const double time_span_step = 50;
/// <summary>
/// The span of time that is visible by the length of the scrolling axes.
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="VisibleTimeRange"/> = 1000.
/// </summary>
public readonly BindableDouble VisibleTimeRange = new BindableDouble(time_span_default)
{
Default = time_span_default,
MinValue = time_span_min,
MaxValue = time_span_max
};
/// <summary>
/// Whether to reverse the scrolling direction is reversed. Note that this does _not_ invert the hit objects.
/// </summary>
protected readonly BindableBool Reversed = new BindableBool();
/// <summary>
/// The container that contains the <see cref="SpeedAdjustmentContainer"/>s and <see cref="DrawableHitObject"/>s.
/// </summary>
public new readonly ScrollingHitObjectContainer HitObjects;
/// <summary>
/// Creates a new <see cref="ScrollingPlayfield"/>.
/// </summary>
/// <param name="scrollingAxes">The axes on which <see cref="DrawableHitObject"/>s in this container should scroll.</param>
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width</param>
protected ScrollingPlayfield(Axes scrollingAxes, float? customWidth = null)
: base(customWidth)
{
base.HitObjects = HitObjects = new ScrollingHitObjectContainer(scrollingAxes) { RelativeSizeAxes = Axes.Both };
HitObjects.VisibleTimeRange.BindTo(VisibleTimeRange);
HitObjects.Reversed.BindTo(Reversed);
}
private List<ScrollingPlayfield> nestedPlayfields;
/// <summary>
/// All the <see cref="ScrollingPlayfield"/>s nested inside this playfield.
/// </summary>
public IEnumerable<ScrollingPlayfield> NestedPlayfields => nestedPlayfields;
/// <summary>
/// Adds a <see cref="ScrollingPlayfield"/> to this playfield. The nested <see cref="ScrollingPlayfield"/>
/// will be given all of the same speed adjustments as this playfield.
/// </summary>
/// <param name="otherPlayfield">The <see cref="ScrollingPlayfield"/> to add.</param>
protected void AddNested(ScrollingPlayfield otherPlayfield)
{
if (nestedPlayfields == null)
nestedPlayfields = new List<ScrollingPlayfield>();
nestedPlayfields.Add(otherPlayfield);
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (state.Keyboard.ControlPressed)
{
switch (args.Key)
{
case Key.Minus:
transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
break;
case Key.Plus:
transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
break;
}
}
return false;
}
private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None)
{
this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing));
}
private class TransformVisibleTimeRange : Transform<double, ScrollingPlayfield>
{
private double valueAt(double time)
{
if (time < StartTime) return StartValue;
if (time >= EndTime) return EndValue;
return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
}
public override string TargetMember => "VisibleTimeRange.Value";
protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time);
protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value;
}
/// <summary>
/// A container that provides the foundation for sorting <see cref="DrawableHitObject"/>s into <see cref="SpeedAdjustmentContainer"/>s.
/// </summary>
public class ScrollingHitObjectContainer : HitObjectContainer
{
/// <summary>
/// Gets or sets the range of time that is visible by the length of the scrolling axes.
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="VisibleTimeRange"/> = 1000.
/// </summary>
public readonly BindableDouble VisibleTimeRange = new BindableDouble { Default = 1000 };
/// <summary>
/// Whether to reverse the scrolling direction is reversed.
/// </summary>
public readonly BindableBool Reversed = new BindableBool();
private readonly SortedContainer speedAdjustments;
public IReadOnlyList<SpeedAdjustmentContainer> SpeedAdjustments => speedAdjustments;
private readonly SpeedAdjustmentContainer defaultSpeedAdjustment;
private readonly Axes scrollingAxes;
/// <summary>
/// Creates a new <see cref="ScrollingHitObjectContainer"/>.
/// </summary>
/// <param name="scrollingAxes">The axes upon which hit objects should appear to scroll inside this container.</param>
public ScrollingHitObjectContainer(Axes scrollingAxes)
{
this.scrollingAxes = scrollingAxes;
AddInternal(speedAdjustments = new SortedContainer { RelativeSizeAxes = Axes.Both });
// Default speed adjustment
AddSpeedAdjustment(defaultSpeedAdjustment = new SpeedAdjustmentContainer(new MultiplierControlPoint(0)));
}
/// <summary>
/// Adds a <see cref="SpeedAdjustmentContainer"/> to this container, re-sorting all hit objects
/// in the last <see cref="SpeedAdjustmentContainer"/> that occurred (time-wise) before it.
/// </summary>
/// <param name="speedAdjustment">The <see cref="SpeedAdjustmentContainer"/>.</param>
public void AddSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment)
{
speedAdjustment.ScrollingAxes = scrollingAxes;
speedAdjustment.VisibleTimeRange.BindTo(VisibleTimeRange);
speedAdjustment.Reversed.BindTo(Reversed);
if (speedAdjustments.Count > 0)
{
// We need to re-sort all hit objects in the speed adjustment container prior to figure out if they
// should now lie within this one
var existingAdjustment = adjustmentContainerAt(speedAdjustment.ControlPoint.StartTime);
for (int i = 0; i < existingAdjustment.Count; i++)
{
DrawableHitObject hitObject = existingAdjustment[i];
if (!speedAdjustment.CanContain(hitObject.HitObject.StartTime))
continue;
existingAdjustment.Remove(hitObject);
speedAdjustment.Add(hitObject);
i--;
}
}
speedAdjustments.Add(speedAdjustment);
}
/// <summary>
/// Removes a <see cref="SpeedAdjustmentContainer"/> from this container, re-sorting all hit objects
/// which it contained into new <see cref="SpeedAdjustmentContainer"/>s.
/// </summary>
/// <param name="speedAdjustment">The <see cref="SpeedAdjustmentContainer"/> to remove.</param>
public void RemoveSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment)
{
if (speedAdjustment == defaultSpeedAdjustment)
throw new InvalidOperationException($"The default {nameof(SpeedAdjustmentContainer)} must not be removed.");
if (!speedAdjustments.Remove(speedAdjustment))
return;
while (speedAdjustment.Count > 0)
{
DrawableHitObject hitObject = speedAdjustment[0];
speedAdjustment.Remove(hitObject);
Add(hitObject);
}
}
public override IEnumerable<DrawableHitObject> Objects => speedAdjustments.SelectMany(s => s.Children);
/// <summary>
/// Adds a hit object to this <see cref="ScrollingHitObjectContainer"/>. The hit objects will be queued to be processed
/// new <see cref="SpeedAdjustmentContainer"/>s are added to this <see cref="ScrollingHitObjectContainer"/>.
/// </summary>
/// <param name="hitObject">The hit object to add.</param>
public override void Add(DrawableHitObject hitObject)
{
if (!(hitObject is IScrollingHitObject))
throw new InvalidOperationException($"Hit objects added to a {nameof(ScrollingHitObjectContainer)} must implement {nameof(IScrollingHitObject)}.");
adjustmentContainerAt(hitObject.HitObject.StartTime).Add(hitObject);
}
public override bool Remove(DrawableHitObject hitObject) => speedAdjustments.Any(s => s.Remove(hitObject));
/// <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) => speedAdjustments.FirstOrDefault(c => c.CanContain(time)) ?? defaultSpeedAdjustment;
private class SortedContainer : Container<SpeedAdjustmentContainer>
{
protected override int Compare(Drawable x, Drawable y)
{
var sX = (SpeedAdjustmentContainer)x;
var sY = (SpeedAdjustmentContainer)y;
int result = sY.ControlPoint.StartTime.CompareTo(sX.ControlPoint.StartTime);
if (result != 0)
return result;
return base.Compare(y, x);
}
}
}
}
}