Integrate osu! distance snapping grid (#6539)

Integrate osu! distance snapping grid

Co-authored-by: Dean Herbert <pe@ppy.sh>
This commit is contained in:
Dean Herbert 2019-10-21 19:54:49 +09:00 committed by GitHub
commit 6b50d2194a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 188 additions and 41 deletions

View File

@ -13,6 +13,7 @@ public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
public OsuDistanceSnapGrid(OsuHitObject hitObject)
: base(hitObject, hitObject.StackedEndPosition)
{
Masking = true;
}
protected override float GetVelocity(double time, ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
@ -52,5 +54,31 @@ public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObjec
return base.CreateBlueprintFor(hitObject);
}
protected override DistanceSnapGrid CreateDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
{
var objects = selectedHitObjects.ToList();
if (objects.Count == 0)
{
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime <= EditorClock.CurrentTime);
if (lastObject == null)
return null;
return new OsuDistanceSnapGrid(lastObject);
}
else
{
double minTime = objects.Min(h => h.StartTime);
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime < minTime);
if (lastObject == null)
return null;
return new OsuDistanceSnapGrid(lastObject);
}
}
}
}

View File

@ -4,12 +4,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Configuration;
@ -22,6 +24,7 @@
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Edit
{
@ -30,16 +33,20 @@ public abstract class HitObjectComposer<TObject> : HitObjectComposer, IPlacement
where TObject : HitObject
{
protected IRulesetConfigManager Config { get; private set; }
protected EditorBeatmap<TObject> EditorBeatmap { get; private set; }
protected readonly Ruleset Ruleset;
[Resolved]
protected IFrameBasedClock EditorClock { get; private set; }
private IWorkingBeatmap workingBeatmap;
private Beatmap<TObject> playableBeatmap;
private EditorBeatmap<TObject> editorBeatmap;
private IBeatmapProcessor beatmapProcessor;
private DrawableEditRulesetWrapper<TObject> drawableRulesetWrapper;
private BlueprintContainer blueprintContainer;
private Container distanceSnapGridContainer;
private DistanceSnapGrid distanceSnapGrid;
private readonly List<Container> layerContainers = new List<Container>();
private InputManager inputManager;
@ -67,11 +74,13 @@ private void load(IFrameBasedClock framedClock)
return;
}
var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer();
layerBelowRuleset.Child = new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both };
var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[]
{
distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both },
new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both }
});
var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer();
layerAboveRuleset.Child = blueprintContainer = new BlueprintContainer();
var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChild(blueprintContainer = new BlueprintContainer());
layerContainers.Add(layerBelowRuleset);
layerContainers.Add(layerAboveRuleset);
@ -114,11 +123,13 @@ private void load(IFrameBasedClock framedClock)
};
toolboxCollection.Items =
CompositionTools.Select(t => new RadioButton(t.Name, () => blueprintContainer.CurrentTool = t))
.Prepend(new RadioButton("Select", () => blueprintContainer.CurrentTool = null))
CompositionTools.Select(t => new RadioButton(t.Name, () => selectTool(t)))
.Prepend(new RadioButton("Select", () => selectTool(null)))
.ToList();
toolboxCollection.Items[0].Select();
blueprintContainer.SelectionChanged += selectionChanged;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -130,14 +141,14 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl
beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap);
editorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
editorBeatmap.HitObjectAdded += addHitObject;
editorBeatmap.HitObjectRemoved += removeHitObject;
editorBeatmap.StartTimeChanged += updateHitObject;
EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
EditorBeatmap.HitObjectAdded += addHitObject;
EditorBeatmap.HitObjectRemoved += removeHitObject;
EditorBeatmap.StartTimeChanged += updateHitObject;
var dependencies = new DependencyContainer(parent);
dependencies.CacheAs<IEditorBeatmap>(editorBeatmap);
dependencies.CacheAs<IEditorBeatmap<TObject>>(editorBeatmap);
dependencies.CacheAs<IEditorBeatmap>(EditorBeatmap);
dependencies.CacheAs<IEditorBeatmap<TObject>>(EditorBeatmap);
Config = dependencies.Get<RulesetConfigCache>().GetConfigFor(Ruleset);
@ -151,6 +162,14 @@ protected override void LoadComplete()
inputManager = GetContainingInputManager();
}
protected override void Update()
{
base.Update();
if (EditorClock.ElapsedFrameTime != 0 && blueprintContainer.CurrentTool != null)
showGridFor(Enumerable.Empty<HitObject>());
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@ -164,19 +183,53 @@ protected override void UpdateAfterChildren()
});
}
private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
private void removeHitObject(HitObject hitObject)
private void selectionChanged(IEnumerable<HitObject> selectedHitObjects)
{
beatmapProcessor?.PreProcess();
beatmapProcessor?.PostProcess();
var hitObjects = selectedHitObjects.ToArray();
if (!hitObjects.Any())
distanceSnapGridContainer.Hide();
else
showGridFor(hitObjects);
}
private void updateHitObject(HitObject hitObject)
private void selectTool(HitObjectCompositionTool tool)
{
beatmapProcessor?.PreProcess();
hitObject.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty);
beatmapProcessor?.PostProcess();
blueprintContainer.CurrentTool = tool;
if (tool == null)
distanceSnapGridContainer.Hide();
else
showGridFor(Enumerable.Empty<HitObject>());
}
private void showGridFor(IEnumerable<HitObject> selectedHitObjects)
{
distanceSnapGridContainer.Clear();
distanceSnapGrid = CreateDistanceSnapGrid(selectedHitObjects);
if (distanceSnapGrid != null)
{
distanceSnapGridContainer.Child = distanceSnapGrid;
distanceSnapGridContainer.Show();
}
}
private ScheduledDelegate scheduledUpdate;
private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
private void removeHitObject(HitObject hitObject) => updateHitObject(null);
private void updateHitObject([CanBeNull] HitObject hitObject)
{
scheduledUpdate?.Cancel();
scheduledUpdate = Schedule(() =>
{
beatmapProcessor?.PreProcess();
hitObject?.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty);
beatmapProcessor?.PostProcess();
});
}
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
@ -188,20 +241,30 @@ private void updateHitObject(HitObject hitObject)
public void BeginPlacement(HitObject hitObject)
{
if (distanceSnapGrid != null)
hitObject.StartTime = GetSnappedTime(hitObject.StartTime, distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position));
}
public void EndPlacement(HitObject hitObject) => editorBeatmap.Add(hitObject);
public void EndPlacement(HitObject hitObject)
{
EditorBeatmap.Add(hitObject);
showGridFor(Enumerable.Empty<HitObject>());
}
public void Delete(HitObject hitObject) => editorBeatmap.Remove(hitObject);
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
public override Vector2 GetSnappedPosition(Vector2 position) => distanceSnapGrid?.GetSnapPosition(position) ?? position;
public override double GetSnappedTime(double startTime, Vector2 position) => distanceSnapGrid?.GetSnapTime(position) ?? startTime;
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorBeatmap != null)
if (EditorBeatmap != null)
{
editorBeatmap.HitObjectAdded -= addHitObject;
editorBeatmap.HitObjectRemoved -= removeHitObject;
EditorBeatmap.HitObjectAdded -= addHitObject;
EditorBeatmap.HitObjectRemoved -= removeHitObject;
}
}
}
@ -234,5 +297,17 @@ internal HitObjectComposer()
/// Creates a <see cref="SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// </summary>
public virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler();
/// <summary>
/// Creates the <see cref="DistanceSnapGrid"/> applicable for a <see cref="HitObject"/> selection.
/// </summary>
/// <param name="selectedHitObjects">The <see cref="HitObject"/> selection.</param>
/// <returns>The <see cref="DistanceSnapGrid"/> for <paramref name="selectedHitObjects"/>.</returns>
[CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable<HitObject> selectedHitObjects) => null;
public abstract Vector2 GetSnappedPosition(Vector2 position);
public abstract double GetSnappedTime(double startTime, Vector2 screenSpacePosition);
}
}

View File

@ -78,6 +78,7 @@ public abstract class DrawableHitObject : SkinReloadableDrawable
/// </summary>
public JudgementResult Result { get; private set; }
private Bindable<double> startTimeBindable;
private Bindable<int> comboIndexBindable;
public override bool RemoveWhenNotAlive => false;
@ -127,7 +128,10 @@ protected override void LoadComplete()
{
base.LoadComplete();
apply(HitObject);
HitObject.DefaultsApplied += onDefaultsApplied;
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => updateState(ArmedState.Idle, true));
if (HitObject is IHasComboInformation combo)
{
@ -136,8 +140,11 @@ protected override void LoadComplete()
}
updateState(ArmedState.Idle, true);
onDefaultsApplied();
}
private void onDefaultsApplied() => apply(HitObject);
private void apply(HitObject hitObject)
{
#pragma warning disable 618 // can be removed 20200417
@ -494,6 +501,12 @@ protected virtual void CheckForResult(bool userTriggered, double timeOffset)
/// </summary>
/// <param name="judgement">The <see cref="Judgement"/> that provides the scoring information.</param>
protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(HitObject, judgement);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
HitObject.DefaultsApplied -= onDefaultsApplied;
}
}
public abstract class DrawableHitObject<TObject> : DrawableHitObject

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 JetBrains.Annotations;
@ -28,6 +29,11 @@ public class HitObject
/// </summary>
private const double control_point_leniency = 1;
/// <summary>
/// Invoked after <see cref="ApplyDefaults"/> has completed on this <see cref="HitObject"/>.
/// </summary>
public event Action DefaultsApplied;
public readonly Bindable<double> StartTimeBindable = new Bindable<double>();
/// <summary>
@ -113,6 +119,8 @@ public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty d
foreach (var h in nestedHitObjects)
h.ApplyDefaults(controlPointInfo, difficulty);
DefaultsApplied?.Invoke();
}
protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
@ -14,20 +15,20 @@
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class BlueprintContainer : CompositeDrawable
{
private SelectionBlueprintContainer selectionBlueprints;
public event Action<IEnumerable<HitObject>> SelectionChanged;
private SelectionBlueprintContainer selectionBlueprints;
private Container<PlacementBlueprint> placementBlueprintContainer;
private PlacementBlueprint currentPlacement;
private SelectionHandler selectionHandler;
private InputManager inputManager;
private IEnumerable<SelectionBlueprint> selections => selectionBlueprints.Children.Where(c => c.IsAlive);
[Resolved]
private HitObjectComposer composer { get; set; }
@ -143,7 +144,7 @@ protected override bool OnMouseMove(MouseMoveEvent e)
{
if (currentPlacement != null)
{
currentPlacement.UpdatePosition(e.ScreenSpaceMousePosition);
updatePlacementPosition(e.ScreenSpaceMousePosition);
return true;
}
@ -178,19 +179,27 @@ private void refreshTool()
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
blueprint.UpdatePosition(inputManager.CurrentState.Mouse.Position);
updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
}
}
private void updatePlacementPosition(Vector2 screenSpacePosition)
{
Vector2 snappedGridPosition = composer.GetSnappedPosition(ToLocalSpace(screenSpacePosition));
Vector2 snappedScreenSpacePosition = ToScreenSpace(snappedGridPosition);
currentPlacement.UpdatePosition(snappedScreenSpacePosition);
}
/// <summary>
/// Select all masks in a given rectangle selection area.
/// </summary>
/// <param name="rect">The rectangle to perform a selection on in screen-space coordinates.</param>
private void select(RectangleF rect)
{
foreach (var blueprint in selections.ToList())
foreach (var blueprint in selectionBlueprints)
{
if (blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint))
if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint))
blueprint.Select();
else
blueprint.Deselect();
@ -200,27 +209,40 @@ private void select(RectangleF rect)
/// <summary>
/// Deselects all selected <see cref="SelectionBlueprint"/>s.
/// </summary>
private void deselectAll() => selections.ToList().ForEach(m => m.Deselect());
private void deselectAll() => selectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
private void onBlueprintSelected(SelectionBlueprint blueprint)
{
selectionHandler.HandleSelected(blueprint);
selectionBlueprints.ChangeChildDepth(blueprint, 1);
SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects);
}
private void onBlueprintDeselected(SelectionBlueprint blueprint)
{
selectionHandler.HandleDeselected(blueprint);
selectionBlueprints.ChangeChildDepth(blueprint, 0);
SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects);
}
private void onSelectionRequested(SelectionBlueprint blueprint, InputState state) => selectionHandler.HandleSelectionRequested(blueprint, state);
private void onDragRequested(SelectionBlueprint blueprint, DragEvent dragEvent)
{
var movePosition = blueprint.ScreenSpaceMovementStartPosition + dragEvent.ScreenSpaceMousePosition - dragEvent.ScreenSpaceMouseDownPosition;
HitObject draggedObject = blueprint.DrawableObject.HitObject;
selectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, blueprint.ScreenSpaceMovementStartPosition, movePosition));
Vector2 movePosition = blueprint.ScreenSpaceMovementStartPosition + dragEvent.ScreenSpaceMousePosition - dragEvent.ScreenSpaceMouseDownPosition;
Vector2 snappedPosition = composer.GetSnappedPosition(ToLocalSpace(movePosition));
// Move the hitobjects
selectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, blueprint.ScreenSpaceMovementStartPosition, ToScreenSpace(snappedPosition)));
// Apply the start time at the newly snapped-to position
double offset = composer.GetSnappedTime(draggedObject.StartTime, snappedPosition) - draggedObject.StartTime;
foreach (HitObject obj in selectionHandler.SelectedHitObjects)
obj.StartTime += offset;
}
protected override void Dispose(bool isDisposing)

View File

@ -26,10 +26,10 @@ public class SelectionHandler : CompositeDrawable
{
public const float BORDER_RADIUS = 2;
protected IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
private readonly List<SelectionBlueprint> selectedBlueprints;
protected IEnumerable<HitObject> SelectedHitObjects => selectedBlueprints.Select(b => b.DrawableObject.HitObject);
public IEnumerable<HitObject> SelectedHitObjects => selectedBlueprints.Select(b => b.DrawableObject.HitObject);
private Drawable outline;