public class MyAwesomeRulesetRuleset : Ruleset{ // Display name shown in the game public override string Description => "My Awesome Gameplay Mode"; // Short identifier used in URLs and file names public override string ShortName => "awesome"; // API version for compatibility tracking public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; // Create the visual gameplay instance public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) { return new DrawableMyAwesomeRulesetRuleset(this, beatmap, mods); } // Create the beatmap converter public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) { return new MyAwesomeRulesetBeatmapConverter(beatmap, this); } // Create the difficulty calculator public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) { return new MyAwesomeRulesetDifficultyCalculator(RulesetInfo, beatmap); } // Define available mods public override IEnumerable<Mod> GetModsFor(ModType type) { switch (type) { case ModType.Automation: return new[] { new MyAwesomeRulesetModAutoplay() }; case ModType.DifficultyIncrease: return new Mod[] { new ModHardRock(), new ModSuddenDeath(), new ModPerfect(), }; case ModType.DifficultyReduction: return new Mod[] { new ModEasy(), new ModNoFail(), }; default: return Array.Empty<Mod>(); } } // Define input key bindings public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[] { new KeyBinding(InputKey.Z, MyAwesomeRulesetAction.Button1), new KeyBinding(InputKey.X, MyAwesomeRulesetAction.Button2), }; // Create the ruleset icon public override Drawable CreateIcon() => new MyAwesomeRulesetIcon(this);}
public partial class DrawableMyAwesomeRulesetRuleset : DrawableRuleset<MyAwesomeRulesetHitObject>{ public DrawableMyAwesomeRulesetRuleset( MyAwesomeRulesetRuleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) : base(ruleset, beatmap, mods) { } // Create the playfield that holds hit objects protected override Playfield CreatePlayfield() => new MyAwesomeRulesetPlayfield(); // Create the input manager for handling player input protected override PassThroughInputManager CreateInputManager() => new MyAwesomeRulesetInputManager(Ruleset.RulesetInfo); // Create the playfield adjustment container for scaling/positioning protected override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new MyAwesomeRulesetPlayfieldAdjustmentContainer();}
public partial class MyAwesomeRulesetPlayfield : Playfield{ private readonly Container hitObjectContainer; public MyAwesomeRulesetPlayfield() { // Add any visual components to your playfield InternalChildren = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Alpha = 0.5f, }, hitObjectContainer = new Container { RelativeSizeAxes = Axes.Both, }, }; } // Register where hit objects should be added protected override void LoadComplete() { base.LoadComplete(); // Hit objects are automatically added to this container RegisterPool<MyAwesomeRulesetHitObject, DrawableMyAwesomeRulesetHitObject>(10); }}
Use object pooling with RegisterPool<THitObject, TDrawable>() for better performance, especially with many hit objects.
public class MyAwesomeRulesetBeatmapConverter : BeatmapConverter<MyAwesomeRulesetHitObject>{ public MyAwesomeRulesetBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatmap, ruleset) { } // Check if this beatmap can be converted public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition); // Convert each hit object protected override IEnumerable<MyAwesomeRulesetHitObject> ConvertHitObject( HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { yield return new MyAwesomeRulesetHitObject { StartTime = original.StartTime, Position = (original as IHasPosition)?.Position ?? Vector2.Zero, Samples = original.Samples, }; }}
Always implement CanConvert() to check if a beatmap is compatible with your ruleset. Return false for incompatible beatmaps to prevent errors.
public partial class TestSceneOsuPlayer : PlayerTestScene{ protected override Ruleset CreatePlayerRuleset() => new MyAwesomeRulesetRuleset(); [Test] public void TestGameplay() { // Your tests here }}
1
Run the test project
dotnet run --project osu.Game.Rulesets.MyAwesomeRuleset.Tests
2
Select test scenes
Use the test browser to select different test scenarios.
API Version Mismatch: Always set RulesetAPIVersionSupported = CURRENT_RULESET_API_VERSION and rebuild when updating osu!.
Null Reference Exceptions: Initialize all required components in constructors. Use nullable annotations to catch potential null issues at compile time.
Performance Issues: Use object pooling for hit objects, and avoid heavy computations in the draw thread.