1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 20:22:55 +08:00

Merge pull request #8886 from smoogipoo/fix-mania-selection

Fix mania selection blueprints not moving/moving incorrectly
This commit is contained in:
Dean Herbert 2020-04-29 17:39:14 +09:00 committed by GitHub
commit 0375bbfde1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 293 additions and 131 deletions

View File

@ -0,0 +1,174 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneManiaHitObjectComposer : EditorClockTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ManiaBlueprintContainer)
};
private TestComposer composer;
[SetUp]
public void Setup() => Schedule(() =>
{
BeatDivisor.Value = 8;
Clock.Seek(0);
Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both };
});
[Test]
public void TestDragOffscreenSelectionVerticallyUpScroll()
{
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
AddStep("click last object", () =>
{
originalPosition = lastObject.DrawPosition;
InputManager.MoveMouseTo(lastObject);
InputManager.PressButton(MouseButton.Left);
});
AddStep("move mouse downwards", () =>
{
InputManager.MoveMouseTo(lastObject, new Vector2(0, 20));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0);
AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50);
}
[Test]
public void TestDragOffscreenSelectionVerticallyDownScroll()
{
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
AddStep("set down scroll", () => ((Bindable<ScrollingDirection>)composer.Composer.ScrollingInfo.Direction).Value = ScrollingDirection.Down);
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
AddStep("click last object", () =>
{
originalPosition = lastObject.DrawPosition;
InputManager.MoveMouseTo(lastObject);
InputManager.PressButton(MouseButton.Left);
});
AddStep("move mouse upwards", () =>
{
InputManager.MoveMouseTo(lastObject, new Vector2(0, -20));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0);
AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50);
}
[Test]
public void TestDragOffscreenSelectionHorizontally()
{
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
AddStep("click last object", () =>
{
originalPosition = lastObject.DrawPosition;
InputManager.MoveMouseTo(lastObject);
InputManager.PressButton(MouseButton.Left);
});
AddStep("move mouse right", () =>
{
var firstColumn = composer.Composer.Playfield.GetColumn(0);
var secondColumn = composer.Composer.Playfield.GetColumn(1);
InputManager.MoveMouseTo(lastObject, new Vector2(secondColumn.ScreenSpaceDrawQuad.Centre.X - firstColumn.ScreenSpaceDrawQuad.Centre.X + 1, 0));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 1));
// Todo: They'll move vertically by the height of a note since there's no snapping and the selection point is the middle of the note.
AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y <= DefaultNotePiece.NOTE_HEIGHT);
}
private class TestComposer : CompositeDrawable
{
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
public readonly EditorBeatmap EditorBeatmap;
public readonly ManiaHitObjectComposer Composer;
public TestComposer()
{
InternalChildren = new Drawable[]
{
EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
{
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }
},
Composer = new ManiaHitObjectComposer(new ManiaRuleset())
};
for (int i = 0; i < 10; i++)
EditorBeatmap.Add(new Note { StartTime = 100 * i });
}
}
}
}

View File

@ -76,5 +76,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
}
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
public override Vector2 SelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
}
}

View File

@ -3,8 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
@ -15,13 +13,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{
public Vector2 ScreenSpaceDragPosition { get; private set; }
public Vector2 DragPosition { get; private set; }
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
protected IClock EditorClock { get; private set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
@ -34,12 +27,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
RelativeSizeAxes = Axes.None;
}
[BackgroundDependencyLoader]
private void load(IAdjustableClock clock)
{
EditorClock = clock;
}
protected override void Update()
{
base.Update();
@ -47,22 +34,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
}
protected override bool OnMouseDown(MouseDownEvent e)
{
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
return base.OnMouseDown(e);
}
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
}
public override void Show()
{
DrawableObject.AlwaysAlive = true;

View File

@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Edit
return base.CreateBlueprintFor(hitObject);
}
protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -37,7 +38,33 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
public int TotalColumns => ((ManiaPlayfield)drawableRuleset.Playfield).TotalColumns;
public ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
public int TotalColumns => Playfield.TotalColumns;
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time)
{
var hoc = Playfield.GetColumn(0).HitObjectContainer;
float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y;
if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down)
{
// We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
// The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
// so when scrolling downwards the coordinates need to be flipped.
targetPosition = hoc.DrawHeight - targetPosition;
}
double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition,
EditorClock.CurrentTime,
drawableRuleset.ScrollingInfo.TimeRange.Value,
hoc.DrawHeight);
return base.GetSnappedPosition(position, targetTime);
}
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
{

View File

@ -4,11 +4,8 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
@ -22,85 +19,16 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private IManiaHitObjectComposer composer { get; set; }
private IClock editorClock;
[BackgroundDependencyLoader]
private void load(IAdjustableClock clock)
{
editorClock = clock;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
adjustOrigins(maniaBlueprint);
performDragMovement(moveEvent);
performColumnMovement(lastColumn, moveEvent);
return true;
}
/// <summary>
/// Ensures that the position of hitobjects remains centred to the mouse position.
/// E.g. The hitobject position will change if the editor scrolls while a hitobject is dragged.
/// </summary>
/// <param name="reference">The <see cref="ManiaSelectionBlueprint"/> that received the drag event.</param>
private void adjustOrigins(ManiaSelectionBlueprint reference)
{
var referenceParent = (HitObjectContainer)reference.DrawableObject.Parent;
float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.DrawableObject.OriginPosition.Y;
float targetPosition = referenceParent.ToLocalSpace(reference.ScreenSpaceDragPosition).Y - offsetFromReferenceOrigin;
// Flip the vertical coordinate space when scrolling downwards
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
targetPosition -= referenceParent.DrawHeight;
float movementDelta = targetPosition - reference.DrawableObject.Position.Y;
foreach (var b in SelectedBlueprints.OfType<ManiaSelectionBlueprint>())
b.DrawableObject.Y += movementDelta;
}
private void performDragMovement(MoveSelectionEvent moveEvent)
{
float delta = moveEvent.InstantDelta.Y;
// When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen.
// This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height.
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
delta -= moveEvent.Blueprint.Parent.DrawHeight; // todo: probably wrong
foreach (var selectionBlueprint in SelectedBlueprints)
{
var b = (OverlaySelectionBlueprint)selectionBlueprint;
var hitObject = b.DrawableObject;
var objectParent = (HitObjectContainer)hitObject.Parent;
// StartTime could be used to adjust the position if only one movement event was received per frame.
// However this is not the case and ScrollingHitObjectContainer performs movement in UpdateAfterChildren() so the position must also be updated to be valid for further movement events
hitObject.Y += delta;
float targetPosition = hitObject.Position.Y;
// The scrolling algorithm always assumes an anchor at the top of the screen, so the position must be flipped when scrolling downwards to reflect a top anchor
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
targetPosition = -targetPosition;
objectParent.Remove(hitObject);
hitObject.HitObject.StartTime = scrollingInfo.Algorithm.TimeAt(targetPosition,
editorClock.CurrentTime,
scrollingInfo.TimeRange.Value,
objectParent.DrawHeight);
objectParent.Add(hitObject);
}
}
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition);

View File

@ -1,18 +0,0 @@
// 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 osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Edit.Masks
{
public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{
protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject)
{
RelativeSizeAxes = Axes.None;
}
}
}

View File

@ -13,11 +13,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public abstract class DrawableManiaHitObject : DrawableHitObject<ManiaHitObject>
{
/// <summary>
/// Whether this <see cref="DrawableManiaHitObject"/> should always remain alive.
/// </summary>
internal bool AlwaysAlive;
/// <summary>
/// The <see cref="ManiaAction"/> which causes this <see cref="DrawableManiaHitObject{TObject}"/> to be hit.
/// </summary>
@ -54,7 +49,62 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Direction.BindValueChanged(OnDirectionChanged, true);
}
protected override bool ShouldBeAlive => AlwaysAlive || base.ShouldBeAlive;
private double computedLifetimeStart;
public override double LifetimeStart
{
get => base.LifetimeStart;
set
{
computedLifetimeStart = value;
if (!AlwaysAlive)
base.LifetimeStart = value;
}
}
private double computedLifetimeEnd;
public override double LifetimeEnd
{
get => base.LifetimeEnd;
set
{
computedLifetimeEnd = value;
if (!AlwaysAlive)
base.LifetimeEnd = value;
}
}
private bool alwaysAlive;
/// <summary>
/// Whether this <see cref="DrawableManiaHitObject"/> should always remain alive.
/// </summary>
internal bool AlwaysAlive
{
get => alwaysAlive;
set
{
if (alwaysAlive == value)
return;
alwaysAlive = value;
if (value)
{
// Set the base lifetimes directly, to avoid mangling the computed lifetimes
base.LifetimeStart = double.MinValue;
base.LifetimeEnd = double.MaxValue;
}
else
{
LifetimeStart = computedLifetimeStart;
LifetimeEnd = computedLifetimeEnd;
}
}
}
protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
foreach (var column in stage.Columns)
{
if (column.ReceivePositionalInputAt(screenSpacePosition))
if (column.ReceivePositionalInputAt(new Vector2(screenSpacePosition.X, column.ScreenSpaceDrawQuad.Centre.Y)))
{
found = column;
break;
@ -87,6 +87,31 @@ namespace osu.Game.Rulesets.Mania.UI
return found;
}
/// <summary>
/// Retrieves a <see cref="Column"/> by index.
/// </summary>
/// <param name="index">The index of the column.</param>
/// <returns>The <see cref="Column"/> corresponding to the given index.</returns>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="index"/> is less than 0 or greater than <see cref="TotalColumns"/>.</exception>
public Column GetColumn(int index)
{
if (index < 0 || index > TotalColumns - 1)
throw new ArgumentOutOfRangeException(nameof(index));
foreach (var stage in stages)
{
if (index >= stage.Columns.Count)
{
index -= stage.Columns.Count;
continue;
}
return stage.Columns[index];
}
throw new ArgumentOutOfRangeException(nameof(index));
}
/// <summary>
/// Retrieves the total amount of columns across all stages in this playfield.
/// </summary>

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public PassThroughInputManager KeyBindingInputManager;
public override double GameplayStartTime => Objects.First().StartTime - 2000;
public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0;
private readonly Lazy<Playfield> playfield;

View File

@ -401,12 +401,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
HitObject draggedObject = movementBlueprint.HitObject;
// The final movement position, relative to screenSpaceMovementStartPosition
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// Retrieve a snapped position.
(Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
// Move the hitobjects
// Move the hitobjects.
if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition))))
return true;

View File

@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual
/// Provides a clock, beat-divisor, and scrolling capability for test cases of editor components that
/// are preferrably tested within the presence of a clock and seek controls.
/// </summary>
public abstract class EditorClockTestScene : OsuTestScene
public abstract class EditorClockTestScene : OsuManualInputManagerTestScene
{
protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor();
protected new readonly EditorClock Clock;