// Copyright (c) 2007-2017 ppy Pty Ltd . // 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.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Timing; namespace osu.Game.Rulesets.UI { /// /// A type of specialized towards scrolling s. /// public class ScrollingPlayfield : Playfield where TObject : HitObject where TJudgement : Judgement { /// /// The default span of time visible by the length of the scrolling axes. /// This is clamped between and . /// private const double time_span_default = 1500; /// /// The minimum span of time that may be visible by the length of the scrolling axes. /// private const double time_span_min = 50; /// /// The maximum span of time that may be visible by the length of the scrolling axes. /// private const double time_span_max = 10000; /// /// The step increase/decrease of the span of time visible by the length of the scrolling axes. /// private const double time_span_step = 50; /// /// 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 = 1000. /// public readonly BindableDouble VisibleTimeRange = new BindableDouble(time_span_default) { Default = time_span_default, MinValue = time_span_min, MaxValue = time_span_max }; /// /// Whether to reverse the scrolling direction is reversed. Note that this does _not_ invert the hit objects. /// protected readonly BindableBool Reversed = new BindableBool(); /// /// The container that contains the s and s. /// public new readonly ScrollingHitObjectContainer HitObjects; /// /// Creates a new . /// /// The axes on which s in this container should scroll. /// Whether we want our internal coordinate system to be scaled to a specified width 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> nestedPlayfields; /// /// All the s nested inside this playfield. /// public IEnumerable> NestedPlayfields => nestedPlayfields; /// /// Adds a to this playfield. The nested /// will be given all of the same speed adjustments as this playfield. /// /// The to add. protected void AddNested(ScrollingPlayfield otherPlayfield) { if (nestedPlayfields == null) nestedPlayfields = new List>(); 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> { 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; } /// /// A container that provides the foundation for sorting s into s. /// public class ScrollingHitObjectContainer : HitObjectContainer { /// /// 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 = 1000. /// public readonly BindableDouble VisibleTimeRange = new BindableDouble { Default = 1000 }; /// /// Whether to reverse the scrolling direction is reversed. /// public readonly BindableBool Reversed = new BindableBool(); private readonly SortedContainer speedAdjustments; public IReadOnlyList SpeedAdjustments => speedAdjustments; private readonly SpeedAdjustmentContainer defaultSpeedAdjustment; private readonly Axes scrollingAxes; /// /// Creates a new . /// /// The axes upon which hit objects should appear to scroll inside this container. public ScrollingHitObjectContainer(Axes scrollingAxes) { this.scrollingAxes = scrollingAxes; AddInternal(speedAdjustments = new SortedContainer { RelativeSizeAxes = Axes.Both }); // Default speed adjustment AddSpeedAdjustment(defaultSpeedAdjustment = new SpeedAdjustmentContainer(new MultiplierControlPoint(0))); } /// /// Adds a to this container, re-sorting all hit objects /// in the last that occurred (time-wise) before it. /// /// The . 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); } /// /// Removes a from this container, re-sorting all hit objects /// which it contained into new s. /// /// The to remove. 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 Objects => speedAdjustments.SelectMany(s => s.Children); /// /// Adds a hit object to this . The hit objects will be queued to be processed /// new s are added to this . /// /// The hit object to add. 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)); /// /// Finds the which provides the speed adjustment active at a time. /// If there is no active at the time, then the first (time-wise) speed adjustment is returned. /// /// The time to find the active at. /// The active at . Null if there are no speed adjustments. private SpeedAdjustmentContainer adjustmentContainerAt(double time) => speedAdjustments.FirstOrDefault(c => c.CanContain(time)) ?? defaultSpeedAdjustment; private class SortedContainer : Container { 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); } } } } }