Skip to content

Commit

Permalink
Merge pull request #25154 from peppy/taiko-beat-snap-grid
Browse files Browse the repository at this point in the history
Add beat snap grid to osu!taiko editor
  • Loading branch information
bdach authored Oct 19, 2023
2 parents e8d0962 + 2c6bf9e commit cd3ff2b
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 495 deletions.
173 changes: 6 additions & 167 deletions osu.Game.Rulesets.Catch/Edit/CatchBeatSnapGrid.cs
Original file line number Diff line number Diff line change
@@ -1,180 +1,19 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK.Graphics;
using osu.Game.Screens.Edit.Compose.Components;

namespace osu.Game.Rulesets.Catch.Edit
{
/// <summary>
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
/// </summary>
/// <remarks>
/// This class heavily borrows from osu!mania's implementation (ManiaBeatSnapGrid).
/// If further changes are to be made, they should also be applied there.
/// If the scale of the changes are large enough, abstracting may be a good path.
/// </remarks>
public partial class CatchBeatSnapGrid : Component
public partial class CatchBeatSnapGrid : BeatSnapGrid
{
private const double visible_range = 750;

/// <summary>
/// The range of time values of the current selection.
/// </summary>
public (double start, double end)? SelectionTimeRange
{
set
{
if (value == selectionTimeRange)
return;

selectionTimeRange = value;
lineCache.Invalidate();
}
}

[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;

[Resolved]
private OsuColour colours { get; set; } = null!;

[Resolved]
private BindableBeatDivisor beatDivisor { get; set; } = null!;

private readonly Cached lineCache = new Cached();

private (double start, double end)? selectionTimeRange;

private ScrollingHitObjectContainer lineContainer = null!;

[BackgroundDependencyLoader]
private void load(HitObjectComposer composer)
protected override IEnumerable<Container> GetTargetContainers(HitObjectComposer composer) => new[]
{
lineContainer = new ScrollingHitObjectContainer();

((CatchPlayfield)composer.Playfield).UnderlayElements.Add(lineContainer);

beatDivisor.BindValueChanged(_ => createLines(), true);
}

protected override void Update()
{
base.Update();

if (!lineCache.IsValid)
{
lineCache.Validate();
createLines();
}
}

private readonly Stack<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();

private void createLines()
{
foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
availableLines.Push(line);

lineContainer.Clear();

if (selectionTimeRange == null)
return;

var range = selectionTimeRange.Value;

var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);

double time = timingPoint.Time;
int beat = 0;

// progress time until in the visible range.
while (time < range.start - visible_range)
{
time += timingPoint.BeatLength / beatDivisor.Value;
beat++;
}

while (time < range.end + visible_range)
{
var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);

// switch to the next timing point if we have reached it.
if (nextTimingPoint.Time > timingPoint.Time)
{
beat = 0;
time = nextTimingPoint.Time;
timingPoint = nextTimingPoint;
}

Color4 colour = BindableBeatDivisor.GetColourFor(
BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);

if (!availableLines.TryPop(out var line))
line = new DrawableGridLine();

line.HitObject.StartTime = time;
line.Colour = colour;

lineContainer.Add(line);

beat++;
time += timingPoint.BeatLength / beatDivisor.Value;
}

// required to update ScrollingHitObjectContainer's cache.
lineContainer.UpdateSubTree();

foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
{
time = line.HitObject.StartTime;

if (time >= range.start && time <= range.end)
line.Alpha = 1;
else
{
double timeSeparation = time < range.start ? range.start - time : time - range.end;
line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
}
}
}

private partial class DrawableGridLine : DrawableHitObject
{
public DrawableGridLine()
: base(new HitObject())
{
RelativeSizeAxes = Axes.X;
Height = 2;

AddInternal(new Box { RelativeSizeAxes = Axes.Both });
}

[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.BottomLeft;
Anchor = Anchor.BottomLeft;
}

protected override void UpdateInitialTransforms()
{
// don't perform any fading – we are handling that ourselves.
LifetimeEnd = HitObject.StartTime + visible_range;
}
}
((CatchPlayfield)composer.Playfield).UnderlayElements
};
}
}
90 changes: 13 additions & 77 deletions osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
Expand All @@ -32,10 +30,6 @@ public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHi

private CatchDistanceSnapGrid distanceSnapGrid = null!;

private InputManager inputManager = null!;

private CatchBeatSnapGrid beatSnapGrid = null!;

private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
{
MinValue = 1,
Expand Down Expand Up @@ -74,50 +68,28 @@ private void load()
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
}));

AddInternal(beatSnapGrid = new CatchBeatSnapGrid());
}

protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons());

protected override void LoadComplete()
{
base.LoadComplete();

inputManager = GetContainingInputManager();
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
{
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
};

protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);

if (BlueprintContainer.CurrentTool is SelectTool)
{
if (EditorBeatmap.SelectedHitObjects.Any())
{
beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
}
else
beatSnapGrid.SelectionTimeRange = null;
}
else
{
var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
if (result.Time is double time)
beatSnapGrid.SelectionTimeRange = (time, time);
else
beatSnapGrid.SelectionTimeRange = null;
}
}
protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid();

protected override void Update()
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{
base.Update();

updateDistanceSnapGrid();
}
new FruitCompositionTool(),
new JuiceStreamCompositionTool(),
new BananaShowerCompositionTool()
};

public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
Expand All @@ -142,19 +114,6 @@ public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}

protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
{
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
};

protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{
new FruitCompositionTool(),
new JuiceStreamCompositionTool(),
new BananaShowerCompositionTool()
};

public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
Expand All @@ -173,8 +132,6 @@ public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePositio
return result;
}

protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);

private PalpableCatchHitObject? getLastSnappableHitObject(double time)
{
var hitObject = EditorBeatmap.HitObjects.OfType<CatchHitObject>().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower));
Expand Down Expand Up @@ -215,33 +172,12 @@ public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePositio
return null;
}

double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position);
double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
return getLastSnappableHitObject(timeAtCursor);

default:
return null;
}
}

private void updateDistanceSnapGrid()
{
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True)
{
distanceSnapGrid.Hide();
return;
}

var sourceHitObject = getDistanceSnapGridSourceHitObject();

if (sourceHitObject == null)
{
distanceSnapGrid.Hide();
return;
}

distanceSnapGrid.Show();
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
}
}
}
Loading

0 comments on commit cd3ff2b

Please sign in to comment.