Skip to main content

Overview

Difficulty calculation is the system that determines how hard a beatmap is to play. It produces:
  • Star Rating - The overall difficulty displayed to players
  • Difficulty Attributes - Detailed breakdown of specific difficulty aspects
  • Performance Points - Player skill rating based on score performance
The calculation processes hit objects through various “skills” that measure different aspects of difficulty (aim, speed, reading, etc.).

DifficultyCalculator

The base class for all difficulty calculation:
public abstract class DifficultyCalculator
{
    protected IBeatmap Beatmap { get; }
    protected readonly IWorkingBeatmap WorkingBeatmap;
    
    public virtual int Version => 0;
    
    protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap);
    
    // Main calculation methods
    public DifficultyAttributes Calculate(CancellationToken cancellationToken = default);
    public DifficultyAttributes Calculate(IEnumerable<Mod> mods, CancellationToken cancellationToken = default);
    
    public List<TimedDifficultyAttributes> CalculateTimed(CancellationToken cancellationToken = default);
    public List<TimedDifficultyAttributes> CalculateTimed(IEnumerable<Mod> mods, CancellationToken cancellationToken = default);
    
    // Abstract methods to implement
    protected abstract DifficultyAttributes CreateDifficultyAttributes(
        IBeatmap beatmap, 
        Mod[] mods, 
        Skill[] skills, 
        double clockRate);
    
    protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(
        IBeatmap beatmap, 
        double clockRate);
    
    protected abstract Skill[] CreateSkills(
        IBeatmap beatmap, 
        Mod[] mods, 
        double clockRate);
    
    // Optional overrides
    protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
    protected virtual IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input);
}
Version
int
A yymmdd version number used to track when reprocessing is required. Increment when changing calculation logic.
CreateDifficultyAttributes
method
required
Combines skill values into final difficulty attributes. Called after all objects are processed.
CreateDifficultyHitObjects
method
required
Converts HitObjects into DifficultyHitObjects that contain analysis data (delta times, distances, etc.).
CreateSkills
method
required
Creates the skills used to analyze difficulty. Each skill measures a different aspect.

Basic Implementation

Here’s a minimal difficulty calculator:
osu.Game.Rulesets.MyRuleset/MyRulesetDifficultyCalculator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;

namespace osu.Game.Rulesets.MyRuleset
{
    public class MyRulesetDifficultyCalculator : DifficultyCalculator
    {
        // Update this when calculation logic changes
        public override int Version => 20260304;
        
        public MyRulesetDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
            : base(ruleset, beatmap)
        {
        }

        protected override DifficultyAttributes CreateDifficultyAttributes(
            IBeatmap beatmap,
            Mod[] mods,
            Skill[] skills,
            double clockRate)
        {
            // For now, return minimal attributes
            return new DifficultyAttributes(mods, 0)
            {
                MaxCombo = beatmap.HitObjects.Count
            };
        }

        protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(
            IBeatmap beatmap,
            double clockRate)
        {
            // Empty implementation - no difficulty objects yet
            return Enumerable.Empty<DifficultyHitObject>();
        }

        protected override Skill[] CreateSkills(
            IBeatmap beatmap,
            Mod[] mods,
            double clockRate)
        {
            // No skills yet - will add later
            return Array.Empty<Skill>();
        }
    }
}
This minimal implementation won’t calculate meaningful difficulty. It’s just a starting point that allows your ruleset to compile.

DifficultyHitObject

Difficulty hit objects contain analysis data:
public class DifficultyHitObject
{
    public readonly HitObject BaseObject;
    public readonly HitObject LastObject;
    public readonly double DeltaTime;
    public readonly double StartTime;
    public readonly double EndTime;
    public readonly int Index;
}
Create custom difficulty objects for your ruleset:
osu.Game.Rulesets.MyRuleset/Difficulty/Preprocessing/MyRulesetDifficultyHitObject.cs
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.MyRuleset.Objects;
using osuTK;

public class MyRulesetDifficultyHitObject : DifficultyHitObject
{
    // Additional analysis data
    public readonly double Distance;
    public readonly double Angle;
    public readonly Vector2 Position;
    
    public MyRulesetDifficultyHitObject(
        HitObject hitObject,
        HitObject lastObject,
        double clockRate,
        List<DifficultyHitObject> objects,
        int index)
        : base(hitObject, lastObject, clockRate, objects, index)
    {
        var currentObject = (MyRulesetHitObject)hitObject;
        var lastObjectPositioned = (MyRulesetHitObject)lastObject;
        
        Position = currentObject.Position;
        
        // Calculate distance from last object
        Distance = Vector2.Distance(Position, lastObjectPositioned.Position);
        
        // Calculate angle between movements
        if (Index >= 2)
        {
            var prevPrevObject = (MyRulesetHitObject)objects[Index - 2].BaseObject;
            Vector2 lastMovement = lastObjectPositioned.Position - prevPrevObject.Position;
            Vector2 currentMovement = Position - lastObjectPositioned.Position;
            
            double dot = Vector2.Dot(lastMovement, currentMovement);
            double det = lastMovement.X * currentMovement.Y - lastMovement.Y * currentMovement.X;
            Angle = Math.Abs(Math.Atan2(det, dot));
        }
    }
}
Implement in your calculator:
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(
    IBeatmap beatmap,
    double clockRate)
{
    var hitObjects = beatmap.HitObjects.Cast<MyRulesetHitObject>().ToList();
    
    for (int i = 1; i < hitObjects.Count; i++)
    {
        yield return new MyRulesetDifficultyHitObject(
            hitObjects[i],
            hitObjects[i - 1],
            clockRate,
            new List<DifficultyHitObject>(),
            i
        );
    }
}

Skills

Skills process difficulty objects and produce difficulty values:
public abstract class Skill
{
    protected IReadOnlyList<Mod> Mods { get; }
    
    protected Skill(Mod[] mods);
    
    public abstract void Process(DifficultyHitObject current);
    public abstract double DifficultyValue();
}
Two common base classes extend this:

StrainSkill

For difficulty that accumulates over time (aim, speed):
osu.Game.Rulesets.MyRuleset/Difficulty/Skills/Aim.cs
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.MyRuleset.Difficulty.Preprocessing;

public class Aim : StrainSkill
{
    private const double skill_multiplier = 26.25;
    private const double strain_decay_base = 0.15;
    
    public Aim(Mod[] mods)
        : base(mods)
    {
    }

    protected override double SkillMultiplier => skill_multiplier;
    protected override double StrainDecayBase => strain_decay_base;

    protected override double StrainValueOf(DifficultyHitObject current)
    {
        var myObject = (MyRulesetDifficultyHitObject)current;
        
        // Calculate strain based on distance and angle
        double distanceStrain = Math.Pow(myObject.Distance, 1.3);
        double angleStrain = Math.Sin(myObject.Angle) * 1.5;
        
        return (distanceStrain + angleStrain) / myObject.DeltaTime;
    }
}
SkillMultiplier
double
Global multiplier applied to final difficulty value. Tune this to scale your star rating appropriately.
StrainDecayBase
double
How quickly strain decays over time. Higher = strain persists longer. Typically 0.1-0.3.
StrainValueOf
method
required
Calculates the strain (difficulty) for a single hit object.

StrainDecaySkill

For continuous difficulty that decays between objects:
osu.Game.Rulesets.MyRuleset/Difficulty/Skills/Speed.cs
public class Speed : StrainDecaySkill
{
    private const double skill_multiplier = 1.43;
    private const double strain_decay_base = 0.3;
    
    public Speed(Mod[] mods)
        : base(mods)
    {
    }

    protected override double SkillMultiplier => skill_multiplier;
    protected override double StrainDecayBase => strain_decay_base;

    protected override double StrainValueAt(DifficultyHitObject current)
    {
        var myObject = (MyRulesetDifficultyHitObject)current;
        
        // Faster = more difficulty
        return 1000.0 / myObject.DeltaTime;
    }
}
Register skills in your calculator:
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
    return new Skill[]
    {
        new Aim(mods),
        new Speed(mods),
    };
}

DifficultyAttributes

Combine skill values into final attributes:
osu.Game.Rulesets.MyRuleset/Difficulty/MyRulesetDifficultyAttributes.cs
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;

public class MyRulesetDifficultyAttributes : DifficultyAttributes
{
    [JsonProperty("aim_difficulty")]
    public double AimDifficulty { get; set; }
    
    [JsonProperty("speed_difficulty")]
    public double SpeedDifficulty { get; set; }
    
    [JsonProperty("approach_rate")]
    public double ApproachRate { get; set; }
    
    [JsonProperty("overall_difficulty")]
    public double OverallDifficulty { get; set; }
    
    public MyRulesetDifficultyAttributes()
    {
    }
    
    public MyRulesetDifficultyAttributes(Mod[] mods, double starRating)
        : base(mods, starRating)
    {
    }
    
    public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
    {
        foreach (var v in base.ToDatabaseAttributes())
            yield return v;
            
        yield return (ATTRIB_ID_AIM, AimDifficulty);
        yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
    }
}
Create attributes in your calculator:
protected override DifficultyAttributes CreateDifficultyAttributes(
    IBeatmap beatmap,
    Mod[] mods,
    Skill[] skills,
    double clockRate)
{
    if (beatmap.HitObjects.Count == 0)
        return new MyRulesetDifficultyAttributes { Mods = mods };
    
    var aimSkill = (Aim)skills[0];
    var speedSkill = (Speed)skills[1];
    
    double aimRating = Math.Sqrt(aimSkill.DifficultyValue()) * 0.0675;
    double speedRating = Math.Sqrt(speedSkill.DifficultyValue()) * 0.0675;
    
    // Combine aim and speed into total star rating
    double starRating = Math.Sqrt(
        Math.Pow(aimRating, 2) + 
        Math.Pow(speedRating, 2)
    );
    
    return new MyRulesetDifficultyAttributes(mods, starRating)
    {
        AimDifficulty = aimRating,
        SpeedDifficulty = speedRating,
        MaxCombo = beatmap.HitObjects.Count,
        ApproachRate = beatmap.Difficulty.ApproachRate,
        OverallDifficulty = beatmap.Difficulty.OverallDifficulty,
    };
}
Star rating typically ranges from 0-10+. Scale your skill values appropriately. Most rulesets multiply by ~0.06-0.07 and take the square root.

PerformanceCalculator

Convert score performance into performance points (pp):
public abstract class PerformanceCalculator
{
    protected readonly Ruleset Ruleset;
    
    protected PerformanceCalculator(Ruleset ruleset);
    
    public PerformanceAttributes Calculate(ScoreInfo score, DifficultyAttributes attributes);
    public PerformanceAttributes Calculate(ScoreInfo score, IWorkingBeatmap beatmap);
    
    protected abstract PerformanceAttributes CreatePerformanceAttributes(
        ScoreInfo score,
        DifficultyAttributes attributes);
}
Implement performance calculation:
osu.Game.Rulesets.MyRuleset/Difficulty/MyRulesetPerformanceCalculator.cs
using System;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;

public class MyRulesetPerformanceCalculator : PerformanceCalculator
{
    public MyRulesetPerformanceCalculator(Ruleset ruleset)
        : base(ruleset)
    {
    }

    protected override PerformanceAttributes CreatePerformanceAttributes(
        ScoreInfo score,
        DifficultyAttributes attributes)
    {
        var myAttributes = (MyRulesetDifficultyAttributes)attributes;
        
        double multiplier = 1.12; // Base multiplier
        
        // Apply mod multipliers
        if (score.Mods.Any(m => m is ModNoFail))
            multiplier *= 0.90;
        if (score.Mods.Any(m => m is ModHidden))
            multiplier *= 1.06;
        
        // Calculate aim pp
        double aimValue = ComputeAimValue(score, myAttributes);
        
        // Calculate speed pp
        double speedValue = ComputeSpeedValue(score, myAttributes);
        
        // Calculate accuracy pp
        double accuracyValue = ComputeAccuracyValue(score, myAttributes);
        
        // Combine all pp values
        double totalValue = Math.Pow(
            Math.Pow(aimValue, 1.1) +
            Math.Pow(speedValue, 1.1) +
            Math.Pow(accuracyValue, 1.1),
            1.0 / 1.1
        ) * multiplier;
        
        return new MyRulesetPerformanceAttributes
        {
            Total = totalValue,
            Aim = aimValue,
            Speed = speedValue,
            Accuracy = accuracyValue,
        };
    }
    
    private double ComputeAimValue(ScoreInfo score, MyRulesetDifficultyAttributes attributes)
    {
        double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
        
        // Penalize misses
        double missPenalty = Math.Pow(0.97, score.Statistics.GetValueOrDefault(HitResult.Miss));
        aimValue *= missPenalty;
        
        // Reward accuracy
        aimValue *= Math.Max(score.Accuracy, 0.5);
        
        return aimValue;
    }
    
    private double ComputeSpeedValue(ScoreInfo score, MyRulesetDifficultyAttributes attributes)
    {
        double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
        
        // Penalize misses more heavily for speed
        double missPenalty = Math.Pow(0.95, score.Statistics.GetValueOrDefault(HitResult.Miss));
        speedValue *= missPenalty;
        
        return speedValue;
    }
    
    private double ComputeAccuracyValue(ScoreInfo score, MyRulesetDifficultyAttributes attributes)
    {
        double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * 
                               Math.Pow(score.Accuracy, 24.0) * 2.83;
        
        return accuracyValue;
    }
}
Register in your ruleset:
osu.Game.Rulesets.MyRuleset/MyRulesetRuleset.cs
public override PerformanceCalculator? CreatePerformanceCalculator()
{
    return new MyRulesetPerformanceCalculator(this);
}
Performance calculation formulas require extensive playtesting and tuning. Start with simplified calculations and refine based on community feedback.

Testing Difficulty Calculation

Test your calculator with known beatmaps:
public partial class TestSceneDifficultyCalculation : OsuTestScene
{
    [Test]
    public void TestDifficultyCalculation()
    {
        var beatmap = CreateBeatmap(new MyRulesetRuleset().RulesetInfo);
        var calculator = new MyRulesetDifficultyCalculator(beatmap.BeatmapInfo.Ruleset, beatmap);
        
        var attributes = calculator.Calculate();
        
        Assert.Greater(attributes.StarRating, 0);
        Assert.Greater(attributes.MaxCombo, 0);
    }
    
    [Test]
    public void TestDoubleTimeMultiplier()
    {
        var beatmap = CreateBeatmap(new MyRulesetRuleset().RulesetInfo);
        var calculator = new MyRulesetDifficultyCalculator(beatmap.BeatmapInfo.Ruleset, beatmap);
        
        var noMod = calculator.Calculate();
        var withDT = calculator.Calculate(new[] { new MyRulesetModDoubleTime() });
        
        Assert.Greater(withDT.StarRating, noMod.StarRating);
    }
}

Best Practices

Start Simple: Begin with basic calculations and add complexity gradually. Test frequently with real beatmaps.
Version Tracking: Increment Version whenever you change calculation logic. This helps track when scores need recalculation.
Balance Star Ratings: Aim for 0-10+ stars to match osu! conventions. Most popular beatmaps fall in the 3-7 star range.
Test with Mods: Ensure difficulty scales appropriately with Double Time, Hard Rock, and other mods.
Performance calculation affects player rankings. Test thoroughly and consider community feedback before finalizing formulas.

Next Steps

Mods System

Learn how mods affect difficulty calculation

Community Rulesets

See real difficulty calculations from other rulesets

Build docs developers (and LLMs) love