1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 03:42:58 +08:00

Merge pull request #504 from peppy/song-select-loading-reoptimisation

Song select performance / code cleanup.
This commit is contained in:
Dan Balasescu 2017-03-18 02:19:02 +09:00 committed by GitHub
commit 124efe8126
13 changed files with 452 additions and 352 deletions

View File

@ -10,7 +10,7 @@ using osu.Game.Database;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
internal class BeatmapGroup : IStateful<BeatmapGroupState> public class BeatmapGroup : IStateful<BeatmapGroupState>
{ {
public BeatmapPanel SelectedPanel; public BeatmapPanel SelectedPanel;
@ -63,8 +63,6 @@ namespace osu.Game.Beatmaps.Drawables
{ {
BeatmapSet = beatmapSet; BeatmapSet = beatmapSet;
WorkingBeatmap beatmap = database.GetWorkingBeatmap(BeatmapSet.Beatmaps.FirstOrDefault()); WorkingBeatmap beatmap = database.GetWorkingBeatmap(BeatmapSet.Beatmaps.FirstOrDefault());
foreach (var b in BeatmapSet.Beatmaps)
b.StarDifficulty = (float)(database.GetWorkingBeatmap(b).Beatmap?.CalculateStarDifficulty() ?? -1f);
Header = new BeatmapSetHeader(beatmap) Header = new BeatmapSetHeader(beatmap)
{ {

View File

@ -18,7 +18,7 @@ using osu.Game.Graphics.Sprites;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
internal class BeatmapPanel : Panel public class BeatmapPanel : Panel
{ {
public BeatmapInfo Beatmap; public BeatmapInfo Beatmap;
private Sprite background; private Sprite background;
@ -59,6 +59,8 @@ namespace osu.Game.Beatmaps.Drawables
protected override void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden) protected override void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden)
{ {
if (!IsLoaded) return;
base.ApplyState(last); base.ApplyState(last);
if (last == PanelSelectedState.Hidden && State != last) if (last == PanelSelectedState.Hidden && State != last)
@ -138,7 +140,7 @@ namespace osu.Game.Beatmaps.Drawables
}, },
starCounter = new StarCounter starCounter = new StarCounter
{ {
Count = beatmap.StarDifficulty, Count = (float)beatmap.StarDifficulty,
Scale = new Vector2(0.8f), Scale = new Vector2(0.8f),
} }
} }

View File

@ -17,7 +17,7 @@ using OpenTK.Graphics;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
internal class BeatmapSetHeader : Panel public class BeatmapSetHeader : Panel
{ {
public Action<BeatmapSetHeader> GainedSelection; public Action<BeatmapSetHeader> GainedSelection;
private SpriteText title, artist; private SpriteText title, artist;
@ -32,10 +32,6 @@ namespace osu.Game.Beatmaps.Drawables
Children = new Drawable[] Children = new Drawable[]
{ {
new PanelBackground(beatmap)
{
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer new FillFlowContainer
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
@ -74,13 +70,23 @@ namespace osu.Game.Beatmaps.Drawables
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config, OsuGameBase game)
{ {
this.config = config; this.config = config;
preferUnicode = config.GetBindable<bool>(OsuConfig.ShowUnicode); preferUnicode = config.GetBindable<bool>(OsuConfig.ShowUnicode);
preferUnicode.ValueChanged += preferUnicode_changed; preferUnicode.ValueChanged += preferUnicode_changed;
preferUnicode_changed(preferUnicode, null); preferUnicode_changed(preferUnicode, null);
new PanelBackground(beatmap)
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
}.LoadAsync(game, b =>
{
Add(b);
b.FadeInFromZero(200);
});
} }
private void preferUnicode_changed(object sender, EventArgs e) private void preferUnicode_changed(object sender, EventArgs e)
@ -98,16 +104,18 @@ namespace osu.Game.Beatmaps.Drawables
private class PanelBackground : BufferedContainer private class PanelBackground : BufferedContainer
{ {
private readonly WorkingBeatmap working;
public PanelBackground(WorkingBeatmap working) public PanelBackground(WorkingBeatmap working)
{ {
this.working = working;
CacheDrawnFrameBuffer = true; CacheDrawnFrameBuffer = true;
Children = new[] Children = new Drawable[]
{ {
new BeatmapBackgroundSprite(working)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
new FillFlowContainer new FillFlowContainer
{ {
Depth = -1, Depth = -1,
@ -151,21 +159,6 @@ namespace osu.Game.Beatmaps.Drawables
}, },
}; };
} }
[BackgroundDependencyLoader]
private void load(OsuGameBase game)
{
new BeatmapBackgroundSprite(working)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
}.LoadAsync(game, bg =>
{
Add(bg);
ForceRedraw();
});
}
} }
public void AddDifficultyIcons(IEnumerable<BeatmapPanel> panels) public void AddDifficultyIcons(IEnumerable<BeatmapPanel> panels)

View File

@ -12,7 +12,7 @@ using osu.Framework.Extensions.Color4Extensions;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
internal class Panel : Container, IStateful<PanelSelectedState> public class Panel : Container, IStateful<PanelSelectedState>
{ {
public const float MAX_HEIGHT = 80; public const float MAX_HEIGHT = 80;
@ -51,6 +51,8 @@ namespace osu.Game.Beatmaps.Drawables
protected virtual void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden) protected virtual void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden)
{ {
if (!IsLoaded) return;
switch (state) switch (state)
{ {
case PanelSelectedState.Hidden: case PanelSelectedState.Hidden:
@ -115,7 +117,7 @@ namespace osu.Game.Beatmaps.Drawables
} }
} }
internal enum PanelSelectedState public enum PanelSelectedState
{ {
Hidden, Hidden,
NotSelected, NotSelected,

View File

@ -123,16 +123,15 @@ namespace osu.Game.Database
/// <param name="paths">Multiple locations on disk</param> /// <param name="paths">Multiple locations on disk</param>
public void Import(IEnumerable<string> paths) public void Import(IEnumerable<string> paths)
{ {
Stack<BeatmapSetInfo> sets = new Stack<BeatmapSetInfo>();
foreach (string p in paths) foreach (string p in paths)
{
try try
{ {
BeatmapSetInfo set = getBeatmapSet(p); BeatmapSetInfo set = getBeatmapSet(p);
//If we have an ID then we already exist in the database. //If we have an ID then we already exist in the database.
if (set.ID == 0) if (set.ID == 0)
sets.Push(set); Import(new[] { set });
// We may or may not want to delete the file depending on where it is stored. // We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with beatmaps from default storage. // e.g. reconstructing/repairing database with beatmaps from default storage.
@ -152,9 +151,7 @@ namespace osu.Game.Database
e = e.InnerException ?? e; e = e.InnerException ?? e;
Logger.Error(e, @"Could not import beatmap set"); Logger.Error(e, @"Could not import beatmap set");
} }
}
// Batch commit with multiple sets to database
Import(sets);
} }
/// <summary> /// <summary>
@ -236,6 +233,8 @@ namespace osu.Game.Database
// TODO: Diff beatmap metadata with set metadata and leave it here if necessary // TODO: Diff beatmap metadata with set metadata and leave it here if necessary
beatmap.BeatmapInfo.Metadata = null; beatmap.BeatmapInfo.Metadata = null;
beatmap.BeatmapInfo.StarDifficulty = beatmap.CalculateStarDifficulty();
beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo);
} }
beatmapSet.StoryboardFile = archive.StoryboardFilename; beatmapSet.StoryboardFile = archive.StoryboardFilename;

View File

@ -75,16 +75,7 @@ namespace osu.Game.Database
// Metadata // Metadata
public string Version { get; set; } public string Version { get; set; }
private float starDifficulty = -1; public double StarDifficulty { get; set; }
public float StarDifficulty
{
get
{
return starDifficulty < 0 ? (Difficulty?.OverallDifficulty ?? 5) : starDifficulty;
}
set { starDifficulty = value; }
}
public bool Equals(BeatmapInfo other) public bool Equals(BeatmapInfo other)
{ {

View File

@ -24,7 +24,7 @@ namespace osu.Game.Database
[OneToMany(CascadeOperations = CascadeOperation.All)] [OneToMany(CascadeOperations = CascadeOperation.All)]
public List<BeatmapInfo> Beatmaps { get; set; } public List<BeatmapInfo> Beatmaps { get; set; }
public float MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty); public double MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty);
public bool DeletePending { get; set; } public bool DeletePending { get; set; }

View File

@ -27,6 +27,7 @@ namespace osu.Game.Screens.Menu
internal override bool ShowOverlays => buttons.State != MenuState.Initial; internal override bool ShowOverlays => buttons.State != MenuState.Initial;
private BackgroundScreen background; private BackgroundScreen background;
private Screen songSelect;
protected override BackgroundScreen CreateBackground() => background; protected override BackgroundScreen CreateBackground() => background;
@ -46,7 +47,7 @@ namespace osu.Game.Screens.Menu
OnChart = delegate { Push(new ChartListing()); }, OnChart = delegate { Push(new ChartListing()); },
OnDirect = delegate { Push(new OnlineListing()); }, OnDirect = delegate { Push(new OnlineListing()); },
OnEdit = delegate { Push(new Editor()); }, OnEdit = delegate { Push(new Editor()); },
OnSolo = delegate { Push(new PlaySongSelect()); }, OnSolo = delegate { Push(consumeSongSelect()); },
OnMulti = delegate { Push(new Lobby()); }, OnMulti = delegate { Push(new Lobby()); },
OnTest = delegate { Push(new TestBrowser()); }, OnTest = delegate { Push(new TestBrowser()); },
OnExit = delegate { Exit(); }, OnExit = delegate { Exit(); },
@ -62,6 +63,24 @@ namespace osu.Game.Screens.Menu
background.LoadAsync(game); background.LoadAsync(game);
buttons.OnSettings = game.ToggleOptions; buttons.OnSettings = game.ToggleOptions;
preloadSongSelect();
}
private void preloadSongSelect()
{
if (songSelect == null)
{
songSelect = new PlaySongSelect();
songSelect.LoadAsync(Game);
}
}
private Screen consumeSongSelect()
{
var s = songSelect;
songSelect = null;
return s;
} }
protected override void OnEntering(Screen last) protected override void OnEntering(Screen last)
@ -86,6 +105,9 @@ namespace osu.Game.Screens.Menu
{ {
base.OnResuming(last); base.OnResuming(last);
//we may have consumed our preloaded instance, so let's make another.
preloadSongSelect();
const float length = 300; const float length = 300;
buttons.State = MenuState.TopLevel; buttons.State = MenuState.TopLevel;

View File

@ -15,47 +15,234 @@ using OpenTK.Input;
using System.Collections; using System.Collections;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using System.Diagnostics; using System.Diagnostics;
using osu.Game.Screens.Select.Filter; using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Threading;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
internal class CarouselContainer : ScrollContainer, IEnumerable<BeatmapGroup> internal class BeatmapCarousel : ScrollContainer, IEnumerable<BeatmapGroup>
{ {
private Container<Panel> scrollableContent; public BeatmapInfo SelectedBeatmap => selectedPanel?.Beatmap;
private List<BeatmapGroup> groups = new List<BeatmapGroup>();
private List<Panel> panels = new List<Panel>();
public BeatmapGroup SelectedGroup { get; private set; } public Action BeatmapsChanged;
public BeatmapPanel SelectedPanel { get; private set; }
public IEnumerable<BeatmapSetInfo> Beatmaps
{
get
{
return groups.Select(g => g.BeatmapSet);
}
set
{
scrollableContent.Clear(false);
panels.Clear();
groups.Clear();
List<BeatmapGroup> newGroups = null;
Task.Run(() =>
{
newGroups = value.Select(createGroup).ToList();
criteria.Filter(newGroups);
}).ContinueWith(t =>
{
Schedule(() =>
{
foreach (var g in newGroups)
addGroup(g);
computeYPositions();
BeatmapsChanged?.Invoke();
});
});
}
}
private List<float> yPositions = new List<float>(); private List<float> yPositions = new List<float>();
public CarouselContainer() /// <summary>
{ /// Required for now unfortunately.
DistanceDecayJump = 0.01; /// </summary>
private BeatmapDatabase database;
private Container<Panel> scrollableContent;
private List<BeatmapGroup> groups = new List<BeatmapGroup>();
private List<Panel> panels = new List<Panel>();
private BeatmapGroup selectedGroup;
private BeatmapPanel selectedPanel;
public BeatmapCarousel()
{
Add(scrollableContent = new Container<Panel> Add(scrollableContent = new Container<Panel>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}); });
} }
public void AddGroup(BeatmapGroup group) public void AddBeatmap(BeatmapSetInfo beatmapSet)
{ {
groups.Add(group); var group = createGroup(beatmapSet);
panels.Add(group.Header); //for the time being, let's completely load the difficulty panels in the background.
group.Header.UpdateClock(Clock); //this likely won't scale so well, but allows us to completely async the loading flow.
foreach (BeatmapPanel panel in group.BeatmapPanels) Schedule(delegate
{ {
panels.Add(panel); addGroup(group);
panel.UpdateClock(Clock); computeYPositions();
} if (selectedGroup == null)
selectGroup(group);
computeYPositions(); });
} }
public void RemoveGroup(BeatmapGroup group) public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true)
{
if (beatmap == null)
{
SelectNext();
return;
}
foreach (BeatmapGroup group in groups)
{
var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
if (panel != null)
{
selectGroup(group, panel, animated);
return;
}
}
}
public void RemoveBeatmap(BeatmapSetInfo info) => removeGroup(groups.Find(b => b.BeatmapSet.ID == info.ID));
public Action<BeatmapGroup, BeatmapInfo> SelectionChanged;
public Action StartRequested;
public void SelectNext(int direction = 1, bool skipDifficulties = true)
{
if (groups.Count == 0)
{
selectedGroup = null;
selectedPanel = null;
return;
}
if (!skipDifficulties && selectedGroup != null)
{
int i = selectedGroup.BeatmapPanels.IndexOf(selectedPanel) + direction;
if (i >= 0 && i < selectedGroup.BeatmapPanels.Count)
{
//changing difficulty panel, not set.
selectGroup(selectedGroup, selectedGroup.BeatmapPanels[i]);
return;
}
}
int startIndex = groups.IndexOf(selectedGroup);
int index = startIndex;
do
{
index = (index + direction + groups.Count) % groups.Count;
if (groups[index].State != BeatmapGroupState.Hidden)
{
SelectBeatmap(groups[index].BeatmapPanels.First().Beatmap);
return;
}
} while (index != startIndex);
}
public void SelectRandom()
{
List<BeatmapGroup> visibleGroups = groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden).ToList();
if (visibleGroups.Count < 1)
return;
BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)];
BeatmapPanel panel = group?.BeatmapPanels.First();
if (panel == null)
return;
selectGroup(group, panel);
}
private FilterCriteria criteria = new FilterCriteria();
private ScheduledDelegate filterTask;
public void Filter(FilterCriteria newCriteria = null, bool debounce = true)
{
if (!IsLoaded) return;
criteria = newCriteria ?? criteria ?? new FilterCriteria();
Action perform = delegate
{
filterTask = null;
criteria.Filter(groups);
var filtered = new List<BeatmapGroup>(groups);
scrollableContent.Clear(false);
panels.Clear();
groups.Clear();
foreach (var g in filtered)
addGroup(g);
computeYPositions();
if (selectedGroup == null || selectedGroup.State == BeatmapGroupState.Hidden)
SelectNext();
};
filterTask?.Cancel();
if (debounce)
filterTask = Scheduler.AddDelayed(perform, 250);
else
perform();
}
public IEnumerator<BeatmapGroup> GetEnumerator() => groups.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private BeatmapGroup createGroup(BeatmapSetInfo beatmapSet)
{
database.GetChildren(beatmapSet);
beatmapSet.Beatmaps.ForEach(b => { if (b.Metadata == null) b.Metadata = beatmapSet.Metadata; });
return new BeatmapGroup(beatmapSet, database)
{
SelectionChanged = SelectionChanged,
StartRequested = b => StartRequested?.Invoke(),
State = BeatmapGroupState.Collapsed
};
}
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapDatabase database)
{
this.database = database;
}
private void addGroup(BeatmapGroup group)
{
groups.Add(group);
panels.Add(group.Header);
panels.AddRange(group.BeatmapPanels);
}
private void removeGroup(BeatmapGroup group)
{ {
groups.Remove(group); groups.Remove(group);
panels.Remove(group.Header); panels.Remove(group.Header);
@ -65,18 +252,12 @@ namespace osu.Game.Screens.Select
scrollableContent.Remove(group.Header); scrollableContent.Remove(group.Header);
scrollableContent.Remove(group.BeatmapPanels); scrollableContent.Remove(group.BeatmapPanels);
if (selectedGroup == group)
SelectNext();
computeYPositions(); computeYPositions();
} }
private void movePanel(Panel panel, bool advance, bool animated, ref float currentY)
{
yPositions.Add(currentY);
panel.MoveToY(currentY, animated ? 750 : 0, EasingTypes.OutExpo);
if (advance)
currentY += panel.DrawHeight + 5;
}
/// <summary> /// <summary>
/// Computes the target Y positions for every panel in the carousel. /// Computes the target Y positions for every panel in the carousel.
/// </summary> /// </summary>
@ -99,7 +280,7 @@ namespace osu.Game.Screens.Select
foreach (BeatmapPanel panel in group.BeatmapPanels) foreach (BeatmapPanel panel in group.BeatmapPanels)
{ {
if (panel == SelectedPanel) if (panel == selectedPanel)
selectedY = currentY + panel.DrawHeight / 2 - DrawHeight / 2; selectedY = currentY + panel.DrawHeight / 2 - DrawHeight / 2;
panel.MoveToX(-50, 500, EasingTypes.OutExpo); panel.MoveToX(-50, 500, EasingTypes.OutExpo);
@ -129,105 +310,62 @@ namespace osu.Game.Screens.Select
return selectedY; return selectedY;
} }
public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true) private void movePanel(Panel panel, bool advance, bool animated, ref float currentY)
{ {
foreach (BeatmapGroup group in groups) yPositions.Add(currentY);
{ panel.MoveToY(currentY, animated ? 750 : 0, EasingTypes.OutExpo);
var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
if (panel != null) if (advance)
{ currentY += panel.DrawHeight + 5;
selectGroup(group, panel, animated);
return;
}
}
} }
private void selectGroup(BeatmapGroup group, BeatmapPanel panel, bool animated = true) private void selectGroup(BeatmapGroup group, BeatmapPanel panel = null, bool animated = true)
{ {
if (panel == null)
panel = group.BeatmapPanels.First();
Trace.Assert(group.BeatmapPanels.Contains(panel), @"Selected panel must be in provided group"); Trace.Assert(group.BeatmapPanels.Contains(panel), @"Selected panel must be in provided group");
if (SelectedGroup != null && SelectedGroup != group && SelectedGroup.State != BeatmapGroupState.Hidden) if (selectedGroup != null && selectedGroup != group && selectedGroup.State != BeatmapGroupState.Hidden)
SelectedGroup.State = BeatmapGroupState.Collapsed; selectedGroup.State = BeatmapGroupState.Collapsed;
group.State = BeatmapGroupState.Expanded; group.State = BeatmapGroupState.Expanded;
SelectedGroup = group; selectedGroup = group;
panel.State = PanelSelectedState.Selected; panel.State = PanelSelectedState.Selected;
SelectedPanel = panel; selectedPanel = panel;
float selectedY = computeYPositions(animated); float selectedY = computeYPositions(animated);
ScrollTo(selectedY, animated); ScrollTo(selectedY, animated);
} }
public void Sort(SortMode mode) protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{ {
List<BeatmapGroup> sortedGroups = new List<BeatmapGroup>(groups); int direction = 0;
switch (mode) bool skipDifficulties = false;
switch (args.Key)
{ {
case SortMode.Artist: case Key.Up:
sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase)); direction = -1;
break; break;
case SortMode.Title: case Key.Down:
sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase)); direction = 1;
break; break;
case SortMode.Author: case Key.Left:
sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author, y.BeatmapSet.Metadata.Author, StringComparison.InvariantCultureIgnoreCase)); direction = -1;
skipDifficulties = true;
break; break;
case SortMode.Difficulty: case Key.Right:
sortedGroups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty)); direction = 1;
break; skipDifficulties = true;
default:
Sort(SortMode.Artist); // Temporary
break; break;
} }
scrollableContent.Clear(false); if (direction == 0)
panels.Clear(); return base.OnKeyDown(state, args);
groups.Clear();
foreach (BeatmapGroup group in sortedGroups) SelectNext(direction, skipDifficulties);
AddGroup(group); return true;
}
/// <summary>
/// Computes the x-offset of currently visible panels. Makes the carousel appear round.
/// </summary>
/// <param name="dist">
/// Vertical distance from the center of the carousel container
/// ranging from -1 to 1.
/// </param>
/// <param name="halfHeight">Half the height of the carousel container.</param>
private static float offsetX(float dist, float halfHeight)
{
// The radius of the circle the carousel moves on.
const float circle_radius = 3;
double discriminant = Math.Max(0, circle_radius * circle_radius - dist * dist);
float x = (circle_radius - (float)Math.Sqrt(discriminant)) * halfHeight;
return 125 + x;
}
/// <summary>
/// Update a panel's x position and multiplicative alpha based on its y position and
/// the current scroll position.
/// </summary>
/// <param name="p">The panel to be updated.</param>
/// <param name="halfHeight">Half the draw height of the carousel container.</param>
private void updatePanel(Panel p, float halfHeight)
{
var height = p.IsPresent ? p.DrawHeight : 0;
float panelDrawY = p.Position.Y - Current + height / 2;
float dist = Math.Abs(1f - panelDrawY / halfHeight);
// Setting the origin position serves as an additive position on top of potential
// local transformation we may want to apply (e.g. when a panel gets selected, we
// may want to smoothly transform it leftwards.)
p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0);
// We are applying a multiplicative alpha (which is internally done by nesting an
// additional container and setting that container's alpha) such that we can
// layer transformations on top, with a similar reasoning to the previous comment.
p.SetMultiplicativeAlpha(MathHelper.Clamp(1.75f - 1.5f * dist, 0, 1));
} }
protected override void Update() protected override void Update()
@ -276,80 +414,46 @@ namespace osu.Game.Screens.Select
updatePanel(p, halfHeight); updatePanel(p, halfHeight);
} }
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) /// <summary>
/// Computes the x-offset of currently visible panels. Makes the carousel appear round.
/// </summary>
/// <param name="dist">
/// Vertical distance from the center of the carousel container
/// ranging from -1 to 1.
/// </param>
/// <param name="halfHeight">Half the height of the carousel container.</param>
private static float offsetX(float dist, float halfHeight)
{ {
int direction = 0; // The radius of the circle the carousel moves on.
bool skipDifficulties = false; const float circle_radius = 3;
double discriminant = Math.Max(0, circle_radius * circle_radius - dist * dist);
float x = (circle_radius - (float)Math.Sqrt(discriminant)) * halfHeight;
switch (args.Key) return 125 + x;
{
case Key.Up:
direction = -1;
break;
case Key.Down:
direction = 1;
break;
case Key.Left:
direction = -1;
skipDifficulties = true;
break;
case Key.Right:
direction = 1;
skipDifficulties = true;
break;
}
if (direction == 0)
return base.OnKeyDown(state, args);
SelectNext(direction, skipDifficulties);
return true;
} }
public void SelectNext(int direction = 1, bool skipDifficulties = true) /// <summary>
/// Update a panel's x position and multiplicative alpha based on its y position and
/// the current scroll position.
/// </summary>
/// <param name="p">The panel to be updated.</param>
/// <param name="halfHeight">Half the draw height of the carousel container.</param>
private void updatePanel(Panel p, float halfHeight)
{ {
if (!skipDifficulties && SelectedGroup != null) var height = p.IsPresent ? p.DrawHeight : 0;
{
int i = SelectedGroup.BeatmapPanels.IndexOf(SelectedPanel) + direction;
if (i >= 0 && i < SelectedGroup.BeatmapPanels.Count) float panelDrawY = p.Position.Y - Current + height / 2;
{ float dist = Math.Abs(1f - panelDrawY / halfHeight);
//changing difficulty panel, not set.
selectGroup(SelectedGroup, SelectedGroup.BeatmapPanels[i]);
return;
}
}
int startIndex = groups.IndexOf(SelectedGroup); // Setting the origin position serves as an additive position on top of potential
int index = startIndex; // local transformation we may want to apply (e.g. when a panel gets selected, we
// may want to smoothly transform it leftwards.)
p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0);
do // We are applying a multiplicative alpha (which is internally done by nesting an
{ // additional container and setting that container's alpha) such that we can
index = (index + direction + groups.Count) % groups.Count; // layer transformations on top, with a similar reasoning to the previous comment.
if (groups[index].State != BeatmapGroupState.Hidden) p.SetMultiplicativeAlpha(MathHelper.Clamp(1.75f - 1.5f * dist, 0, 1));
{
SelectBeatmap(groups[index].BeatmapPanels.First().Beatmap);
return;
}
} while (index != startIndex);
} }
public void SelectRandom()
{
List<BeatmapGroup> visibleGroups = groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden).ToList();
if (visibleGroups.Count < 1)
return;
BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)];
BeatmapPanel panel = group?.BeatmapPanels.First();
if (panel == null)
return;
selectGroup(group, panel);
}
public IEnumerator<BeatmapGroup> GetEnumerator() => groups.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using OpenTK; using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
@ -15,14 +16,13 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
using Container = osu.Framework.Graphics.Containers.Container; using Container = osu.Framework.Graphics.Containers.Container;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Modes;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
public class FilterControl : Container public class FilterControl : Container
{ {
public Action FilterChanged; public Action<FilterCriteria> FilterChanged;
public string Search => searchTextBox.Text;
private OsuTabControl<SortMode> sortTabs; private OsuTabControl<SortMode> sortTabs;
@ -30,16 +30,16 @@ namespace osu.Game.Screens.Select
private SortMode sort = SortMode.Title; private SortMode sort = SortMode.Title;
public SortMode Sort public SortMode Sort
{ {
get { return sort; } get { return sort; }
set set
{ {
if (sort != value) if (sort != value)
{ {
sort = value; sort = value;
FilterChanged?.Invoke(); FilterChanged?.Invoke(CreateCriteria());
} }
} }
} }
private GroupMode group = GroupMode.All; private GroupMode group = GroupMode.All;
@ -51,11 +51,19 @@ namespace osu.Game.Screens.Select
if (group != value) if (group != value)
{ {
group = value; group = value;
FilterChanged?.Invoke(); FilterChanged?.Invoke(CreateCriteria());
} }
} }
} }
public FilterCriteria CreateCriteria() => new FilterCriteria
{
Group = group,
Sort = sort,
SearchText = searchTextBox.Text,
Mode = playMode
};
public Action Exit; public Action Exit;
private SearchTextBox searchTextBox; private SearchTextBox searchTextBox;
@ -88,7 +96,7 @@ namespace osu.Game.Screens.Select
OnChange = (sender, newText) => OnChange = (sender, newText) =>
{ {
if (newText) if (newText)
FilterChanged?.Invoke(); FilterChanged?.Invoke(CreateCriteria());
}, },
Exit = () => Exit?.Invoke(), Exit = () => Exit?.Invoke(),
}, },
@ -152,16 +160,21 @@ namespace osu.Game.Screens.Select
searchTextBox.HoldFocus = false; searchTextBox.HoldFocus = false;
searchTextBox.TriggerFocusLost(); searchTextBox.TriggerFocusLost();
} }
public void Activate() public void Activate()
{ {
searchTextBox.HoldFocus = true; searchTextBox.HoldFocus = true;
} }
[BackgroundDependencyLoader] private readonly Bindable<PlayMode> playMode = new Bindable<PlayMode>();
private void load(OsuColour colours)
[BackgroundDependencyLoader(permitNulls:true)]
private void load(OsuColour colours, OsuGame osu)
{ {
sortTabs.AccentColour = colours.GreenLight; sortTabs.AccentColour = colours.GreenLight;
if (osu != null)
playMode.BindTo(osu.PlayMode);
} }
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true;

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 System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Modes;
using osu.Game.Screens.Select.Filter;
namespace osu.Game.Screens.Select
{
public class FilterCriteria
{
public GroupMode Group;
public SortMode Sort;
public string SearchText;
public PlayMode Mode;
public void Filter(List<BeatmapGroup> groups)
{
foreach (var g in groups)
{
var set = g.BeatmapSet;
bool hasCurrentMode = set.Beatmaps.Any(bm => bm.Mode == Mode);
bool match = hasCurrentMode;
match &= string.IsNullOrEmpty(SearchText)
|| (set.Metadata.Artist ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.ArtistUnicode ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.Title ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.TitleUnicode ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1;
switch (g.State)
{
case BeatmapGroupState.Hidden:
if (match) g.State = BeatmapGroupState.Collapsed;
break;
default:
if (!match) g.State = BeatmapGroupState.Hidden;
break;
}
}
switch (Sort)
{
default:
case SortMode.Artist:
groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase));
break;
case SortMode.Title:
groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase));
break;
case SortMode.Author:
groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author, y.BeatmapSet.Metadata.Author, StringComparison.InvariantCultureIgnoreCase));
break;
case SortMode.Difficulty:
groups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty));
break;
}
}
}
}

View File

@ -2,10 +2,8 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using OpenTK; using OpenTK;
using OpenTK.Input; using OpenTK.Input;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -19,7 +17,6 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Database; using osu.Game.Database;
@ -38,7 +35,7 @@ namespace osu.Game.Screens.Select
private BeatmapDatabase database; private BeatmapDatabase database;
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap);
private CarouselContainer carousel; private BeatmapCarousel carousel;
private TrackManager trackManager; private TrackManager trackManager;
private DialogOverlay dialogOverlay; private DialogOverlay dialogOverlay;
@ -51,8 +48,6 @@ namespace osu.Game.Screens.Select
private SampleChannel sampleChangeDifficulty; private SampleChannel sampleChangeDifficulty;
private SampleChannel sampleChangeBeatmap; private SampleChannel sampleChangeBeatmap;
private List<BeatmapGroup> beatmapGroups;
protected virtual bool ShowFooter => true; protected virtual bool ShowFooter => true;
/// <summary> /// <summary>
@ -72,7 +67,6 @@ namespace osu.Game.Screens.Select
const float carousel_width = 640; const float carousel_width = 640;
const float filter_height = 100; const float filter_height = 100;
beatmapGroups = new List<BeatmapGroup>();
Add(new ParallaxContainer Add(new ParallaxContainer
{ {
Padding = new MarginPadding { Top = filter_height }, Padding = new MarginPadding { Top = filter_height },
@ -87,18 +81,20 @@ namespace osu.Game.Screens.Select
} }
} }
}); });
Add(carousel = new CarouselContainer Add(carousel = new BeatmapCarousel
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Size = new Vector2(carousel_width, 1), Size = new Vector2(carousel_width, 1),
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
SelectionChanged = selectionChanged,
StartRequested = raiseSelect
}); });
Add(FilterControl = new FilterControl Add(FilterControl = new FilterControl
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = filter_height, Height = filter_height,
FilterChanged = () => filterChanged(), FilterChanged = criteria => filterChanged(criteria),
Exit = Exit, Exit = Exit,
}); });
Add(beatmapInfoWedge = new BeatmapInfoWedge Add(beatmapInfoWedge = new BeatmapInfoWedge
@ -132,8 +128,7 @@ namespace osu.Game.Screens.Select
} }
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapDatabase beatmaps, AudioManager audio, DialogOverlay dialog, Framework.Game game, private void load(BeatmapDatabase beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours)
OsuGame osu, OsuColour colours)
{ {
if (Footer != null) if (Footer != null)
{ {
@ -161,7 +156,16 @@ namespace osu.Game.Screens.Select
initialAddSetsTask = new CancellationTokenSource(); initialAddSetsTask = new CancellationTokenSource();
Task.Factory.StartNew(() => addBeatmapSets(game, initialAddSetsTask.Token), initialAddSetsTask.Token); carousel.BeatmapsChanged = beatmapsLoaded;
carousel.Beatmaps = database.Query<BeatmapSetInfo>().Where(b => !b.DeletePending);
}
private void beatmapsLoaded()
{
if (Beatmap != null)
carousel.SelectBeatmap(Beatmap.BeatmapInfo, false);
else
carousel.SelectNext();
} }
private void raiseSelect() private void raiseSelect()
@ -173,61 +177,15 @@ namespace osu.Game.Screens.Select
} }
public void SelectRandom() => carousel.SelectRandom(); public void SelectRandom() => carousel.SelectRandom();
protected abstract void OnSelected(); protected abstract void OnSelected();
private ScheduledDelegate filterTask; private void filterChanged(FilterCriteria criteria, bool debounce = true)
private void filterChanged(bool debounce = true, bool eagerSelection = true)
{ {
filterTask?.Cancel(); carousel.Filter(criteria, debounce);
filterTask = Scheduler.AddDelayed(() =>
{
filterTask = null;
var search = FilterControl.Search;
BeatmapGroup newSelection = null;
carousel.Sort(FilterControl.Sort);
foreach (var beatmapGroup in carousel)
{
var set = beatmapGroup.BeatmapSet;
bool hasCurrentMode = set.Beatmaps.Any(bm => bm.Mode == playMode);
bool match = hasCurrentMode;
match &= string.IsNullOrEmpty(search)
|| (set.Metadata.Artist ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.ArtistUnicode ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.Title ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.TitleUnicode ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1;
if (match)
{
if (newSelection == null || beatmapGroup.BeatmapSet.OnlineBeatmapSetID == Beatmap.BeatmapSetInfo.OnlineBeatmapSetID)
{
if (newSelection != null)
newSelection.State = BeatmapGroupState.Collapsed;
newSelection = beatmapGroup;
}
else
beatmapGroup.State = BeatmapGroupState.Collapsed;
}
else
{
beatmapGroup.State = BeatmapGroupState.Hidden;
}
}
if (newSelection != null)
{
if (newSelection.BeatmapPanels.Any(b => b.Beatmap.ID == Beatmap.BeatmapInfo.ID))
carousel.SelectBeatmap(Beatmap.BeatmapInfo, false);
else if (eagerSelection)
carousel.SelectBeatmap(newSelection.BeatmapSet.Beatmaps[0], false);
}
}, debounce ? 250 : 0);
} }
private void onBeatmapSetAdded(BeatmapSetInfo s) => Schedule(() => addBeatmapSet(s, Game, true)); private void onBeatmapSetAdded(BeatmapSetInfo s) => carousel.AddBeatmap(s);
private void onBeatmapSetRemoved(BeatmapSetInfo s) => Schedule(() => removeBeatmapSet(s)); private void onBeatmapSetRemoved(BeatmapSetInfo s) => Schedule(() => removeBeatmapSet(s));
@ -288,10 +246,7 @@ namespace osu.Game.Screens.Select
initialAddSetsTask.Cancel(); initialAddSetsTask.Cancel();
} }
private void playMode_ValueChanged(object sender, EventArgs e) private void playMode_ValueChanged(object sender, EventArgs e) => carousel.Filter();
{
filterChanged(false);
}
private void changeBackground(WorkingBeatmap beatmap) private void changeBackground(WorkingBeatmap beatmap)
{ {
@ -352,63 +307,18 @@ namespace osu.Game.Screens.Select
} }
} }
private void addBeatmapSet(BeatmapSetInfo beatmapSet, Framework.Game game, bool select = false) private void selectBeatmap(BeatmapSetInfo beatmapSet = null)
{ {
beatmapSet = database.GetWithChildren<BeatmapSetInfo>(beatmapSet.ID); carousel.SelectBeatmap(beatmapSet != null ? beatmapSet.Beatmaps.First() : Beatmap?.BeatmapInfo);
beatmapSet.Beatmaps.ForEach(b =>
{
database.GetChildren(b);
if (b.Metadata == null) b.Metadata = beatmapSet.Metadata;
});
var group = new BeatmapGroup(beatmapSet, database)
{
SelectionChanged = selectionChanged,
StartRequested = b => raiseSelect()
};
//for the time being, let's completely load the difficulty panels in the background.
//this likely won't scale so well, but allows us to completely async the loading flow.
Task.WhenAll(group.BeatmapPanels.Select(panel => panel.LoadAsync(game))).ContinueWith(task => Schedule(delegate
{
beatmapGroups.Add(group);
group.State = BeatmapGroupState.Collapsed;
carousel.AddGroup(group);
filterChanged(false, false);
if (Beatmap == null || select)
carousel.SelectBeatmap(beatmapSet.Beatmaps.First());
else
carousel.SelectBeatmap(Beatmap.BeatmapInfo);
}));
} }
private void removeBeatmapSet(BeatmapSetInfo beatmapSet) private void removeBeatmapSet(BeatmapSetInfo beatmapSet)
{ {
var group = beatmapGroups.Find(b => b.BeatmapSet.ID == beatmapSet.ID); carousel.RemoveBeatmap(beatmapSet);
if (group == null) return; if (carousel.SelectedBeatmap == null)
if (carousel.SelectedGroup == group)
carousel.SelectNext();
beatmapGroups.Remove(group);
carousel.RemoveGroup(group);
if (beatmapGroups.Count == 0)
Beatmap = null; Beatmap = null;
} }
private void addBeatmapSets(Framework.Game game, CancellationToken token)
{
foreach (var beatmapSet in database.Query<BeatmapSetInfo>().Where(b => !b.DeletePending))
{
if (token.IsCancellationRequested) return;
addBeatmapSet(beatmapSet, game);
}
}
private void promptDelete() private void promptDelete()
{ {
if (Beatmap != null) if (Beatmap != null)

View File

@ -197,7 +197,8 @@
<Compile Include="Screens\Play\PlayerLoader.cs" /> <Compile Include="Screens\Play\PlayerLoader.cs" />
<Compile Include="Screens\Play\SkipButton.cs" /> <Compile Include="Screens\Play\SkipButton.cs" />
<Compile Include="Modes\UI\StandardComboCounter.cs" /> <Compile Include="Modes\UI\StandardComboCounter.cs" />
<Compile Include="Screens\Select\CarouselContainer.cs" /> <Compile Include="Screens\Select\BeatmapCarousel.cs" />
<Compile Include="Screens\Select\FilterCriteria.cs" />
<Compile Include="Screens\Select\Filter\GroupMode.cs" /> <Compile Include="Screens\Select\Filter\GroupMode.cs" />
<Compile Include="Screens\Select\Filter\SortMode.cs" /> <Compile Include="Screens\Select\Filter\SortMode.cs" />
<Compile Include="Screens\Select\MatchSongSelect.cs" /> <Compile Include="Screens\Select\MatchSongSelect.cs" />