/// <summary> /// Move the randomised position of a hit circle so that it fits inside the playfield. /// </summary> /// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns> private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo) { var previousPosition = objectInfo.PositionRandomised; objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding( objectInfo.PositionRandomised, (float)circle.Radius ); circle.Position = objectInfo.PositionRandomised; return(objectInfo.PositionRandomised - previousPosition); }
public void ApplyToBeatmap(IBeatmap beatmap) { if (!(beatmap is OsuBeatmap osuBeatmap)) { return; } var hitObjects = osuBeatmap.HitObjects; Seed.Value ??= RNG.Next(); rng = new Random((int)Seed.Value); RandomObjectInfo previous = null; float rateOfChangeMultiplier = 0; for (int i = 0; i < hitObjects.Count; i++) { var hitObject = hitObjects[i]; var current = new RandomObjectInfo(hitObject); // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams if (i % 3 == 0) { rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; } if (hitObject is Spinner) { previous = null; continue; } applyRandomisation(rateOfChangeMultiplier, previous, current); hitObject.Position = current.PositionRandomised; // update end position as it may have changed as a result of the position update. current.EndPositionRandomised = current.PositionRandomised; if (hitObject is Slider slider) { moveSliderIntoPlayfield(slider, current); } previous = current; } }
/// <summary> /// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. /// </summary> private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo) { var minMargin = getMinSliderMargin(slider); slider.Position = new Vector2( Math.Clamp(slider.Position.X, minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right), Math.Clamp(slider.Position.Y, minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom) ); currentObjectInfo.PositionRandomised = slider.Position; currentObjectInfo.EndPositionRandomised = slider.EndPosition; shiftNestedObjects(slider, currentObjectInfo.PositionRandomised - currentObjectInfo.PositionOriginal); }
/// <summary> /// Returns the final position of the hit object /// </summary> /// <returns>Final position of the hit object</returns> private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current) { if (previous == null) { var playfieldSize = OsuPlayfield.BASE_SIZE; current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y); return; } float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object // to allow jumps and prevent too sharp turns during streams. var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / playfield_diagonal; current.AngleRad = (float)randomAngleRad + previous.AngleRad; if (current.AngleRad < 0) { current.AngleRad += 2 * (float)Math.PI; } var posRelativeToPrev = new Vector2( distanceToPrev * (float)Math.Cos(current.AngleRad), distanceToPrev * (float)Math.Sin(current.AngleRad) ); posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev); current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); var position = previous.EndPositionRandomised + posRelativeToPrev; // Move hit objects back into the playfield if they are outside of it, // which would sometimes happen during big jumps otherwise. position.X = MathHelper.Clamp(position.X, 0, OsuPlayfield.BASE_SIZE.X); position.Y = MathHelper.Clamp(position.Y, 0, OsuPlayfield.BASE_SIZE.Y); current.PositionRandomised = position; }
private List <RandomObjectInfo> randomiseObjects(IEnumerable <OsuHitObject> hitObjects) { Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects"); var randomObjects = new List <RandomObjectInfo>(); RandomObjectInfo?previous = null; float rateOfChangeMultiplier = 0; foreach (OsuHitObject hitObject in hitObjects) { var current = new RandomObjectInfo(hitObject); randomObjects.Add(current); // rateOfChangeMultiplier only changes every 5 iterations in a combo // to prevent shaky-line-shaped streams if (hitObject.IndexInCurrentCombo % 5 == 0) { rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; } if (previous == null) { current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); } else { current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object // to allow jumps and prevent too sharp turns during streams. // Allow maximum jump angle when jump distance is more than half of playfield diagonal length current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f)); } previous = current; } return(randomObjects); }
/// <summary> /// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. /// </summary> private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo) { // Min. distances from the slider's position to the playfield border var minMargin = new MarginPadding(); foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderEndCircle)) { if (!(hitObject is OsuHitObject osuHitObject)) { continue; } var relativePos = Vector2.Subtract(osuHitObject.Position, slider.Position); minMargin.Left = Math.Max(minMargin.Left, -relativePos.X); minMargin.Right = Math.Max(minMargin.Right, relativePos.X); minMargin.Top = Math.Max(minMargin.Top, -relativePos.Y); minMargin.Bottom = Math.Max(minMargin.Bottom, relativePos.Y); } if (slider.Position.X < minMargin.Left) { slider.Position = new Vector2(minMargin.Left, slider.Position.Y); } else if (slider.Position.X + minMargin.Right > OsuPlayfield.BASE_SIZE.X) { slider.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - minMargin.Right, slider.Position.Y); } if (slider.Position.Y < minMargin.Top) { slider.Position = new Vector2(slider.Position.X, minMargin.Top); } else if (slider.Position.Y + minMargin.Bottom > OsuPlayfield.BASE_SIZE.Y) { slider.Position = new Vector2(slider.Position.X, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom); } currentObjectInfo.PositionRandomised = slider.Position; currentObjectInfo.EndPositionRandomised = slider.EndPosition; }
/// <summary> /// Returns the final position of the hit object /// </summary> /// <returns>Final position of the hit object</returns> private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current) { if (previous == null) { var playfieldSize = OsuPlayfield.BASE_SIZE; current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y); return; } float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object // to allow jumps and prevent too sharp turns during streams. // Allow maximum jump angle when jump distance is more than half of playfield diagonal length var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, distanceToPrev / (playfield_diagonal * 0.5f)); current.AngleRad = (float)randomAngleRad + previous.AngleRad; if (current.AngleRad < 0) { current.AngleRad += 2 * (float)Math.PI; } var posRelativeToPrev = new Vector2( distanceToPrev * (float)Math.Cos(current.AngleRad), distanceToPrev * (float)Math.Sin(current.AngleRad) ); posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev); current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); current.PositionRandomised = previous.EndPositionRandomised + posRelativeToPrev; }
/// <summary> /// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. /// </summary> /// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns> private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo) { var possibleMovementBounds = calculatePossibleMovementBounds(slider); var previousPosition = objectInfo.PositionRandomised; // Clamp slider position to the placement area // If the slider is larger than the playfield, force it to stay at the original position var newX = possibleMovementBounds.Width < 0 ? objectInfo.PositionOriginal.X : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right); var newY = possibleMovementBounds.Height < 0 ? objectInfo.PositionOriginal.Y : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom); slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY); objectInfo.EndPositionRandomised = slider.EndPosition; shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal); return(objectInfo.PositionRandomised - previousPosition); }
private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo?previous, RandomObjectInfo?beforePrevious) { float previousAbsoluteAngle = 0f; if (previous != null) { Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; Vector2 relativePosition = previous.HitObject.Position - earliestPosition; previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); } float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; var posRelativeToPrev = new Vector2( current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) ); Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre; posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); current.PositionRandomised = lastEndPosition + posRelativeToPrev; }
public void ApplyToBeatmap(IBeatmap beatmap) { if (!(beatmap is OsuBeatmap osuBeatmap)) { return; } var hitObjects = osuBeatmap.HitObjects; Seed.Value ??= RNG.Next(); rng = new Random((int)Seed.Value); RandomObjectInfo previous = null; float rateOfChangeMultiplier = 0; for (int i = 0; i < hitObjects.Count; i++) { var hitObject = hitObjects[i]; var current = new RandomObjectInfo(hitObject); // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams if (i % 3 == 0) { rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; } if (hitObject is Spinner) { previous = null; continue; } applyRandomisation(rateOfChangeMultiplier, previous, current); hitObject.Position = current.PositionRandomised; // update end position as it may have changed as a result of the position update. current.EndPositionRandomised = current.PositionRandomised; switch (hitObject) { case Slider slider: // Shift nested objects the same distance as the slider got shifted in the randomisation process // so that moveSliderIntoPlayfield() can determine their relative distances to slider.Position and thus minMargin shiftNestedObjects(slider, Vector2.Subtract(slider.Position, current.PositionOriginal)); var oldPos = new Vector2(slider.Position.X, slider.Position.Y); moveSliderIntoPlayfield(slider, current); // Shift them again to move them to their final position after the slider got moved into the playfield shiftNestedObjects(slider, Vector2.Subtract(slider.Position, oldPos)); break; } previous = current; } }
public void ApplyToBeatmap(IBeatmap beatmap) { if (!(beatmap is OsuBeatmap osuBeatmap)) { return; } var hitObjects = osuBeatmap.HitObjects; Seed.Value ??= RNG.Next(); rng = new Random((int)Seed.Value); RandomObjectInfo previous = null; float rateOfChangeMultiplier = 0; for (int i = 0; i < hitObjects.Count; i++) { var hitObject = hitObjects[i]; var current = new RandomObjectInfo(hitObject); // rateOfChangeMultiplier only changes every 5 iterations in a combo // to prevent shaky-line-shaped streams if (hitObject.IndexInCurrentCombo % 5 == 0) { rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; } if (hitObject is Spinner) { previous = null; continue; } applyRandomisation(rateOfChangeMultiplier, previous, current); // Move hit objects back into the playfield if they are outside of it Vector2 shift = Vector2.Zero; switch (hitObject) { case HitCircle circle: shift = clampHitCircleToPlayfield(circle, current); break; case Slider slider: shift = clampSliderToPlayfield(slider, current); break; } if (shift != Vector2.Zero) { var toBeShifted = new List <OsuHitObject>(); for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--) { // only shift hit circles if (!(hitObjects[j] is HitCircle)) { break; } toBeShifted.Add(hitObjects[j]); } if (toBeShifted.Count > 0) { applyDecreasingShift(toBeShifted, shift); } } previous = current; } }