Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change Taiko Classic HD, HR and HDHR to better match stable #27136

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 134 additions & 2 deletions osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
Expand All @@ -12,16 +17,143 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IApplicableToDrawableHitObject
{
private DrawableTaikoRuleset drawableTaikoRuleset = null!;

private TaikoPlayfieldAdjustmentContainer adjustmentContainer = null!;

private IReadOnlyList<Mod> mods = Array.Empty<Mod>();

private const float hd_base_fade_out_duration = 0.375f;

private const float hd_base_initial_alpha = 0.75f;

private const float hdhr_base_fade_out_duration = 0.2f;

private const float hdhr_base_initial_alpha = 0.2f;

private const float hidden_base_aspect = 4f / 3f;

/// <summary>
/// Time range at which notes start to become visible in milliseconds.
/// </summary>
private const double nm_fade_in_time_range = 10000d / 1.4d;

/// <summary>
/// Duration notes take to fade in in milliseconds.
/// </summary>
private const float nm_fade_in_duration = 400f;

/// <summary>
/// Whether note fading in is enabled.
/// </summary>
private bool fadeInEnabled = true;

private readonly BindableFloat hiddenFadeOutDuration = new BindableFloat(hd_base_fade_out_duration);

private readonly BindableFloat hiddenInitialAlpha = new BindableFloat(hd_base_initial_alpha);

public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
adjustmentContainer = (TaikoPlayfieldAdjustmentContainer)drawableRuleset.PlayfieldAdjustmentContainer;

// drawableRuleset.Mods should always be non-null here, but just in case.
mods = drawableRuleset.Mods ?? mods;

// Disable uppper bound for playfield aspect ratio
// In stable, nomod time range is limited by fading notes in instead
adjustmentContainer.MaximumAspect = float.PositiveInfinity;
adjustmentContainer.MinimumAspect = 5f / 4f;

TaikoModHidden? hidden = mods.OfType<TaikoModHidden>().FirstOrDefault();

if (mods.OfType<TaikoModHardRock>().Any())
{
// Hardrock disables note fading in
fadeInEnabled = false;

// For hardrock, the playfield time range is clamped to within classicMaxTimeRange and the equivalent
// time range for a 16:10 aspect ratio.
adjustmentContainer.TrimOnOverflow = false;

// Apply stable aspect ratio limits for hardrock (visually taken)
adjustmentContainer.MaximumAspect = 1.963f;

// This is accurate to 4:3, but slightly off for 5:4
adjustmentContainer.MinimumAspect = 1.666f;

adjustmentContainer.MinimumRelativeHeight = 0.26f;
adjustmentContainer.MaximumRelativeHeight = 0.26f;

if (hidden != null)
{
hiddenInitialAlpha.BindTo(hidden.InitialAlpha);
hiddenFadeOutDuration.BindTo(hidden.FadeOutDuration);
drawableRuleset.OnUpdate += d => adjustHidden(
hdhr_base_fade_out_duration, hdhr_base_initial_alpha, 16f / 9f, 0.8f);
}
}
else if (hidden != null)
{
// Hidden disables note fading in
fadeInEnabled = false;

// Stable limits the aspect ratio to 4:3
adjustmentContainer.MaximumAspect = hidden_base_aspect;

// Enable playfield trimming for hidden
adjustmentContainer.TrimOnOverflow = true;

// Enable aspect ratio adjustment for hidden (see adjustHidden)
hiddenInitialAlpha.BindTo(hidden.InitialAlpha);
hiddenFadeOutDuration.BindTo(hidden.FadeOutDuration);
drawableRuleset.OnUpdate += d => adjustHidden(
hd_base_fade_out_duration, hd_base_initial_alpha, hidden_base_aspect);
}
}

public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableTaikoHitObject hit)
{
hit.SnapJudgementLocation = true;

if (fadeInEnabled)
{
drawable.ApplyCustomUpdateState += fadeIn;
}
}
}

// Fade in notes with fixed duration.
private void fadeIn(DrawableHitObject o, ArmedState state)
{
double preempt = nm_fade_in_time_range / drawableTaikoRuleset.ControlPointAt(o.HitObject.StartTime).Multiplier;
double start = o.HitObject.StartTime - preempt;
o.Alpha = 0;

using (o.BeginAbsoluteSequence(start))
{
o.FadeInFromZero(nm_fade_in_duration);
}
}

// Adjust hidden initial alpha and fade out duration for different aspect ratios
private void adjustHidden(
float baseFadeOutDuration,
float baseInitialAlpha,
float baseAspect,
float adjustmentRatio = 1f)
{
float clampedAspect = adjustmentContainer.ClampedCurrentAspect;

float fadeOutDurationAdjustment = clampedAspect / baseAspect - 1;
fadeOutDurationAdjustment *= adjustmentRatio;
hiddenFadeOutDuration.Value = baseFadeOutDuration + fadeOutDurationAdjustment;

float initialAlphaAdjustment = clampedAspect / baseAspect - 1;
initialAlphaAdjustment *= adjustmentRatio;
hiddenInitialAlpha.Value = baseInitialAlpha + initialAlphaAdjustment;
}
}
}
19 changes: 17 additions & 2 deletions osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
Expand Down Expand Up @@ -29,7 +30,20 @@ public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset<TaikoHitOb
/// How long hitobjects take to fade out, in terms of the scrolling length.
/// Range: [0, 1]
/// </summary>
private const float fade_out_duration = 0.375f;
public readonly BindableFloat FadeOutDuration = new BindableFloat(0.375f)
{
MinValue = 0f,
MaxValue = 1f
};

/// <summary>
/// The initial alpha of hitobjects when they appear.
/// </summary>
public readonly BindableFloat InitialAlpha = new BindableFloat(1f)
{
MinValue = 0f,
MaxValue = 1f
};

private DrawableTaikoRuleset drawableRuleset = null!;

Expand All @@ -51,7 +65,8 @@ protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject,
case DrawableHit:
double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier;
double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time;
double duration = preempt * fade_out_duration;
double duration = preempt * FadeOutDuration.Value;
hitObject.Alpha = InitialAlpha.Value;

using (hitObject.BeginAbsoluteSequence(start))
{
Expand Down
7 changes: 1 addition & 6 deletions osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ public partial class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObj
{
public new BindableDouble TimeRange => base.TimeRange;

public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);

public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;

protected new TaikoPlayfieldAdjustmentContainer PlayfieldAdjustmentContainer => (TaikoPlayfieldAdjustmentContainer)base.PlayfieldAdjustmentContainer;
Expand Down Expand Up @@ -102,10 +100,7 @@ public MultiplierControlPoint ControlPointAt(double time)
return ControlPoints[result];
}

public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
};
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer();

protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);

Expand Down
1 change: 1 addition & 0 deletions osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ private void load(OsuColour colours)
new Container
{
Name = "Bar line content",
Masking = true,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = hit_target_width / 2 + hit_target_offset },
Children = new Drawable[]
Expand Down
72 changes: 53 additions & 19 deletions osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.UI;
Expand All @@ -12,12 +11,38 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
public const float MAXIMUM_ASPECT = 16f / 9f;
public const float MINIMUM_ASPECT = 5f / 4f;

private const float stable_gamefield_height = 480f;

public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this setting gone, including the conditional in Update() depending on it? Doesn't this change behaviour of nomod?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was previously set to false in classic to disable aspect range limitations. With these changes playfield aspect range is always limited to some range and never fully unlocked

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe players wanted the unlock in the first place, which is why I'm not sure where this is coming from.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With stable, time range is never fully unlocked:

  • With NM, it's limited to a little over 2:1 by fading notes in beyond that
  • With HD, it's trimmed to 4:3 by having a black bar block the notes beyond that
  • With HR, the playfield "stretches", speeding up notes beyond 16:9, keeping the effective time range to be equal to a maximum of 16:9
  • With HDHR, the HR rule applies and playfield isn't trimmed, but notes fades out much earlier

(Note that these are purely from observations, in code they could be implemented differently)

Perhaps we can name this better - time range has been referred to in aspect ratio equivalents in the community, but it might be better to separate the two concepts here, especially with trimOnOverflow set to true, the effective time range and display aspect ratio can be decoupled. The issue with this is we are still expressing time range limits in aspect ratio equivalents in the code here, and I'm not sure if there's a better way to express them without introducing more arbitrary-seeming fractions or decimals

/// <summary>
/// The maximum aspect ratio the playfield can be adjusted to.
/// </summary>
protected internal float MaximumAspect = 16f / 9f;

/// <summary>
/// The minimum aspect ratio the playfield can be adjusted to.
/// </summary>
protected internal float MinimumAspect = 5f / 4f;

/// <summary>
/// Aspect ratio of the playfield's resolution clamped between <see cref="MinimumAspect"/> and
/// <see cref="MaximumAspect"/>.
/// </summary>
protected internal float ClampedCurrentAspect { get; private set; } = 16f / 9f;

/// <summary>
/// The maximum relative height of the playfield. This is a fraction of the available area.
/// </summary>
protected internal float MaximumRelativeHeight = 1f / 3f;

/// <summary>
/// The minimum relative height of the playfield. This is a fraction of the available area.
/// </summary>
protected internal float MinimumRelativeHeight = 0f;

/// <summary>
/// Whether the playfield should be trimmed when the aspect ratio exceeds the maximum.
/// </summary>
protected internal bool TrimOnOverflow = false;

public TaikoPlayfieldAdjustmentContainer()
{
Expand All @@ -37,41 +62,50 @@ protected override void Update()

float relativeHeight = base_relative_height;

float widthScale = 1.0f;

// Players coming from stable expect to be able to change the aspect ratio regardless of the window size.
// We originally wanted to limit this more, but there was considerable pushback from the community.
//
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is.
if (LockPlayfieldAspectRange.Value)
{
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;
//
// This is separate from CurrentAspect as this needs to be the unbounded aspect ratio.
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;

if (currentAspect > MAXIMUM_ASPECT)
relativeHeight *= currentAspect / MAXIMUM_ASPECT;
else if (currentAspect < MINIMUM_ASPECT)
relativeHeight *= currentAspect / MINIMUM_ASPECT;
if (currentAspect > MaximumAspect)
{
if (TrimOnOverflow)
{
widthScale = MaximumAspect / currentAspect;
}
else
{
relativeHeight *= currentAspect / MaximumAspect;
}
}
else if (currentAspect < MinimumAspect)
{
relativeHeight *= currentAspect / MinimumAspect;
}

// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
relativeHeight = Math.Clamp(relativeHeight, MinimumRelativeHeight, MaximumRelativeHeight);

Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
Width = 1 / Scale.X;
Width = 1 / Scale.X * widthScale;
}

public double ComputeTimeRange()
{
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;

if (LockPlayfieldAspectRange.Value)
currentAspect = Math.Clamp(currentAspect, MINIMUM_ASPECT, MAXIMUM_ASPECT);
ClampedCurrentAspect = Math.Clamp(Parent!.ChildSize.X / Parent!.ChildSize.Y, MinimumAspect, MaximumAspect);

// in a game resolution of 1024x768, stable's scrolling system consists of objects being placed 600px (widthScaled - 40) away from their hit location.
// however, the point at which the object renders at the end of the screen is exactly x=640, but stable makes the object start moving from beyond the screen instead of the boundary point.
// therefore, in lazer we have to adjust the "in length" so that it's in a 640px->160px fashion before passing it down as a "time range".
// see stable's "in length": https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L168
const float stable_hit_location = 160f;
float widthScaled = currentAspect * stable_gamefield_height;
float widthScaled = ClampedCurrentAspect * stable_gamefield_height;
float inLength = widthScaled - stable_hit_location;

// also in a game resolution of 1024x768, stable makes hit objects scroll from 760px->160px at a duration of 6000ms, divided by slider velocity (i.e. at a rate of 0.1px/ms)
Expand Down
Loading