diff --git a/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs b/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs new file mode 100644 index 0000000000..363b0b481e --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.MathUtils; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Modes.Objects; +using osu.Game.Screens.Play; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseSongProgress : TestCase + { + public override string Description => @"With fake data"; + + private SongProgress progress; + + public override void Reset() + { + base.Reset(); + + Add(progress = new SongProgress + { + AudioClock = new StopwatchClock(true), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }); + + AddStep("Toggle Bar", progress.ToggleBar); + AddWaitStep(5); + AddStep("Toggle Bar", progress.ToggleBar); + AddWaitStep(2); + AddRepeatStep("New Values", displayNewValues, 5); + + displayNewValues(); + } + + private void displayNewValues() + { + List objects = new List(); + for (double i = 0; i < 5000; i += RNG.NextDouble() * 10 + i / 1000) + objects.Add(new HitObject { StartTime = i }); + + progress.Objects = objects; + } + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index ffafa219a8..d75bb94308 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -207,6 +207,7 @@ + diff --git a/osu.Game/Modes/UI/HitRenderer.cs b/osu.Game/Modes/UI/HitRenderer.cs index dd5eff5a95..6962c80d87 100644 --- a/osu.Game/Modes/UI/HitRenderer.cs +++ b/osu.Game/Modes/UI/HitRenderer.cs @@ -58,6 +58,8 @@ namespace osu.Game.Modes.UI /// public bool HasReplayLoaded => InputManager.ReplayInputHandler != null; + public abstract IEnumerable Objects { get; } + /// /// Whether all the HitObjects have been judged. /// @@ -185,6 +187,8 @@ namespace osu.Game.Modes.UI private readonly Container content; + public override IEnumerable Objects => Beatmap.HitObjects; + protected HitRenderer(WorkingBeatmap beatmap) : base(beatmap) { diff --git a/osu.Game/Modes/UI/HudOverlay.cs b/osu.Game/Modes/UI/HudOverlay.cs index 0e1a3fb1c2..4902baf9b9 100644 --- a/osu.Game/Modes/UI/HudOverlay.cs +++ b/osu.Game/Modes/UI/HudOverlay.cs @@ -26,6 +26,7 @@ namespace osu.Game.Modes.UI public readonly ScoreCounter ScoreCounter; public readonly RollingCounter AccuracyCounter; public readonly HealthDisplay HealthDisplay; + public readonly SongProgress Progress; private Bindable showKeyCounter; private Bindable showHud; @@ -37,6 +38,7 @@ namespace osu.Game.Modes.UI protected abstract RollingCounter CreateAccuracyCounter(); protected abstract ScoreCounter CreateScoreCounter(); protected abstract HealthDisplay CreateHealthDisplay(); + protected abstract SongProgress CreateProgress(); protected HudOverlay() { @@ -53,6 +55,7 @@ namespace osu.Game.Modes.UI ScoreCounter = CreateScoreCounter(), AccuracyCounter = CreateAccuracyCounter(), HealthDisplay = CreateHealthDisplay(), + Progress = CreateProgress(), } }); } diff --git a/osu.Game/Modes/UI/StandardHudOverlay.cs b/osu.Game/Modes/UI/StandardHudOverlay.cs index cb8e6c1808..161a700bef 100644 --- a/osu.Game/Modes/UI/StandardHudOverlay.cs +++ b/osu.Game/Modes/UI/StandardHudOverlay.cs @@ -57,6 +57,13 @@ namespace osu.Game.Modes.UI Position = new Vector2(0, 30), }; + protected override SongProgress CreateProgress() => new SongProgress() + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + }; + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Overlays/DragBar.cs b/osu.Game/Overlays/DragBar.cs index 53a01c9e9c..bb28f08553 100644 --- a/osu.Game/Overlays/DragBar.cs +++ b/osu.Game/Overlays/DragBar.cs @@ -13,7 +13,7 @@ namespace osu.Game.Overlays { public class DragBar : Container { - private readonly Box fill; + protected readonly Container Fill; public Action SeekRequested; @@ -27,7 +27,7 @@ namespace osu.Game.Overlays { enabled = value; if (!enabled) - fill.Width = 0; + Fill.Width = 0; } } @@ -37,12 +37,20 @@ namespace osu.Game.Overlays Children = new Drawable[] { - fill = new Box + Fill = new Container { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, + Name = "FillContainer", + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - Width = 0 + Width = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both + } + } } }; } @@ -51,21 +59,23 @@ namespace osu.Game.Overlays { if (IsSeeking || !IsEnabled) return; - updatePosition(position); + updatePosition(position, false); } private void seek(InputState state) { - if (!IsEnabled) return; float seekLocation = state.Mouse.Position.X / DrawWidth; + + if (!IsEnabled) return; + SeekRequested?.Invoke(seekLocation); updatePosition(seekLocation); } - private void updatePosition(float position) + private void updatePosition(float position, bool easing = true) { position = MathHelper.Clamp(position, 0, 1); - fill.TransformTo(() => fill.Width, position, 200, EasingTypes.OutQuint, new TransformSeek()); + Fill.TransformTo(() => Fill.Width, position, easing ? 200 : 0, EasingTypes.OutQuint, new TransformSeek()); } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 01d5d0770a..fa564cdd61 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -63,9 +63,7 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(AudioManager audio, BeatmapDatabase beatmaps, OsuConfigManager config) { - var beatmap = Beatmap.Beatmap; - - if (beatmap.BeatmapInfo?.Mode > PlayMode.Taiko) + if (Beatmap.Beatmap.BeatmapInfo?.Mode > PlayMode.Taiko) { //we only support osu! mode for now because the hitobject parsing is crappy and needs a refactor. Exit(); @@ -126,6 +124,9 @@ namespace osu.Game.Screens.Play hudOverlay.BindProcessor(scoreProcessor); hudOverlay.BindHitRenderer(HitRenderer); + hudOverlay.Progress.Objects = HitRenderer.Objects; + hudOverlay.Progress.AudioClock = interpolatedSourceClock; + //bind HitRenderer to ScoreProcessor and ourselves (for a pass situation) HitRenderer.OnAllJudged += onCompletion; @@ -225,6 +226,7 @@ namespace osu.Game.Screens.Play lastPauseActionTime = Time.Current; hudOverlay.KeyCounter.IsCounting = false; + hudOverlay.Progress.Show(); pauseOverlay.Retries = RestartCount; pauseOverlay.Show(); }); @@ -234,6 +236,7 @@ namespace osu.Game.Screens.Play { lastPauseActionTime = Time.Current; hudOverlay.KeyCounter.IsCounting = true; + hudOverlay.Progress.Hide(); pauseOverlay.Hide(); sourceClock.Start(); } diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs new file mode 100644 index 0000000000..52f3b9e1ae --- /dev/null +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -0,0 +1,142 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using System; +using System.Collections.Generic; +using osu.Game.Graphics; +using osu.Framework.Allocation; +using System.Linq; +using osu.Framework.Timing; +using osu.Game.Modes.Objects; +using osu.Game.Modes.Objects.Types; + +namespace osu.Game.Screens.Play +{ + public class SongProgress : OverlayContainer + { + private const int progress_height = 5; + + protected override bool HideOnEscape => false; + + private static readonly Vector2 handle_size = new Vector2(14, 25); + + private const float transition_duration = 200; + + private readonly SongProgressBar bar; + private readonly SongProgressGraph graph; + + public Action OnSeek; + + public IClock AudioClock; + + private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1; + + private IEnumerable objects; + + public IEnumerable Objects + { + set + { + objects = value; + + const int granularity = 200; + + var interval = lastHitTime / granularity; + + var values = new int[granularity]; + + foreach (var h in objects) + { + IHasEndTime end = h as IHasEndTime; + + int startRange = (int)(h.StartTime / interval); + int endRange = (int)((end?.EndTime ?? h.StartTime) / interval); + for (int i = startRange; i <= endRange; i++) + values[i]++; + } + + graph.Values = values; + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + graph.FillColour = bar.FillColour = colours.BlueLighter; + } + + public SongProgress() + { + RelativeSizeAxes = Axes.X; + Height = progress_height + SongProgressGraph.Column.HEIGHT + handle_size.Y; + Y = progress_height; + + Children = new Drawable[] + { + graph = new SongProgressGraph + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Height = SongProgressGraph.Column.HEIGHT, + Margin = new MarginPadding { Bottom = progress_height }, + }, + bar = new SongProgressBar(progress_height, SongProgressGraph.Column.HEIGHT, handle_size) + { + Alpha = 0, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + SeekRequested = delegate (float position) + { + OnSeek?.Invoke(position); + }, + }, + }; + } + + protected override void LoadComplete() + { + State = Visibility.Visible; + } + + private bool barVisible; + + public void ToggleBar() + { + barVisible = !barVisible; + updateBarVisibility(); + } + + private void updateBarVisibility() + { + bar.FadeTo(barVisible ? 1 : 0, transition_duration, EasingTypes.In); + MoveTo(new Vector2(0, barVisible ? 0 : progress_height), transition_duration, EasingTypes.In); + } + + protected override void PopIn() + { + updateBarVisibility(); + FadeIn(500, EasingTypes.OutQuint); + } + + protected override void PopOut() + { + FadeOut(100); + } + + protected override void Update() + { + base.Update(); + + double progress = (AudioClock?.CurrentTime ?? Time.Current) / lastHitTime; + + bar.UpdatePosition((float)progress); + graph.Progress = (int)(graph.ColumnCount * progress); + + } + } +} diff --git a/osu.Game/Screens/Play/SongProgressBar.cs b/osu.Game/Screens/Play/SongProgressBar.cs new file mode 100644 index 0000000000..d0fb3c8a3d --- /dev/null +++ b/osu.Game/Screens/Play/SongProgressBar.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Game.Overlays; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; + +namespace osu.Game.Screens.Play +{ + public class SongProgressBar : DragBar + { + public Color4 FillColour + { + get { return Fill.Colour; } + set { Fill.Colour = value; } + } + + public SongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize) + { + Height = barHeight + handleBarHeight + handleSize.Y; + + Fill.RelativeSizeAxes = Axes.X; + Fill.Height = barHeight; + + Add(new Box + { + Name = "Background", + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = barHeight, + Colour = Color4.Black, + Alpha = 0.5f, + Depth = 1 + }); + + Fill.Add(new Container + { + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + Width = 2, + Height = barHeight + handleBarHeight, + Colour = Color4.White, + Position = new Vector2(2, 0), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.TopCentre, + Size = handleSize, + CornerRadius = 5, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White + } + } + } + } + }); + } + } +} diff --git a/osu.Game/Screens/Play/SongProgressGraph.cs b/osu.Game/Screens/Play/SongProgressGraph.cs new file mode 100644 index 0000000000..4f0cdbf67a --- /dev/null +++ b/osu.Game/Screens/Play/SongProgressGraph.cs @@ -0,0 +1,235 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using System.Linq; +using System.Collections.Generic; +using osu.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Containers; +using osu.Framework.Extensions.Color4Extensions; + +namespace osu.Game.Screens.Play +{ + public class SongProgressGraph : BufferedContainer + { + private Column[] columns = { }; + + public int ColumnCount => columns.Length; + + public override bool HandleInput => false; + + private int progress; + public int Progress + { + get { return progress; } + set + { + if (value == progress) return; + progress = value; + + redrawProgress(); + } + } + + private int[] calculatedValues = { }; // values but adjusted to fit the amount of columns + private int[] values; + public int[] Values + { + get { return values; } + set + { + if (value == values) return; + values = value; + recreateGraph(); + } + } + + private Color4 fillColour; + public Color4 FillColour + { + get { return fillColour; } + set + { + if (value == fillColour) return; + fillColour = value; + + redrawFilled(); + } + } + + public SongProgressGraph() + { + CacheDrawnFrameBuffer = true; + PixelSnapping = true; + } + + private float lastDrawWidth; + protected override void Update() + { + base.Update(); + + // todo: Recreating in update is probably not the best idea + if (DrawWidth == lastDrawWidth) return; + recreateGraph(); + lastDrawWidth = DrawWidth; + } + + /// + /// Redraws all the columns to match their lit/dimmed state. + /// + private void redrawProgress() + { + for (int i = 0; i < columns.Length; i++) + { + columns[i].State = i <= progress ? ColumnState.Lit : ColumnState.Dimmed; + } + + ForceRedraw(); + } + + /// + /// Redraws the filled amount of all the columns. + /// + private void redrawFilled() + { + for (int i = 0; i < ColumnCount; i++) + { + columns[i].Filled = calculatedValues.ElementAtOrDefault(i); + } + } + + /// + /// Takes and adjusts it to fit the amount of columns. + /// + private void recalculateValues() + { + var newValues = new List(); + + if (values == null) + { + for (float i = 0; i < ColumnCount; i++) + newValues.Add(0); + + return; + } + + float step = values.Length / (float)ColumnCount; + for (float i = 0; i < values.Length; i += step) + { + newValues.Add(values[(int)i]); + } + + calculatedValues = newValues.ToArray(); + } + + /// + /// Recreates the entire graph. + /// + private void recreateGraph() + { + var newColumns = new List(); + + for (float x = 0; x < DrawWidth; x += Column.WIDTH) + { + newColumns.Add(new Column(fillColour) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Position = new Vector2(x, 0), + State = ColumnState.Dimmed, + }); + } + + columns = newColumns.ToArray(); + Children = columns; + + recalculateValues(); + redrawFilled(); + redrawProgress(); + } + + public class Column : Container, IStateful + { + private readonly Color4 emptyColour = Color4.White.Opacity(100); + private readonly Color4 litColour; + private readonly Color4 dimmedColour = Color4.White.Opacity(175); + + private const float cube_count = 6; + private const float cube_size = 4; + private const float padding = 2; + public const float WIDTH = cube_size + padding; + public const float HEIGHT = cube_count * WIDTH + padding; + + private readonly List drawableRows = new List(); + + private int filled; + public int Filled + { + get { return filled; } + set + { + if (value == filled) return; + filled = value; + + fillActive(); + } + } + + private ColumnState state; + public ColumnState State + { + get { return state; } + set + { + if (value == state) return; + state = value; + + fillActive(); + } + } + + public Column(Color4 litColour) + { + Size = new Vector2(WIDTH, HEIGHT); + this.litColour = litColour; + + for (int r = 0; r < cube_count; r++) + { + drawableRows.Add(new Box + { + EdgeSmoothness = new Vector2(padding / 4), + Size = new Vector2(cube_size), + Position = new Vector2(0, r * WIDTH + padding), + }); + } + + Children = drawableRows; + + // Reverse drawableRows so when iterating through them they start at the bottom + drawableRows.Reverse(); + } + + private void fillActive() + { + Color4 colour = State == ColumnState.Lit ? litColour : dimmedColour; + + for (int i = 0; i < drawableRows.Count; i++) + { + if (Filled == 0) // i <= Filled doesn't work for zero fill + drawableRows[i].Colour = emptyColour; + else + drawableRows[i].Colour = i <= Filled ? colour : emptyColour; + } + } + } + + public enum ColumnState + { + Lit, + Dimmed + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b9b80ac5b9..f810eeec96 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -337,6 +337,9 @@ + + +