Introduction
This book contains unofficial documentation for osu!framework. The idea is that this documentation gets expanded over time with extensive, beginner-friendly explanations.
While osu!framework is a great 2D framework, documentation can be severely lacking. The main focus of its maintainers is on developing osu! and most of its developers are already intimately familiar with its inner workings. So while official documentation exists, it is fairly limited and new developers are left to do their own research and read the o!f source code to figure out how everything fits together and what features they have at their disposal.
With this book, I try to document some concepts and features in a simple to understand but comprehensive way, in the hope that it serves as a useful resource to fall back on. As a goal, you shouldn't have to sift through osu!framework code to create a game after reading the completed version of this book. I also include a test project with interactive examples, so you can visually experiment with different concepts.
This work is licensed under CC BY 4.0
Why osu!framework?
osu!framework is an open-source 2D game framework written in C#, built to develop the new iteration of osu!, a free-to-play rhythm game. It tries to go further than traditional game frameworks by supplying functionality you would usually find on game-level. This makes it a great choice for developers looking to create a cross-platform 2D game without having to write all the low-level plumbing themselves.
Benefits
Cross-platform support
osu!framework is built to be completely cross-platform. It has excellent support for major desktop platforms (Windows, macOS, Linux) and supports mobile platforms too (Android, iOS). A game built for the desktop can be ported to a mobile platform with minimal work, requiring only a startup project and mobile-friendly input methods to be added.
As the framework tries to abstract all platform-specific code away, it is not unthinkable that osu!framework games may be run on other platforms such as the browser in the future.
High performance
Targeting rhythm games in particular, the bar for performance and input latency is very high. A basic osu!framework game should easily have draw FPS in the thousands and several times that for update FPS. At the time of writing, the main menu of osu!lazer, the flagship osu!framework game, reaches 1200 draw FPS and 10 000 update FPS when uncapped on a 4k monitor with a Ryzen 5 3600 and a GTX 1660 Super. On a mobile device (Huawei Mate 20) the same scene gets about 1800 update FPS with the draw FPS capped to the screens 60Hz display rate.
Many built-in components
During the development of osu! a lot of functionality was considered to be useful to other projects and was thus moved to osu!framework. This functionality includes:
- Input support for mice, touch screens, drawing tablets, keyboards, joysticks and MIDI devices
- Standard UI components such as buttons, sliders, dropdowns, checkboxes, text inputs, color pickers, file pickers, progress bars, context menus, tab menus and more
- Layout abstractions such as a screen stack and overlays
- Database-backed storage
- Localization
- A config system
- Basic physics
- Extensive developer tooling
- More...
Extensibility
Many parts of the framework can be extended or swapped out, such as the audio and window backends, storage and input methods.
Developer tooling
osu!framework comes with a built-in draw tree explorer, on-screen log display, advanced frame statistics overlay and more.

Additionally, there exists a test browser that can be used with dynamic compilation to rapidly develop new UI components and execute automated tests. This allows for test-driven development and can be used to ensure high code coverage.

Downsides
No 3D support
As osu!framework is a 2D game framework, it currently does not have any 3D capabilities. There are plans to include basic 3D support, but it will never become a full-fledged 3D framework. Therefore, it will not be fit for games that require such interaction.
Bloat
While osu!framework is fast, it is by no means small. As of time of writing, a release build of a small desktop game with no assets measures at about 220 MB. The majority of this size bloat comes from the .NET runtime and dependencies. Removing unused runtimes can reduce total size by 50-85%, depending on platform. Link-time optimization may bring this down more.
Existing projects
This page lists some public, open-source projects that use osu!framework.
osu!
osu! (also known as osu!lazer) is the flagship osu!framework project. It is the main driving force behind the framework and is expected to be developed for many years to come. If you want to see osu!framework in action, this is the place.
osu! has 4 game modes of gameplay known as rulesets and supports dynamically loading additional community-made game modes. Each game mode mimics a popular rhythm game such "Osu! Tatakae! Ouendan!", "Taiko No Tatsujin" and "Stepmania" and has a series of mods that can alter gameplay, making the gameplay easier, harder or just plain different. Players can play on their own and submit their best scores to online leaderboards, or play against each other in multiplayer lobbies. All charts (known as beatmaps) are community-made through the in-game editor and peer-reviewed to strict standards before achieving "ranked" status.
osu! rulesets
osu! supports custom game modes, which also use osu!framework to provide UI and gameplay elements. They can be handy resources to see how certain game modes are drawn.
A more complete list of community rulesets can be found here, but here are some highlights:
- sentakki, which aims to recreate Sega's Maimai arcade game
- Karaoke, which uses microphone input to play karaoke
- bosu, which mimics "I wanna be the Boshy"-style platformers
Vignette
Vignette is a VTuber toolkit made with osu!framework. It uses face tracking software to sync up your head movement with a virtual character.
Project Setup
This chapter describes the process of setting up a new game using osu-framework.
While you can start from scratch, it is easier to start a new project using a template. osu!framework offers 2 templates which both come with a Desktop, iOS and Visual Test project. They also include opinionated C# style rules that match osu!framework. Additionally, they come with launch templates for VS Code and Rider. They do not, however, come with a gitignore file, so you will have to add that yourself.
Creating a template-based project
The instructions to create a template project can be found in the osu-framework repo, but they are fairly simple.
First install the NuGet package containing the template pack. If it is already installed, this command will update it to the latest version:
dotnet new -i ppy.osu.Framework.Templates
Then, simply create a new project using the template:
dotnet new osu-framework-game -n MyNewGame
You can replace osu-framework-game with osu-framework-flappy-game. MyNewGame can be replaced by the name of your game.
TemplateGame (Empty Template)
The osu-framework-game template contains a very basic game, with just a spinning sprite box and some text on a pink background. It uses a ScreenStack that sits in a DrawSizePreservingFillContainer to ensure content scales with all screen sizes. The test project contains a standard test browser with some basic visual tests.

FlappyDon (Flappy Bird clone)
The osu-framework-flappy-game template implements a Flappy Bird clone in osu!framework. In addition to the functionality in osu-framework-game, it shows how to handle input, play sound samples and handle game state in a basic way. It doesn't serve as a better base for a game than the empty template, but it can be a good, albeit basic, resource to learn about the framework.

There are many concepts not explained by the FlappyDon template such as UI tests, custom fonts and screen stacks. therefore, it is best to read through the osu!framework source code, through the source code of games made with osu!framework (such as osu! itself) or to read the rest of this unofficial documentation.
Note about assets: the FlappyDon game contains assets from various sources. Excluding the assets taken from osu-resources, they were all originally licensed under permissive licenses and don't require anything more than the MIT license requires. The osu-resources resources were originally licensed under CC-BY-NC 4.0, but are now licensed under the MIT license since they are in the osu-framework repository and don't explicitly have another license. This means you should be free to use all resources in this template.
Last verified against v2021.714.0
Creating a project from scratch
This chapter describes how to set up a basic osu!framework game from scratch. It follows the standard project layout, where game logic is put in a Game project and an executable Desktop project is used for Windows/macOS/Linux support.
You can also read the official documentation on how to set up a project.
For this chapter, we'll use an imaginary game called "My Platformer". This makes it easier to explain naming schemes in different contexts.
Creating a Game project
Your Game project will contain all your game logic.
Start by creating a library project that references the osu.ppy.Framework NuGet package. Its name should be your game name followed by .Game, such as MyPlatformer.Game. Your csproj file should look roughly like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework" Version="2021.714.0"/>
</ItemGroup>
</Project>
Next, create your Game class. This is a class that inherits from osu.Framework.Game and is typically named by adding "Game" to your game name. It will be the startup class for your game, and be the highest level of the draw hierarchy you can control.
Here is an example of a simple Game class that spins a box on the screen:
public class MyPlatformerGame : osu.Framework.Game
{
private Box RotatingBox { get; set; }
[BackgroundDependencyLoader]
private void Load()
{
Add(RotatingBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Colour = Color4.Tomato,
});
}
protected override void Update()
{
base.Update();
RotatingBox.Rotation += (float)Time.Elapsed * 0.1f;
}
}
Note: Creating animations by updating them in the Update method is not recommended. In later chapters, we'll look at Transforms which make this process a lot simpler.
Creating a Desktop platform project
A platform project is a runnable project that provides a means to run your game on a platform. The most common one is the Desktop platform which runs on Windows, macOS and Linux, and it's the one you'll mainly use for testing regardless of the platform you're actually targeting.
Create a new executable project with the WinExe output type. This ensures that no console window gets created when you run your game. Then add a reference to your Game project. Your csproj file should look roughly like this like so:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MyPlatformer.Game\MyPlatformer.Game.csproj"/>
</ItemGroup>
</Project>
Next, in your Program.cs class, construct and run your game:
internal static class Program
{
private static void Main(string[] args)
{
using (GameHost host = Host.GetSuitableHost(@"my-platformer"))
using (var game = new MyPlatformerGame())
host.Run(game);
}
}
The argument you pass to Host.GetSuitableHost(string) is your game name. This will be used as folder name for storage and settings, among other things.
If you run your Desktop project now, it should create a window with your game running in it.

Splitting up the Game class
Typically, the Game class is split up to also have a "GameBase" class. This class will contain setup code that would be useful for e.g. the visual test browser. In that case, your code may look like this:
public class MyPlatformerGameBase : osu.Framework.Game
{
public Bindable<Color4> MainColor { get; set; }
[BackgroundDependencyLoader]
private void Load()
{
// Set up an online texture store so we can load an image from a URL
Textures.AddStore(Host.CreateTextureLoaderStore(new OnlineStore()));
// Create a bindable variable that will be used by the game
MainColor = new Bindable<Color4>(Color4.White);
}
}
public class MyPlatformerGame : MyPlatformerGameBase
{
private Sprite RotatingSprite { get; set; }
[BackgroundDependencyLoader]
private void Load()
{
Add(RotatingSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Texture = Textures.Get("https://i.imgur.com/mu6UPM7.png"),
});
// Ensure the color of the image updates if the color variable changes
MainColor.BindValueChanged(ev => RotatingSprite.Colour = ev.NewValue);
}
protected override void Update()
{
base.Update();
// Change the main color after 5 seconds
if (Time.Current > 5000f && MainColor.IsDefault)
MainColor.Value = Color4.Tomato;
RotatingSprite.Rotation += (float)Time.Elapsed * 0.1f;
}
}
The following code usually resides in the base class:
- global variables that are not exclusively useful to running the full game
- loading resources such as fonts and textures
- setting up dependency injection
The following code usually resides in the main Game class:
- adding Drawables to the draw hierarchy
- starting game logic
In some cases, a Game subclass is made for a platform (i.e. Desktop, Android, iOS). This can be used to provide platform-specific functionality such as getting battery status on mobile or having special update checks.
Adding Visual Tests
To add a visual test project to your solution, see Setup Test Project.
Last verified against v2021.714.0.
Positioning
By default, every drawable is positioned in the coordinate system of its parent, using absolute coordinates. This means that if a container is positioned at (100,100) and its child at (50,50), its absolute coordinate will be (150,150).
It is perfectly fine to set a position outside the parent, e.g. position (150,150) if the parent has size (100,100).
Anchor and Origin
Anchor and Origin are used to determine where to place a drawable relative to its Parent. The default value for Anchor and Origin are TopLeft, which means that if a drawable is positioned at (0,0), it's top-left corner will be aligned to the top-left corner of its Parent.
The Origin of a drawable indicates which point to position the Drawable with. The Anchor of a drawable indicates where to attach that point to the Parent. The Position of a drawable is an offset to that point, in the coordinate system of the parent.
Some examples:
Origin=TopLeftandAnchor=TopLeft: The top left of the drawable is attached to the top left of its parent. This is the default, and means the child is inside the parent.Origin=CentreandAnchor=Centre: The center of the drawable is attached to the center of its parent. The drawable is perfectly centered in the middle of its parent.Origin=CentreLeftandAnchor=CentreRight: The right of the drawable is attached to the left of the parent. This means the drawable is outside the parent, on the left side.
More examples, in image form:
| settings | image |
|---|---|
Anchor=TopLeft, Origin=Centre | ![]() |
Anchor=CentreRight, Origin=CentreRight | ![]() |
Anchor=BottomRight, Origin=CentreRight | ![]() |
There is an interactive example in the FrameworkExamples project in the GitHub repository.
RelativePositionAxes
The RelativePositionAxes controls the coordinate system for the Position of a CompositeDrawable/Container's Children. By default, its value is Axes.None, implying an absolute coordinate system. It can be set to Axes.X, Axes.Y and Axes.Both, indicating the axes to convert to a relative coordinate system. Under the relative coordinate system, Position indicates the position in the parent relatively, where 1 indicates the Parent's full width/height for X and Y respectively.
As an example, a drawable with Anchor=TopLeft, RelativeSizeAxes=Both and Position=(0.5,0.5) is functionally equivalent to a drawable with Anchor=Centre, RelativeSizeAxes=None and Position=(0,0): both are positioned at the center of their parent.
RelativeChildSize
Contrary to what the name suggests, this property indicates the scale/size of the relative coordinate system and does not influence the draw size of the children. If it is set to 2x2, then the (1,1) position will be where (0.5,0.5) was previously. You are essentially changing the relative coordinate system from 1 by 1 to that value.
RelativeChildSize 1x1, Position (1,0) | RelativeChildSize 0.5x1, Position (0.5,0) | RelativeChildSize 2x1, Position (1,0) |
|---|---|---|
![]() | ![]() | ![]() |
Together with RelativeChildOffset, this can be used to create completely arbitrary coordinate systems.
RelativeChildOffset
If a child has RelativePositionAxes set, then RelativeChildOffset indicates an offset to the (0,0) position. The value set in RelativeChildOffset indicates the new coordinate at the previous (0,0) point.
RelativeChildOffset (0,0) | RelativeChildOffset (-0.2,0.1) | RelativeChildOffset (-1,-1) |
|---|---|---|
![]() | ![]() | ![]() |
Together with RelativeChildPosition, this can be used to create completely arbitrary coordinate systems.
Sizing
The Size of a drawable indicates how large a Drawable should be when drawn on the screen. By default, this is directly translated to pixels on the screen, if Parents have no scaling.
AutoSizeAxes
For composite and container drawables, this indicates that the size of the drawable is automatically calculated based on the size of children. In practice, this means that the Width/Height of a CompositeDrawable/Container is calculated to fit all its children.
Calculation goes roughly like this:
- Loop over every drawable in InternalChildren
- If the child has matching BypassAutoSizeAxes set, ignore it (e.g. parent has
AutoSizeAxes=Bothand child hasBypassAutoSizeAxes=X, then it is not included for Width calculations) - If the child has matching RelativePositionAxes or RelativeSizeAxes, ignore it.
- Take the maximum value for
Child.X+Child.WidthorChild.Y+Child.Height, and use that for Width or Height.- Margin and Padding are also taken into account, but have been omitted here for clarity.
Auto-size calculations as of 2021/7/11 can be found here.
It is worth noting that the parent will not become smaller than 0x0, i.e. it will not get a negative size if all drawables are positioned under 0,0.
The effect of BypassAutoSizeAxes looks like this:
Both Boxes BypassAutoSizeAxes=None | Blue Box BypassAutoSizeAxes=Both | Green Box BypassAutoSizeAxes=Both |
|---|---|---|
![]() | ![]() | ![]() |
There is an interactive example in the FrameworkExamples project in the GitHub repository.
The AutoSizeAxes property is only present on CompositeDrawable and Container, not on Drawable. BypassAutoSizeAxes is present on every Drawable.
RelativeSizeAxes
This property indicates how the Size (Width/Height) properties are interpreted. If an axis is not a relative size axis, its Size component is interpreted as a value in pixels (assuming no Scaling of parents). If an axis is a relative size axis, its Size component is interpreted as a fraction of the Size of its Parent.
In practice, this means that if a Drawable has a RelativeSizeAxes of Both and a Size of 1x1, it will grow to fit its parent. If it has a RelativeSizeAxes of X and its Width is 0.5, it will grow to be exactly half the size of its Parent. If the size of the parent updates, the size of the child will automatically update too.
Transforms
A transform is a gradual, predetermined change over time to a property of a Drawable. They are comparable to CSS transitions. osu!framework allows you to add transforms to Drawables and choose the easing function that is used to interpolate between the start and end value of the changed property.
The most common properties used by transitions are:
- Color
- Position and Scale
- Opacity (to fade drawables in and out)
- Rotation
So, some scenarios you can create with this are:
- Expanding/coloring a button when it is being hovered
- Fading out particles
- Continuously rotating or bouncing UI elements
- Moving an element out of the way after clicking it
Basic Usage
To use a single Transform on a drawable, simply call its respective extension method:
// instantly set scale to 1.5x
myBox.ScaleTo(1.5f);
This returns a TransformSequence, so you can chain multiple Transforms to execute them all at once:
// instantly set scale to 1.5x and rotate 90 degrees clockwise
myBox.ScaleTo(1.5f).RotateTo(90f);
By default, transforms will execute instantly, with no visible animation. To make a transform happen over time, pass in a time parameter:
// scale to 1.5x over 1000ms, with no/linear easing
myBox.ScaleTo(1.5f, 1000f);
When no other easing type is provided, the transform will happen linearly. This means that in this scaling example, the size of the box will be 1.125x after 250ms, 1.25x after 500ms and 1.375x after 750ms. Easing can be used to change the rate of change at which this happens and osu!framework supports many types of them. To get an idea of some of them, see easings.net.
// scale to 1.5x over 1000ms, with a sudden start and smooth ending
myBox.ScaleTo(1.5f, 1000f, Easing.OutQuint);
If you want one transition to happen after another, you can use Then. This will wait until all transforms before it are executed before running the next batch.
// scaling and rotation start at the same time
// since scaling takes longer, rotating finishes first
myBox.ScaleTo(1.5f, 1000f).RotateTo(90f, 500f);
// box first scales up, *then* rotates 90 degrees
myBox.ScaleTo(1.5f, 1000f).Then().RotateTo(90f, 500f);
// scale to 1.5x and rotate 90 degrees, *then* scale to 0.5x and rotate back.
myBox
.ScaleTo(1.5f, 1000f).RotateTo(90f, 500f)
.Then()
.ScaleTo(0.5f, 250f).RotateTo(0f, 250f);
You can loop a series of transforms with Loop:
// fade between blue and red every 2 seconds
myBox
.FadeColour(Color4.RoyalBlue, 1000f)
.Then()
.FadeColour(Color4.Tomato, 1000f)
.Loop();
With Delay, you can delay the transforms after it. It is different from Then in that it won't wait for all transitions to finish. This allows you to start a transition while another one is still running:
// move downwards over 1s, and start fading out halfway
myBox.MoveToOffset(new Vector2(0f, 100f), 1000f).Delay(500f).FadeOut(500f);
If you just fade out a drawable, it will remain in the draw hierarchy and be active. You can make a drawable clean up after the transitions are done through Expire. This will queue the drawable for disposal, so it cannot be used after it has expired!
// fade out and mark for disposal
myBox.FadeOut(500f).Expire();
Example: osu!lazer menu buttons
This example shows how to recreate part of the main menu of osu!lazer, including bouncing icons.

[BackgroundDependencyLoader]
private void Load()
{
const int children = 4;
Child = new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = Enumerable.Range(0, children).Select(i => new MenuButton(i)).ToList(),
};
}
private class MenuButton : CompositeDrawable
{
private static Color4[] Colors = {Color4.Aqua, Color4.RoyalBlue, Color4.Tomato, Color4.HotPink};
private readonly int _idx;
private SpriteText _icon;
public MenuButton(int idx)
{
_idx = idx;
}
[BackgroundDependencyLoader]
private void Load()
{
Size = new Vector2(200f, 150f);
InternalChildren = new Drawable[]
{
new Box
{
Name = "Background",
RelativeSizeAxes = Axes.Both,
Depth = 1,
Colour = Colors[_idx % Colors.Length],
},
_icon = new SpriteText
{
Text = ((char) ('A' + _idx)).ToString(),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = FontUsage.Default.With(size: 36f),
Shadow = true,
},
};
}
protected override bool OnHover(HoverEvent e)
{
const float durationStep = 250f;
this.ResizeTo(new Vector2(300f, 150f), 500f, Easing.OutElastic);
// using InCirc/OutCirc for Y movement because it appears similar to normal gravity
// using InSine/OutSine for X movement and rotation because it appears fairly smooth
_icon
// move to left
.MoveToX(-15f, durationStep, Easing.OutSine)
.MoveToY(15f, durationStep, Easing.InCirc)
.RotateTo(-30f, durationStep, Easing.OutSine)
.Then()
// move to top
.MoveToX(0f, durationStep, Easing.InSine)
.MoveToY(-35f, durationStep, Easing.OutCirc)
.RotateTo(0f, durationStep, Easing.InSine)
.Then()
// move to right
.MoveToX(15f, durationStep, Easing.OutSine)
.MoveToY(15f, durationStep, Easing.InCirc)
.RotateTo(30f, durationStep, Easing.OutSine)
.Then()
// move to top again
.MoveToX(0f, durationStep, Easing.InSine)
.MoveToY(-35f, durationStep, Easing.OutCirc)
.RotateTo(0f, durationStep, Easing.InSine)
// loop
.Loop();
// do not propagate hover event down
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
// the icon transition is a loop, we should stop it first
_icon.ClearTransforms();
this.ResizeTo(new Vector2(200f, 150f), 500f, Easing.OutElastic);
_icon.MoveTo(Vector2.Zero, 250f, Easing.OutQuint).RotateTo(0f, 250f, Easing.OutQuint);
base.OnHoverLost(e);
}
}
You can view this example in the FrameworkExamples project.
Creating a transform for other properties
You can create transforms for any property whose type is supported by the framework or implements the IInterpolable interface.
The following types are supported by default in the framework:
- all C# number types, such as byte, uint, float, decimal, etc.
ColourInfoEdgeEffectParametersSRGBColourColor4Colour4Vector2RectangleF
Additionally, the framework implements IInterpolable for the following classes:
MarginPadding
To transform a property, use TransformTo:
box.TransformTo(nameof(box.Shear), Vector2.Zero, 1000, Easing.OutCubic);
The first parameter to TransformTo should be the name of a property with a supported type. In this case the Shear property has type Vector2. It is recommended to use nameof to ensure the name is correct.
Transforming Bindables
Transforming Bindable properties of a Drawable is also possible, as long as its generic parameter is supported. Instead of using TransformTo, use TransformBindableTo:
// checkerboard.TileCountX is of type BindableInt
checkerboard.TransformBindableTo(checkerboard.TileCountX, 32, 1000, Easing.OutCubic);
Implementing IInterpolable
When types are not supported for transforms, the framework checks if they implement IInterpolable. IInterpolable contains a ValueAt method that will then be used to decide the value of that property at a certain point.
Important: While
IInterpolable.ValueAtis an instance method, the class it exists in will be created withFormatterServices.GetSafeUninitializedObjectmeaning all instance members will be zeroed out. Accessing them can cause undefined behavior. You should program yourValueAtmethod as if it is static.
As an example, the following code implements a wrapper type around a string that allows interpolating the length of that string.

private const string LoremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" +
"tempor incididunt ut labore et dolore magna aliqua.";
private const string LoremIpsumShort = "Lorem ipsum dolor sit amet";
[BackgroundDependencyLoader]
private void Load()
{
InterpolatableSpriteText text;
Child = text = new InterpolatableSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
InterpolableText = new InterpolableString(""),
};
AddStep("Interpolate to short lorem",
() => text.TransformTo(nameof(text.InterpolableText), new InterpolableString(LoremIpsumShort),
2000, Easing.OutQuint));
AddStep("Interpolate to long lorem",
() => text.TransformTo(nameof(text.InterpolableText), new InterpolableString(LoremIpsum),
2000, Easing.OutQuint));
}
private class InterpolatableSpriteText : SpriteText
{
public InterpolableString InterpolableText
{
get => new(base.Text.ToString());
set => base.Text = value.Value;
}
}
/// <summary>
/// A string wrapper that can interpolate between lengths. A better interpolation would
/// </summary>
private readonly struct InterpolableString : IInterpolable<InterpolableString>
{
public readonly string Value;
public InterpolableString(string value)
{
Value = value;
}
public InterpolableString ValueAt<TEasing>(double time, InterpolableString startValue,
InterpolableString endValue,
double startTime, double endTime, in TEasing easing) where TEasing : IEasingFunction
{
var longestValue = startValue.Value.Length > endValue.Value.Length ? startValue : endValue;
var shortestValue = startValue.Value.Length <= endValue.Value.Length ? startValue : endValue;
// we can only go from "AAA" to "AAABBB" and vice versa, not from "AAABBB" to "CCC"
if (!longestValue.Value.StartsWith(shortestValue.Value))
throw new Exception("Cannot interpolate between strings with different start");
var newLength = Interpolation.ValueAt(
time, startValue.Value.Length, endValue.Value.Length,
startTime, endTime, easing);
newLength = Math.Clamp(newLength, 0, longestValue.Value.Length);
return new InterpolableString(longestValue.Value[..newLength]);
}
}
You can view this example in the FrameworkExamples project.
Custom easing
If none of the existing easing functions fit your needs, you can define your own:
/// <summary>
/// Moves 450 degrees on a sine function.
/// </summary>
private class SineEasing : IEasingFunction
{
// static instance to use, to prevent unneeded allocations
public static readonly SineEasing Instance = new();
public double ApplyEasing(double time)
{
// Math.Sin takes radians, so convert 450 degrees to radians
const double radian = 360.0 / Math.Tau;
const double totalRadians = 450.0 / radian;
// scale the 0-1 time to 0-450 degrees (in radians)
return Math.Sin(time * totalRadians);
}
}
You can then pass an instance of this class to most easing functions:
box.MoveToX(100f, 1500f, SineEasing.Instance)
You can view this example in the FrameworkExamples project.
Last verified against v2021.728.0
Container Vs CompositeDrawable
Both Container and CompositeDrawable can be inherited from to contain a collection of Drawables, but they have different uses.
CompositeDrawable
abstract class CompositeDrawable : Drawable
A CompositeDrawable is an abstract class that provides functionality to group one or more internal child Drawables into a single parent Drawable. Important to remember is that CompositeDrawable does not expose public methods to dynamically add internal children, this should happen inside a subclass of CompositeDrawable.
Some usages of CompositeDrawable by the framework are:
DelayedLoadWrapper: A wrapping drawable that waits for a specified amount of time after becoming visible before loading its child. Used as children forScrollContainers to not draw every child at once.ModelBackedDrawable<T>: An abstract class which holds a model and recreates its child drawable if said model changes.GridContainer: Contains multipleDrawables which are laid out in a pre-determined grid. AContainercannot be used here because children need to be assigned to a specific slot (Containercontains a single, dynamic collection).
Container
class Container<T> :
CompositeDrawable,
IContainerEnumerable<T>,
IContainerCollection<T>,
ICollection<T>,
IReadOnlyList<T>
where T : Drawable
A Container is a basic implementation of CompositeDrawable which holds a collection of children, accessed through the IReadOnlyList<T> Children property or through methods such as Add, AddRange, Remove, Clear, etc. The class can be inherited from to provide customized functionality, such as automatic positioning and sizing of children.
Container contains a virtual property Content, which is a reference to the container whose InternalChildren will be used for Children. By default, this is a reference to itself meaning it serves like a CompositeDrawable with Add/Remove methods, but overriding this allows for adding a set of Drawables to the underlying CompositeDrawable.InternalChildren that won't show up in Children, such as scroll bars or backgrounds. This is how the framework creates containers such as ScrollContainer which draws additional elements not explicitly added to Children.
As a simple container
A Container can be used to simply group a set of Drawables. Transformations applied to it will be passed down to its children.
[BackgroundDependencyLoader]
private void Load()
{
Drawable drawable;
Add(drawable = new Checkerboard(3)
{
Size = new Vector2(200),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddSliderStep("Scale X", -2f, 2f, 1f, val => drawable.Scale = new Vector2(val, drawable.Scale.Y));
AddSliderStep("Scale Y", -2f, 2f, 1f, val => drawable.Scale = new Vector2(drawable.Scale.X, val));
AddSliderStep("Shear X", -2f, 2f, 0f, val => drawable.Shear = new Vector2(val, drawable.Shear.Y));
AddSliderStep("Shear Y", -2f, 2f, 0f, val => drawable.Shear = new Vector2(drawable.Shear.X, val));
AddSliderStep("Rotate", -360f, 360f, 0f, val => drawable.Rotation = val);
}
Overriding Content
A basic example of overriding the Content property to add extra Drawawbles to a container follows:
/// <summary>
/// A Container with a blue background.
/// </summary>
public class BlueBackgroundContainer : Container
{
private readonly Container _contentContainer;
protected override Container<Drawable> Content => _contentContainer;
public BlueBackgroundContainer()
{
_contentContainer = new Container {AutoSizeAxes = Axes.Both};
}
[BackgroundDependencyLoader]
private void Load()
{
InternalChildren = new Drawable[]
{
_contentContainer,
// a blue background, filling its parent and drawn behind everything else
new Box
{
Colour = Color4.RoyalBlue,
RelativeSizeAxes = Axes.Both,
Depth = 1,
},
};
}
}
Specialized Layout
See Custom Layouts.
Custom Layouts
Some osu!framework containers, such as FillFlowContainer, perform automatic layout of their children, overriding their position. This chapter describes how this works and how to do it yourself.
FillFlowContainer derives from FlowContainer. While the former provides a concrete container you can use, the latter is an abstract class that can be used to make your own custom layouts. However, this is not required.
FillFlowContainer
As an example, FillFlowContainer allows laying out drawables next to each other, similar to a FlowLayoutPanel in WPF or how inline-block and block work in CSS. The direction can be configured to be "full", which fills horizontally but wraps, or horizontal/vertical without wrapping. It also offers other features such as spacing between elements and those it inherits from FlowContainer, such as easing on position after new layouts are calculated.
FlowContainer
FlowContainer<T> is an abstract class that provides functionality to dynamically calculate the positions of your children.
To implement it, the following are required:
IEnumerable<Vector2> ComputeLayoutPositions(): This method should return a list of positions for allFlowingChildren, in order.
You can override the following:
IEnumerable<Drawable> FlowingChildren { get; }: Returns which children to draw, in what order. By default, it takes all live and present children, ordered by their layout position.void InvalidateLayout()
The order of your children can be changed with the void SetLayoutPosition(Drawable, float) method. This will in turn decide the order of the drawables from the default FlowingChildren implementation. To insert an element and immediately set its order, use the void Insert(int, T) method.
Creating your own custom layout class from scratch
Container
A Container provides several methods that may come in useful when overridden:
void UpdateAfterChildren(): This method fromCompositeDrawablegets called every frame after thevoid Drawable.Update()method is executed for allInternalChildren, and before the auto-size calculations are performed. This is a good point to calculate the new layout after an invalidation.AddInternal/RemoveInternal/ClearInternal: Allows for invalidating the layout when new children are added.bool UpdateChildrenLife(): Called to update the life status ofInternalChildrenbased onDrawable.ShouldBeAlive. Can be used to invalidate the layout if base call returnstrue.void AddLayout(LayoutMember): Registers aLayoutMember(or in practice,LayoutValue) that will be invalidated when the correctInvalidationis triggered.
LayoutValue / LayoutMember
LayoutMember is an abstract class that represents part of the layout of a Drawable. It is used to track the "invalidation" of certain properties to determine when parts of the layout must be recomputed. Its public properties are Invalidation, IsValid, Conditions and Source.
Invalidation contains the flags the LayoutMember listens for. For example, if a drawable's DrawSize is changed, it will invalidate LayoutMembers with Invalidation.DrawSize.
TODO: What is Source used for?
The LayoutValue class seems to be an implementation of LayoutMember. LayoutValue<T> can contain a value.
Layout Calculation
- Create 1 or more
LayoutValueobjects with the Invalidation values you want and add useAddLayoutin your constructor. - Invalidate the
LayoutValues when required, e.g. during Add or AddInternal, property changes, etc. - In
void UpdateAfterChildren(), check if your layout is valid and if not, compute your new layout and validate it.
Last verified against v2021.611.0.
Setting up a visual test project
This page describes how to set up a visual test project. This allows you to easily visually inspect scenes and run automated tests against them. Because osu-framework tests support dynamic recompilation (you can edit your code when the test browser is running), this is the recommended way to develop UI components.
Related osu-framework docs:
Setting up the test browser
If you created your project from an osu-framework template, you likely already have a test project and can skip this step.
Create a new C# project targeting .NET Core 3.0 or higher, set the output type to "Windows Application" (<OutputType>WinExe</OutputType> in your csproj file) and add a NuGet reference to ppy.osu.Framework.
Next, create a new class that inherits from osu.Framework.Game. Create a BackgroundDependencyLoader method and add a TestBrowser to your draw hierarchy, passing in the namespace which all your tests start with. For example:
namespace MyGame.Tests
{
public class MyGameTestBrowser : Game
{
[BackgroundDependencyLoader]
private void Load()
{
Child = new TestBrowser(nameof(MyGame));
}
}
}
Often, a DrawSizePreservingFillContainer is used to automatically scale the TestBrowser, so it looks the same on all resolutions. A SafeAreaContainer can also be used, so mobile devices with display notches don't cover anything important:
namespace MyGame.Tests
{
public class MyGameTestBrowser : Game
{
[BackgroundDependencyLoader]
private void Load()
{
Child = new SafeAreaContainer
{
RelativeSizeAxes = Axes.Both,
Child = new DrawSizePreservingFillContainer
{
Child = new TestBrowser(nameof(MyGame)),
},
};
}
}
}
Finally, make sure to launch the game in a GameHost.
namespace MyGame.Tests
{
internal static class Program
{
private static void Main(string[] args)
{
using var host = Host.GetSuitableHost("my-game-tests");
host.Run(new MyGameTestBrowser());
}
}
}
If you launch your test project now, it should look like this:

Creating visual tests
To create a visual test, simple create a class that inherits from osu.Framework.Testing.TestScene. This is a container, so you can create a BackgroundDependencyLoader method and add children to the draw hierarchy.
You can also add "steps" to the test scene. Steps are actions that interact with the test scene, usually to set up different test configurations or to check if certain conditions are true.
This is an example of a test which uses different kinds of steps:
[BackgroundDependencyLoader]
private void Load()
{
// Create a drawable and add it to the draw hierarchy
var box = new Box
{
Size = new Vector2(200),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
Add(box);
// AddStep creates a step that just executes some code. It only fails if an exception is thrown.
AddStep("Change color to red", () => box.Colour = Color4.Tomato);
AddStep("Change color to blue", () => box.Colour = Color4.RoyalBlue);
// AddAssert adds a step that checks for a condition, and fails if it is false.
AddAssert("Is box blue?", () => box.Colour == Color4.RoyalBlue);
// AddRepeatStep runs some code multiple times
AddRepeatStep("Add 15° to rotation", () => box.Rotation += 15f, 6);
AddAssert("Is box rotation 90°?", () => Precision.AlmostEquals(box.Rotation, 90f));
// AddWaitStep simply waits before executing the next step
// The waitCount parameter is the amount of repeats, so this is equivalent to a repeat step with no action
AddWaitStep("Wait 5 steps", 5);
// AddUntilStep waits until a condition is true, or fails when a timeout has passed
AddStep("Rotate to 360° over 3s", () => box.RotateTo(360f, 3_000.0));
AddUntilStep("Box has rotated to 360°", () => Precision.AlmostBigger(box.Rotation, 360f));
// AddToggleStep adds an action with a boolean parameter
AddToggleStep("Shear box", b => box.Shear = b ? new Vector2(0.5f, 0f) : Vector2.Zero);
// AddSliderStep adds an action with a slider that the user can change
AddSliderStep("Change scale", 0.5f, 3f, 1f, val => box.Scale = new Vector2(val));
}
Customizing the TestScene
The osu! project likes to create its own TestScene classes. This allows you to set up dependency injection, add resources, create specialized test runners or provide common functionality. An example can be found here from the osu! repository at 2021/07/05.
Last verified against v2021.707.0.
Old notes
This file contains some old notes I made in the past. It is largely unstructured.
TODO: could explain stuff using test runner
problems with framework
what is Add vs AddInternal?- How do Clocks work?
- How to make buttons that size to fit content? Optionally size to fit container in X?
- Scroll container, how to stop contents from disappearing when scrolling up?
Notes:
- Button needs to be given an Action
ideas for roslyn analyzer:
- error when BDL is not private
- error if using bad AutoSize- or RelativeSizeAxes
- Error when adding object twice to children?
Framework info
Drawables
TODO (drawable, compositedrawable, etc)
Containers
Namespace: osu.Framework.Graphics.Containers
List of containers (excluding Markdown):
- CompositeDrawable
- Container
- BufferedContainer
- CircularContainer
- ClickableContainer
- DrawSizePreservingFillContainer
- FlowContainer
- FillFlowContainer
- SearchContainer
- TextFlowContainer
- CustomizableTextContainer
- FillFlowContainer
- SafeAreaContainer
- SafeAreaDefiningContainer
- ScrollContainer
- BasicScrollContainer
- TabbableContainer
- VisibilityContainer
- OverlayContainer
- FocusedOverlayContainer
- OverlayContainer
- DelayedLoadWrapper
- DelayedLoadUnloadWrapper
- DrawableAudioWrapper
- AudioContainer
- DrawableSample
- DrawableTrack
- GridContainer
- LifetimeManagementContainer
- ModelBackedDrawable
- RearrangeableListContainer
- RearrangeableListItem (abstract)
- TableContainer
- Container
Up to date as of 25/6/2021.
Markdown
TODO
Screens
A Screen is a CompositeDrawable that implements IScreen and can be used with a ScreenStack.
A Screen can be thought of as a menu in a game: you start with your first screen (eg. the main menu) and when clicking a button you push a new menu on the stack (eg. the options or gameplay screen). A screen can be exited again by popping it off the stack, which will return you to the previous screen. The osu-framework wiki describes this as sheets of papers.
IScreen has some extention methods such as Push(newScreen) and Exit() to easily allow pushing and popping screens.
A base class that inherits from Screen can be made to automatically provide animations when transitioning or to place a permanent back button on the screen.
Input
TODO
Implementing new input methods in your game
Basic required classes:
- A custom
InputHandlerthat generates inputs. - An input class (implements
IInput, can also simply extend fromButtonInput<T>). Denotes a single input, can be applied to anInputState. This could be a simple collection of all current input states, according to the device. - An input state class, to be put in
InputState. Contains information about the entire state of the input device (ie. all individual buttons). - Input state change events, represents the change of an
InputState(extendsInputStateChangeEvent) - UI-level events (extend
UIEvent), gets created in anInputManager. Separate events for different actions like KeyUp, KeyDown, MouseClick, MouseDoubleClick, etc.
Adding from within a game:
- Override
InputStateto add your own state. - Override
UserInputManagerSeeUserInputManagerfor the original implementations.- Override
CreateInitialStatecreates your custom state. - Override
HandleInputStateChangechecks if an event is your custom one (and callsPropagateBlockableEvent) or passes down to the base method. - Override
InputHandlersto add your own input handler.
- Override
- In your
Gameclass, overrideCreateUserInputManagerto return your customUserInputManager.
Resource stores
TODO
Audio
Namespace: osu.Framework.Audio
TODO
AudioManager, AudioManager.Tracks and AudioManager.Samples are added to DI. Tracks and Samples can be retrieved through that ITrackStore and ISampleStore respectively. Tracks and Samples can be stored in the Resources/Tracks and Resources/Samples folders to be automatically accessible.
Every audio component inherits from AudioComponent which provides HasCompleted, IsAlive, IsLoaded.
AdjustableAudioComponent extends AudioComponent and implements IAggregateAudioAdjustment and IAdjustableAudioComponent. Volume, Balance and Frequency can be changed.
TODO: AudioCollectionManager "allows managing multiple AdjustableAudioComponents". Used to keep BASS device in sync?
AudioManager extends AudioCollectionManager. Has ITrackStore Tracks, ISampleStore Samples, BindableDouble VolumeSample, BindableDouble VolumeTrack
IAdjustableResourceStore<T> : IResourceStore<T>, IAdjustableAudioComponent
Sample
Namespace: osu.Framework.Audio.Samples
TODO
Sample extends AudioComponent and is abstract. Its constructor allows passing playback concurrency (polyphony). TODO how to create?
ISampleChannel has Play(bool restart = true), Stop(), bool Playing, bool Played
SampleChannel extends AdjustableAudioComponent and implements ISampleChannel. Is abstract; actual implementation is SampleChannelBass. They expose Play(bool restart = true), Stop(), bool Playing, bool Played, bool IsAlive.
SampleStore extends AudioCollectionManager and ISampleStore. It has a SampleChannel Get(string name) method and an async equivalent.
Track
Namespace: osu.Framework.Audio.Tracks
TODO
ITrack has properties Looping, RestartPoint (for loop or Restart), CurrentTime, Length, IsRunning and methods Reset(), Restart(), ResetSpeedAdjustments(), bool Seek(double seed), Start(), Stop().
Track, implementation is TrackBass
ISampleStore, for some reason in .Audio.Tracks.
ITrackStore
Clocks
Namespace: osu.Framework.Timing.
Clocks are usually used to control the timing of Transitions on Drawables or of audio.
Interfaces
IClock: Read-only clock info. Provides CurrentTime, Rate and IsRunning getters. Other interfaces implement this too.IAdjustableClock: Read/write clock. Can start, stop, reset, seek and its rate can be changedIFrameBasedClock: Has a methodProcessFramewhich will update the clock. ElapsedFrameTime, FramesPerSecond and TimeInfo have stats.ISourceChangeableClock: Allows changing the underlying IClock source. Useful for building on other IClocks
Classes
-
ManualClock: The most basic implementation ofIClockwith all properties writable. Note that this only implementsIClock, notIAdjustableClock. You probably wantStopwatchClockinstead. -
StopwatchClock: A basic clock. Built on theSystem.Diagnostics.Stopwatchclass, implementsIAdjustableClock. -
OffsetClock: A clock that extends another clock, but adds an offset to its CurrentTime. ImplementsIClock. -
FramedClock: An implementation ofIFrameBasedClockwhich extends another clock. It implementsIFrameBasedClockandISourceChangeableClock.FramedOffsetClock: An extension ofFramedClockwhich adds an offset.ThrottledFrameClock: An extension ofFramedClockwhich has a propertyMaximumUpdateHzto throttle the rate at whichProcessFrameis called. It does this by blocking the thread until the next ProcessFrame is allowed to be called. This should probably be used by running ProcessFrame in a loop on another thread. Limiting can be disabled by settingMaximumUpdateHzto be less than or equal to 0.
-
InterpolatingFrameClock: A clock that extends another clock, but "smooths it out". This is can be used on anIFrameBasedClockwith low update rate to make it appear to have a higher update rate. Can also be used on other clocks if they don't update in real-time.DecoupleableInterpolatingFramedClock: An extension ofInterpolatingFrameClockwhich "can keep running even if the underlying source has been stopped or cannot handle the current time range". TODO: what does this mean in practice?
Config
TODO
Dependency injection
TODO
Standard DI items
- TextureStore
- AudioManager
- ITrackStore
- ISampleStore
- ShaderManager
- FontStore
- LocalisationManager
- ISafeArea
- GameHost
- Storage
- FrameworkDebugConfigManager
- FrameworkConfigManager
- InputManager
- YourGame
TODO
This file contains an unsorted, incomplete list of interesting items to document.
Additions:
- FillMode for RelativeSizeAxes=Both
Topics:
- Bindable, AggregateBindable
- Lifetimes, expiration, LoadState?
- configuration, framework configuration, config manager
- keybindings (+change/disable framework keybindings?)
- RigidBody, with full platformer example project?
- localization (with a Z)
- TextureAnimation
- low-level graphics, such as DrawNode
- Drawable:
- Explain Drawable, Transformable, Component?
- Drawable.ShouldBeAlive, liveness in general?
- Effects
- Drawable's Clock?
- Drawable Pooling
- Resources:
- Textures and ResourceStores
- Fonts, including:
- FontStore vs custom ITexturedCharacterGlyph (see FlappyDon game)
- The 4 threads (see
GameThread),Schedule(action), schedulers. Can/Should you make your own gamethread? - Utility classes:
- osu.Framework.Utils.Interpolation
- ThreadSafety.IsXThread and when to use it?
- When/Whether async is allowed
On testing:
- Adding descriptions
- Having multiple tests in 1 test scene (groups of steps, example)
- NUnit integration
Additionally, old notes should be brought to their own section and rewritten.
Rough chapter layout sketches
Introduction
- prerequisites
- alternatives (i.e. wobble)
- basic concepts and terminology
- community extensions (such as osu!framework flexbox)










