-
Notifications
You must be signed in to change notification settings - Fork 422
Breaking changes
Occasionally we will make changes which require consumers of the framework to make amendments to maintain compatibility. Wherever possible, we maintain compatibility via [Obsolete]
attributes, but it is encouraged that you leave obsolete warnings turned on, and deal with them sooner rather than later. Generally we aim to leave obsolete methods around for 3-6 months after obsoletion.
This page serves to give a list of all breaking/major changes.
Previously the pool would return drawables correctly before load, but it would not initialise the pool to correct default size, causing overheads on drawable retrieval (usually on update thread).
Previously, the IPC port used for multiple instances of a single osu!framework app was hardcoded in the IPC host itself, effectively making it so that all osu!framework apps would share the same IPC port, which obviously cannot work.
To allow multiple osu!framework apps to utilise IPC concurrently, BindIPC
has thus been replaced by IPCPort
.
- Setting
IPCPort = null
is equivalent toBindIPC = false
. - Setting
IPCPort
to a non-null value is equivalent toBindIPC = true
and will force the use the specific port provided.
Note that it is advised to use a "user port" (in the range of 1024-49151) as per RFC 6335.
This is provided to better treat input when typed by end users, by allowing the ability to respect their regional number settings.
Existing usages of IParseable.Parse()
should pass CultureInfo.InvariantCulture
to preserve behaviour, including usages of IBindable.Parse()
:
Bindable<int> bindable = new Bindable<int>();
- bindable.Parse(5);
+ bindable.Parse(5, CultureInfo.InvariantCulture);
Draw()
is now protected
to match the signature of DrawOpaqueInternal()
. DrawNode
s which draw others no need to do so via a separate (provided) method.
- public override void Draw(IRenderer renderer)
+ protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);
- other.Draw(renderer);
+ DrawOther(other, renderer);
}
All Dropdown
s may now be searched, which requires them to implement a search bar component. Sample implementation:
public partial class MyDropdownHeader : DropdownHeader
{
protected override DropdownSearchBar CreateSearchBar() => new MyDropdownSearchBar();
public partial class MyDropdownSearchBar : DropdownSearchBar
{
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
protected override TextBox CreateTextBox() => new MyTextBox
{
PlaceholderText = "type to search"
};
}
}
See this PR for more details on a migration path.
Masking-related breaking changes from 2023.822.0 have been reverted
After pushing the SSBO masking changes to users, it turned out that the change was not an unambiguous performance win as originally hoped, and as such has been reverted for now in order to avoid diverting focus further away from more important concerns.
In response, framework consumers need to revert any and all changes incurred by the aforementioned release. It is advised to keep the changes somewhere around, however, as the SSBO concept may be revisited at a later date.
Previously it would fail if a file already existed at the target destination. Overwriting is usually the preferred outcome.
Vertices which want to use the built-in VertexShaderDescriptor.TEXTURE
fragment shader, or the sh_Masking.h
particle, will need to adjust the vertex input slightly:
+ #include "Internal/sh_MaskingInfo.h"
layout(location = 0) in vec2 m_Position;
+ layout(location = 1) in int m_MaskingIndex;
+ layout(location = 5) flat out int v_MaskingIndex;
+ layout(location = 6) out highp vec2 v_ScissorPosition;
void main(void)
{
+ InitMasking(m_MaskingIndex);
// Transform from screen space to masking space.
- highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
+ highp vec4 maskingPos = g_MaskingInfo.ToMaskingSpace * vec4(m_Position, 1.0, 0.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z;
+ // Transform from screen space to scissor space.
+ highp vec4 scissorPos = g_MaskingInfo.ToScissorSpace * vec4(m_Position, 1.0, 0.0);
+ v_ScissorPosition = scissorPos.xy / scissorPos.z;
+ v_MaskingIndex = m_MaskingIndex;
gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0);
}
Likewise, the C# definition must also be updated to write the m_MaskingIndex
input, and be constructed with an IRenderer
:
[StructLayout(LayoutKind.Sequential)]
public struct MyCustomVertex : IEquatable<MyCustomVertex>, IVertex
{
[VertexMember(2, VertexAttribPointerType.Float)]
public Vector2 Position;
+ [VertexMember(1, VertexAttribPointerType.Int)]
+ private readonly int maskingIndex;
+ public MyCustomVertex(IRenderer renderer)
+ {
+ this = default;
+ maskingIndex = renderer.CurrentMaskingIndex;
+ }
public readonly bool Equals(MyCustomVertex other) =>
Position.Equals(other.Position)
+ && maskingIndex == other.maskingIndex;
}
Any usages of VertexShaderDescriptor.TEXTURE_2
as a vertex shader in-combination with a non-masking fragment shader (i.e. one that does not use the built-in sh_Masking.h
particle), should be updated to use VertexShaderDescriptor.TEXTURE_2_NO_MASKING
instead.
The way masking applies scissor has changed such that this no longer has any use.
-
ScreenSpaceAABB
renamed toScreenSpaceScissorArea
. -
MaskingRect
renamed toMaskingArea
-
ToScissorSpace
added. IfScreenSpaceScissorArea
truly represents a screen-space area, set this toMatrix3.Identity
, otherwise set it to convert vertex coordinate inputs to the appropriate coordinate space ofScreenSpaceScissorArea
.
The default constructor for this type has been obsoleted and will now generate a compiler error.
As we continue to add more source generators, we've seen increases in compile-time overheads, with local testing showing over 2x compile times with source generators turned on.
osu!framework source generators are made to optimise builds at runtime (mostly by removing reflection overhead). As such, it doesn't make sense to run these for debug releases as they are basically custom release-targeting optimisations.
This should not result in a noticeable change in runtime performance during debug, but will reduce compilation times by over 50% in most cases. This is valuable during debug as the most common case is frequently building / hot reloading for quick iteration.
The new way to retrieve a Clipboard
instance is to resolve it via dependency injection directly. This does not require resolving the GameHost
anymore:
[BackgroundDependencyLoader]
-private void load(GameHost host)
+private void load(Clipboard clipboard)
{
- host?.Clipboard.SetText("example");
+ clipboard.SetText("example");
}
If you were using this, refactor your code to use LocalisableString GenerateItemText(T value)
instead.
Easiest way is to add LocalisableString? CustomDropdownText
to your T
, or wrap T
in a class that has one.
Then simply change GenerateItemText()
to use that:
public partial class MyDropdown : Dropdown<T>
{
protected override LocalisableString GenerateItemText(T value)
=> value.CustomDropdownText ?? base.GenerateItemText(value);
}
Previously, subclasses of FocusedOverlayContainer
were not forced to implement Pop{In,Out}
themselves, despite actually needing to do so for the overlay to actually play transitions out correctly. This has now been changed and inheritors of the class must always implement Pop{In,Out}
.
In practice this should not break any reasonable consumers of the class, except for the need to remove any base.Pop{In,Out}()
calls in classes that directly inherit FocusedOverlayContainer
and call base in Pop{In,Out}()
.
In efforts to migrate the framework to SDL on iOS, the project structure required major changes. For consumers, you're required to remove the AppDelegate
class attached in the game project as GameAppDelegate
no longer exists, and change the Application.Main
method to call GameApplication.Main
as such:
- public static void Main(string[] args) => UIApplication.Main(args, null, typeof(AppDelegate));
+ public static void Main(string[] args) => GameApplication.Main(new MyGameIOS());
This is partially done to force consumers into binding uniform blocks in the newly defined TexturedShaderDrawNode.BindUniformResources
method, such that uniform blocks are always bound whenever the corresponding shader is used for drawing in the framework.
For binding/unbinding shaders, use TexturedShaderDrawNode.BindTextureShader
/TexturedShaderDrawNode.UnbindTextureShader
instead.
Shaders now follow "Vulkan GLSL", which is mostly documented in the spec: GL_KHR_vulkan_glsl
. The primary differences are documented below:
old | new |
---|---|
attribute lowp int In_Value;
varying lowp int Out_Value; |
layout(location = 0) in lowp int InValue;
layout(location = 0) out lowp int OutValue; |
The location increases for each additional member in the respective in
/out
lists.
old | new |
---|---|
varying lowp int In_Value; |
layout(location = 0) in lowp int InValue; |
The location increases for each additional member, and must match the location of the respective member in the vertex shader.
Fragment shaders are required to define an output variable.
old | new |
---|---|
void main(void)
{
gl_FragColor = vec4(0.0);
} |
layout(location = 0) out vec4 colour;
void main(void)
{
colour = vec4(0.0);
} |
Free-floating "global" uniforms are not supported. They must be placed in "uniform blocks".
old | new |
---|---|
uniform lowp int Uniform_Value; |
layout(std140, set = 0, binding = 0) uniform MyUniforms
{
lowp int Uniform_Value;
}; |
Notes:
-
std140
should always be used. - The
set
increases for each unique uniform block. -
binding
is a special required value which indicates where in the program the block should be bound. osu!framework manages this for you and should be 0 for uniform blocks. - Both shader stages can use the same uniform block from the same set, provided that the physical layout matches.
Furthermore, this means that uniform blocks have become a first-class citizen in osu!framework. The code to make use of the above looks like this:
class MyDrawNode : DrawNode
{
private IUniformBuffer<MyBufferData>? bufferData;
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
// Create the uniform buffer.
bufferData ??= renderer.CreateUniformBuffer<MyBufferData>();
// Update the uniform buffer.
bufferData.Data = bufferData.Data with
{
UniformValue = 5
};
// Bind the buffer to the shader.
shader.BindUniformBuffer("MyUniforms", bufferData);
shader.Bind();
// Draw...
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
// Ensure that the buffer is disposed.
bufferData?.Dispose();
}
}
// Pack = 1 is required.
[StructLayout(LayoutKind.Sequential, Pack = 1)]
// Record structs automatically implement IEquatable<>
public record struct MyBufferData
{
// Use types provided in the osu.Framework.Graphics.Shaders.Types namespace.
public UniformInt UniformValue;
// The struct must be a multiple of 16 bytes.
private readonly UniformPadding12 pad2;
}
Runtime validation is present to notify of incorrect alignment/packing.
The combined sampler of GLSL is not supported.
old | new |
---|---|
uniform lowp sampler2D Sampler; |
layout(set = 0, binding = 0) uniform lowp texture2D Texture;
layout(set = 0, binding = 1) uniform lowp sampler Sampler; |
Notes:
-
std140
cannot be applied here unlike for uniform blocks. - The
set
increases along with all other uniform blocks/textures. - The texture and sampler are bound to different points. The texture being bound to 0 and sampler to 1 will be standard for osu!framework going forward.
old | new |
---|---|
vec4 col = texture2D(my_Sampler, ...); |
vec4 col = texture(sampler2D(my_Texture, my_Sampler), ...); |
IHasFilterableChildren
has been removed; use IFilterable
instead (https://github.com/ppy/osu-framework/pull/5530)
Rather than rely on IHasFilterableChildren
to descend down complex hierarchies for filtering purposes, IContainerEnumerable<T>.Children
is used, which results in less boilerplate as you don't need to remember to implement it whenever needed.
IConditionalFilterable
is introduced (https://github.com/ppy/osu-framework/pull/5530)
By implementing IConditionalFilterable
, you get one extra bindable to play with. If the value of said bindable is true
, then the item is included in textual search as usual. If it is false
, however, it - and its entire subtree - is excluded from further search on the premise that the item does not meet external criteria. In practical terms, you would set it to false
when you want to hide some items inside a SearchContainer
because they're unavailable due to other settings.
Classes used in dependency injection need to implement the IDependencyInjectionCandidate
interface (https://github.com/ppy/osu-framework/pull/5548)
This does not affect Drawable
subclasses.
Add source generator for dependency injection (https://github.com/ppy/osu-framework/pull/5541)
This one's been brewing for a long time while I tried to figure out the best way to do things. It provides a structural foundation on top of which we can build other source generators.
For source generators to work at all for us, we unfortunately need to make every Drawable
class partial
. This can be done as a graceful upgrade – previous reflection pathways are still supported so no immediate changes should be required to your project to compile.
I've created a pretty "basic" analyser for warning against non-partial
classes when required. It attempts to discover:
- Types used as the argument to calls of
DependencyContainer.Inject()
. - Types used as type arguments of
CachedModelDependencyContainer<T>
. - Types implementing
ITransformable
,IDrawable
orISourceGeneratedDependencyActivator
.
This covers all current cases while being as least invasive as possible (i.e. not analysing every member of every class ever).
A code fix has been provided for the diagnostic, however if you're planning to codefix the whole solution I suggest the running the following command until it outputs 0 changes:
dotnet format analyzers osu.Desktop.slnf --verbosity d
The code fix seems to not fix all issues on the first attempt. This is potentially caused by my implementation, however I've tried everything that I could find and nothing's worked. I'll look into fixing this as a future effort, however for the most part it's a one-time thing.
The analyser and code fixer don't have tests at the moment. These are also planned in a future effort and will be structured similarly to the source generator tests and the osu-localisation-analyser
project.
- Member accessor validation is not supported yet. I think these will probably be added to an analyser in a future effort, but I've disabled the relevant tests in https://github.com/smoogipoo/osu-framework/commit/f3324205ded5ed1c05fa3a92a8fc4b5c26ec7cb4 for now.
- Build time has increased. This can probably be improved further, however I don't want to drag on this PR much longer.
- Going to definitions of classes is a +1 process now, with every single definition now giving you two locations.
- I eventually want to update the source generator to make use of
IIncrementalGenerator
, but there's a bit more reading to do for that. - The class is generated via AST rather than raw code. This is a personal/stylistic choice because I originally implemented it using raw code and couldn't bear it.
- The SG code quality is not quite where I want it to be at the moment - it's lacking documentation (compared to something like
osu-localisation-analyser
) and the tests are lacking structure. This will be improved in a future effort.
I have not tested VSCode compatibility, but I have no reason to believe it won't work, unlike osu-localisation-analyser
where there's differences in how IDEs add files to workspaces from analysers/codefixes.
Check the pull request thread for CPU and memory profiling, but the short of this is that framework allocations required for dependency injection are down 50% or more; overall allocations at runtime are down 50% (by object count) and also substantially by memory; CPU overhead for DI activation (ie. creating a new Drawable
) is down magnitudes on first usage, and also marginally on subsequent usages.
Put simply, it's a win all-round.
Bindable<T>.CopyTo
should be implemented for any custom bindables (https://github.com/ppy/osu-framework/pull/5531)
To improve the performance of creating clones of bindables (ie. via GetUnboundCopy()
), the copy portion of BindTo()
has been split out into its own method. If you have any custom bindable types which were copying values across within a BindTo()
override, please move the copy operation to CopyTo()
instead. Pay special attention to the direction of assignment, which will reverse from what it was in BindTo
.
A fix should look something like this:
diff --git a/osu.Framework/Bindables/BindableNumber.cs b/osu.Framework/Bindables/BindableNumber.cs
index 6d605667f..430da79d0 100644
--- a/osu.Framework/Bindables/BindableNumber.cs
+++ b/osu.Framework/Bindables/BindableNumber.cs
@@ -199,12 +199,12 @@ protected void TriggerPrecisionChange(BindableNumber<T> source = null, bool prop
PrecisionChanged?.Invoke(precision);
}
- public override void BindTo(Bindable<T> them)
+ public override void CopyTo(Bindable<T> them)
{
if (them is BindableNumber<T> other)
- Precision = other.Precision;
+ other.Precision = Precision;
- base.BindTo(them);
+ base.CopyTo(them);
}
public override void UnbindEvents()
The new naming should better fit future usages outside of depth and stencil tests.
CompositeDrawable.RemoveInternal
, IContainer.Remove
, IContainer.RemoveRange
and IContainer.RemoveAll
now require a bool
parameter
A common mistake we've seen made osu!-side is where drawables are Remove
d from the hierarchy with the intention of never using them again. In such cases, disposal is not guaranteed – nor is unbinding of Bindable
fields/properties – which can lead to event and object leakage.
To prevent this from happening, a new disposeImmediately
parameter has been added.
Generally this should be set to true
unless you intend to reuse the removed drawable (ie. by adding back to the hierarchy at a different location).
In places where the DepthInfo
struct is used with a custom depth function, the following changes are required.
- new DepthInfo(function: DepthFunction.Never)
+ new DepthInfo(function: DepthStencilFunction.Never)
- new DepthInfo(function: DepthFunction.Less)
+ new DepthInfo(function: DepthStencilFunction.LessThan)
- new DepthInfo(function: DepthFunction.Lequal)
+ new DepthInfo(function: DepthStencilFunction.LessThanOrEqual)
- new DepthInfo(function: DepthFunction.Equal)
+ new DepthInfo(function: DepthStencilFunction.Equal)
- new DepthInfo(function: DepthFunction.Gequal)
+ new DepthInfo(function: DepthStencilFunction.GreaterThanOrEqual)
- new DepthInfo(function: DepthFunction.Greater)
+ new DepthInfo(function: DepthStencilFunction.GreaterThan)
- new DepthInfo(function: DepthFunction.Notequal)
+ new DepthInfo(function: DepthStencilFunction.NotEqual)
- new DepthInfo(function: DepthFunction.Always)
+ new DepthInfo(function: DepthStencilFunction.Always)
In order to allow the framework to interoperate with different rendering backends, all rendering functions are now performed through a new IRenderer
interface and parameter.
- DrawNode.Draw(Action<TexturedVertex2D> vertexAction);
+ DrawNode.Draw(IRenderer renderer);
- DrawNode.DrawOpaqueInterior(Action<TexturedVertex2D> vertexAction);
+ DrawNode.DrawOpaqueInterior(IRenderer renderer);
In places where the vertexAction
parameter was previously used (e.g. to draw textures), null
can be given as a parameter instead.
The static GLWrapper
class has been removed, and all drawing functions should be performed via the new DrawNode
parameter instead. There is a 1-1 API correlation between the classes.
public class MyDrawNode : DrawNode
{
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
- GLWrapper.PushDepthInfo(...);
+ renderer.PushDepthInfo(...);
- GLWrapper.SetBlend(...);
+ renderer.SetBlend(...);
- GLWrapper.PopDepthInfo();
+ renderer.PopDepthInfo();
}
}
For cases where it's used as a fallback texture, it can be retrieved by resolving an IRenderer
into the Drawable
class and accessing IRenderer.WhitePixel
.
public class MyDrawable : Drawable
{
+ [BackgroundDependencyLoader]
+ private void load(IRenderer renderer)
+ {
+ texture ??= renderer.WhitePixel;
+ }
private Texture texture;
public Texture Texture
{
- get => texture ?? Texture.WhitePixel;
+ get => texture;
set => texture = value;
}
}
Appearing alongside IRenderer
is the new RendererExtensions
class providing helper methods for common drawing procedures.
public class MyDrawNode : DrawNode
{
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
- DrawQuad(renderer.WhitePixel, ...);
+ renderer.DrawQuad(renderer.WhitePixel, ...);
}
}
All of the following methods have been moved to this class:
DrawTriangle()
DrawQuad()
DrawClipped()
DrawFrameBuffer()
Resolve an IRenderer
into the Drawable
class and use IRenderer.CreateTexture()
method to create textures.
public class MyDrawable : Drawable
{
- private readonly Texture texture;
- public MyDrawable()
- {
- texture = new Texture(100, 100);
- texture.SetData(...);
- }
+ private Texture texture;
+ [BackgroundDependencyLoader]
+ private void load(IRenderer renderer)
+ {
+ texture = renderer.CreateTexture(100, 100);
+ texture.SetData(...);
+ }
}
DummyRenderer
may be used for cases where textures need to be created and neither an IRenderer
nor GameHost
is accessible:
[TestFixture]
public class MyTestFixture
{
public void CreateATexture()
{
- Texture texture = new Texture(1, 1);
+ Texture texture = new DummyRenderer().CreateTexture(1, 1);
}
}
Properties such as TextureGL.BypassTextureUploadQueueing
have been moved to Texture
itself, and Texture
can be used for all rendering procedures. When rendering, textures are now bound to different sampling units via an integer value.
public class MyDrawable : Drawable
{
private Texture texture;
[BackgroundDependencyLoader]
private void load(IRenderer renderer)
{
texture = renderer.CreateTexture(100, 100);
- texture.TextureGL.BypassUploadQueueing = true;
+ texture.BypassUploadQueueing = true;
texture.SetData(...);
}
}
public class MyDrawNode : DrawNode
{
private Texture texture;
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
- texture.TextureGL.Bind();
- texture.TextureGL.Bind(TextureUnit.Texture1);
+ texture.Bind();
+ texture.Bind(1);
// Note: The texture binding above isn't required in either case for the call below.
- DrawQuad(texture.TextureGL, ...);
+ renderer.DrawQuad(texture, ...);
}
}
Create vertex batches via the IRenderer
and store as an IVertexBatch<T>
instead:
public class MyDrawNode : DrawNode
{
- private readonly QuadBatch<TexturedVertex2D> quadBatch = new QuadBatch<TexturedVertex2D>(1, 1);
- private readonly LinearBatch<TexturedVertex2D> linearBatch = new LinearBatch<TexturedVertex2D>(1, 1, PrimitiveType.Triangles);
+ private IVertexBatch<TexturedVertex2D> quadBatch;
+ private IVertexBatch<TexturedVertex2D> linearBatch;
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
+ quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(1, 1);
+ linearBatch ??= renderer.CreateLinearBatch<TexturedVertex2D>(1, 1, PrimitiveTopology.Triangles);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
batch?.Dispose();
}
}
Beware that when creating linear batches, the type parameter has changed from PrimitiveType
to PrimitiveTopology
!
Create frame buffers via the IRenderer
and store as an IFrameBuffer
.
public class MyDrawNode : DrawNode
{
- private readonly FrameBuffer myFrameBuffer = new FrameBuffer(new[] { RenderbufferInternalFormat.DepthComponent16 }, All.Nearest);
+ private IFrameBuffer myFrameBuffer;
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
+ myFrameBuffer ??= renderer.CreateFrameBuffer(new[] { RenderBufferFormat.D16 }, TextureFilteringMode.Nearest);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
myFrameBuffer?.Dispose();
}
}
Beware that the render buffer type parameter has changed from RenderbufferInternalFormat
to RenderBufferFormat
!
Shaders are still created via the ShaderManager
, but are now returned as IShader
s.
public class MyDrawable : Drawable
{
- private Shader shader;
+ private IShader shader;
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
shader = shaders.Load("a", "b");
}
}
One common use case is to provide a new texture store from inside a derived Game
, for which the following change is required:
public class TestGame : osu.Framework.Game
{
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
private void load()
{
- var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")), All.Nearest);
+ var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")), TextureFilteringMode.Nearest);
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore()));
dependencies.Cache(largeStore);
}
}
Beware that the filter mode parameter has changed from All
to TextureFilteringMode
!
public class MyDrawNode : TexturedShaderDrawNode
{
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
- Shader.Bind();
+ var shader = GetAppropriateShader(renderer);
+ shader.Bind();
// ...
- Shader.Unbind();
+ shader.Unbind();
}
- protected override bool RequiresRoundedShader => ...;
+ protected override bool RequiresRoundedShader(IRenderer renderer) => ...;
}
They are now placed under the osu.Framework.Graphics.Rendering.Vertices
namespace
WrapMode -> osu.Framework.Graphics.Textures.WrapMode
Opacity -> osu.Framework.Graphics.Textures.Opacity
ClearInfo -> osu.Framework.Graphics.Rendering.ClearInfo
MaskingInfo -> osu.Framework.Graphics.Rendering.MaskingInfo
DepthInfo -> osu.Framework.Graphics.Rendering.DepthInfo
In an effort to make the texture API easier to comprehend, the AddStore
/RemoveStore
methods have been split into two pairs:
-
AddTextureSource
/RemoveTextureSource
, for adding/removing texture data lookup sources (i.e.TextureLoaderStore
s) -
AddStore
/RemoveStore
, for adding/removingTextureStore
s.
Any existing usages of AddStore
for adding lookup sources must be changed to use AddTextureSource
instead.
Games using osu!framework can generally run at very high frame rates when not much is going on.
This can be counter-productive due to the induced allocation and GPU overhead.
- Allocation overhead can lead to excess garbage collection
- GPU overhead can lead to unexpected pipeline blocking (and stutters as a result). Also, in general graphics card manufacturers do not test their hardware at insane frame rates and therefore drivers are not optimised to handle this kind of throughput.
- We only harvest input at 1000hz, so running any higher has zero benefits.
If you think you know better for your specific application (or more correctly need to remove the limit for benchmarking), set GameHost.AllowBenchmarkUnlimitedFrames
to true
.
Supports filtering by either the original text or the localised form according to the currently selected language. Migration:
- public IEnumerable<string> FilterTerms => new string[] { ... };
+ public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { ... };
Previously, Windows, Linux and Android were inverted. Delta is positive when mouse wheel scrolled to the up or left, in non-"natural" scroll mode (ie. the classic way).
To allow adding more data to IScreen
navigation interface methods in the future without further API breakage, their signatures have been changed to include an "event args" structure in the following manner:
- void OnEntering(IScreen last);
+ void OnEntering(ScreenTransitionEvent e);
- void OnExiting(IScreen next);
+ void OnExiting(ScreenExitEvent e);
- void OnResuming(IScreen last);
+ void OnResuming(ScreenTransitionEvent e);
- void OnSuspending(IScreen next);
+ void OnSuspending(ScreenTransitionEvent e);
The last
and next
arguments from the old signatures can now be accessed via e.Last
and e.Next
respectively.
Additionally, ScreenExitEvent
contains a new Destination
member, that allows to specify which screen is the "destination" screen of an exit operation spanning multiple screens.
Over the years we have maintained our own version of hot reload, affectionately named "dynamic compilation". Even after multiple complete rewrites of the system, there are still edge cases where it will unexpectedly fall over due to being too greedy in including what it considers required for the recompile.
The startup cost when running in debug was considerable (around 10-30s initialisation) and the first-compile overhead could also be high (5-60s). In addition, the dependencies required to make it work increased the final assembly size of osu!framework, even for release deploys.
With the introduction of cross-platform support for hot reload in .NET 6 we have made the decision to switch to the natively supported version.
To use this new version:
- From Rider, you'll get a popup that you have to click "Apply Changes" when a change is made to code while running. You can bind a key to
Apply Hot Reload Changes
in Rider (defaults to Alt+F10) to make it quicker to apply changes. - Running
dotnet watch
from CLI on your test project will automatically watch files and recompile when a hange is seen.
New limitations:
- The limitations of hot reload are listed here.
- Notably, adding new
override
methods orclass
es are not supported. You can workaround this for the common scenario of prototyping new components by creating subclasses and adding theoverride
methods before the initial hot reload.
We are interested in hearing feedback on this change, especially troubling cases where the previous behaviour worked better for you. Hope is that the limitations of the new method are outweighed by the leaner assembly, better performance, and (in general) up-front error when a change can't be applied, rather than an error that can be delayed longer than it would take to run a full recompile/restart.
InputManager.ChangeFocus()
will no longer switch focus to drawables that are not alive, not present or do not have a parent
To avoid unusual scenarios concerning ChangeFocus()
, wherein a drawable could potentially request focus and have focus automatically taken away from it every frame, ChangeFocus()
now checks whether the target drawable is alive, present and has a parent before switching focus to it.
This potentially breaks scenarios such as calling ChangeFocus()
in LoadComplete()
on a child drawable with the expectation that the child drawable should receive focus as soon as its ancestor is added to the draw hierarchy. In such scenarios, the suggested fix is to schedule the ChangeFocus()
operation after children so that it is performed only when the child is fully prepared to receive focus.
In general it is recommended to check the return value of ChangeFocus()
to determine as to whether focus was actually changed.
To facilitate gradiented border support, the type of CompositeDrawable.BorderColour
has changed from SRGBColour
to ColourInfo
. While some implicit conversions from SRGBColour
to ColourInfo
exist, some properties of SRGBColour
are not available on ColourInfo
, as the latter does not always represent a single colour, and may require appropriate adjustments.
KeyBindingContainers
are commonly used in UI, where we expect to be able to receive key repeats. Historically this was controlled by the SendRepeats
flag, but now that all events send KeyBindingPressEvent
with a repeat argument, handling this legacy mode of operation was causing more issues that it was worth. The default expectation should be that repeats arrive.
For cases where key repeat may not be wanted (ie. gameplay, where your code is in complete control of user input and doesn't want to receive arbitrary key repeat events), you may opt out of receiving them by creating a subclass of KeyBindingContainer
:
public class NoRepeatKeyBindingContainer : KeyBindingContainer<T>
{
protected override bool HandleRepeats => false;
}
Alternatively, if you have only certain cases which you wish to opt out of, you can early return in your OnPressed
implementation:
public bool OnPressed(KeyBindingPressEvent<MyAction> e)
{
if (e.Repeat)
return false;
if (e.Action == MyAction.Action)
{
...
}
}
// old code:
var bufferedContainer = new BufferedContainer() { CacheDrawnFrameBuffer = true };
// new code:
var bufferedContainer = new BufferedContainer(cachedFrameBuffer: true);
We had no usages of this. If you were using it, nest the effected content in a BufferedContainer
with cachedFrameBuffer
set to true
.
In preparation for adding localisation support to TextFlowContainer
s, the various AddText()
/AddLine()
overloads will no longer return raw SpriteText
s. Instead, an ITextPart
structure will be returned.
Via ITextPart
, the consumer can both access all Drawables
associated with a given piece of text, as well as react to any future changes in representation of the text by subscribing to DrawablePartsRecreated
. In the future, with more localisation changes, this event will be invoked once a LocalisableString
's displayable content changes, which will trigger a recreation of all parts' drawables, upon which any manual adjustments applied to Drawables
can be re-applied again.
For consumers wanting to implement their own ITextPart
s to extend the functionality of TextFlowContainer
, an abstract TextPart
is also provided which implements the typical flow of handling Drawables
and DrawablePartsRecreated
. The only thing that a consumer has to do when inheriting that class is to implement CreateDrawablesFor(TextFlowContainer)
.
To allow for more arguments without changing the signature of the handling methods, and also for consistency with the input flow in general, both interfaces now provide UIEvent
s rather than placing each parameter directly on the methods.
- public bool OnPressed(T action) { }
- public bool OnScroll(T action, float amount, bool isPrecise) { }
- public void OnReleased(T action) { }
+ public bool OnPressed(KeyBindingPressEvent<T> e) { }
+ public bool OnScroll(KeyBindingScrollEvent<T> e) { }
+ public void OnReleased(KeyBindingReleaseEvent<T> e) { }
The following regex replacements can be used to simplify migration:
Find: bool OnPressed\((\w+)\s+(\w+)\)
Replace: bool OnPressed(KeyBindingPressEvent<$1> e)
Find: void OnReleased\((\w+)\s+(\w+)\)
Replace: void OnReleased(KeyBindingReleaseEvent<$1> e)
Find: bool OnScroll\((\w+) (\w+), float \w+, bool \w+\)
Replace: bool OnScroll(KeyBindingScrollEvent<$1> e)
This is not a change to actual playback behaviour - it only affects the return value of the playback position, which now matches the underlying playback behaviour.
Until now, the method of reusing tooltip instances was problematic (two tooltips handling the same data type could not exist). Instance sharing is now based on the constructed tooltip's Type
, rather than the data type.
An example of how you should update your code follows.
Before:
public override bool SetContent(object content)
{
if (!(content is CustomContent custom))
return false;
text.Text = content.ToString(); // whatever you need to do here.
return true;
}
After:
public override void SetContent(object content)
{
text.Text = content.ToString(); // whatever you need to do here.
}
Bindables previously used Activator.CreateInstance()
to implement the GetBoundCopy()
call. In profiling this has turned out to be a bottleneck in some scenarios, so in order to reduce the associated runtime overhead to about half, all implementors of IBindable
now must implement CreateInstance()
.
This also applies to all inheritors of framework-provided bindable types, who must instead override CreateInstance()
from their base types.
For a given leaf bindable type in the inheritance hierarchy, the method should return a new constructed instance of the leaf type. The recommended pattern to use is as follows:
public class BaseBindable : IBindable
{
IBindable IBindable.CreateInstance() => CreateInstance();
protected virtual BaseBindable CreateInstance() => new BaseBindable();
IBindable IBindable.GetBoundCopy() => GetBoundCopy();
public BaseBindable GetBoundCopy()
=> IBindable.GetBoundCopyImplementation(this); // automatically calls CreateInstance()
}
public sealed class CustomBindable : BaseBindable
{
protected override BaseBindable CreateInstance() => new CustomBindable();
}
EnumLocalisationMapper
for enum localisation has been replaced with per-member LocalisableDescription
attributes
As the current way for localising enum
s require a side-class for the mapping, it became a tedious procedure to localise enums and lengthens a lot of files for supporting such.
Therefore a new LocalisableDescription
attribute has been added allowing for localisation in a more performant and single-lined way.
- [LocalisableEnum(typeof(SearchEnumLocalisationMapper))]
public enum Search
{
+ [LocalisableDescription(typeof(Strings), nameof(Strings.SearchHide))]
Hide,
- Show
- }
-
- public class SearchEnumLocalisationMapper : EnumLocalisationMapper<Search>
- {
- public override LocalisableString Map(Search value)
- {
- switch (value)
- {
- case Search.Hide:
- return Strings.SearchHide;
-
- case Search.Show:
- return Strings.SearchShow;
- default:
- throw new ArgumentOutOfRangeException(nameof(value), value, null);
- }
- }
+ [LocalisableDescription(typeof(Strings), nameof(Strings.SearchShow))]
+ Show
}
This however require from all consumers to store the LocalisableString
s in static providing classes, similar to osu!'s Strings
classes.
Benchmarks:
-
EnumLocalisationMapper
:Method Times Mean Error StdDev Median GetLocalisableDescription 1 6.866 us 0.5021 us 1.383 us 6.316 us GetLocalisableDescription 10 93.360 us 4.9223 us 13.883 us 92.744 us GetLocalisableDescription 100 817.775 us 31.0268 us 90.996 us 812.132 us GetLocalisableDescription 1000 10,794.319 us 820.1783 us 2,392.498 us 10,718.182 us -
LocalisableDescription
:Method Times Mean Error StdDev Median GetLocalisableDescription 1 4.631 us 0.1148 us 0.3331 us 4.514 us GetLocalisableDescription 10 54.555 us 1.3511 us 3.9412 us 54.440 us GetLocalisableDescription 100 540.643 us 15.9231 us 45.6864 us 539.138 us GetLocalisableDescription 1000 7,218.708 us 270.7659 us 768.1179 us 7,192.596 us
If PlatformAction.ActionType
was matched, change to a matching of PlatformAction
itself like:
bool OnPressed(PlatformAction action) {
- switch (action.ActionType)
+ switch (action)
{
- case PlatformActionType.Cut:
+ case PlatformAction.Cut:
For PlatformActionMethod.Delete
, and if only the Delete key should be handled (not Backspace etc.), change to PlatformAction.Delete
:
- switch (action.ActionMethod)
+ switch (action)
{
- case PlatformActionMethod.Delete:
+ case PlatformAction.Delete:
IHasTooltip.Text
now takes a LocalisableString
instead of regular string. Users of custom tooltip containers may also have to change checks in the SetContent()
method of their tooltip type to check if content is a LocalisableString
instead of a regular string.
This was only added in the previous release, but has since been replaced with the bindable GameThread.State
, which gives more detail about the current state of the thread.
The result of this method is now more correctly applied to headers via FontSize
rather than Scale
. If you were overriding this method, please multiply your returned values by 20
to maintain sizing compatibility.
We were already using Roboto in the visual tests contexts, but fallback to OpenSans could be seen for tool windows in both framework and consumer projects. In an effort to consolidate this visually, all framework components now use Roboto, the chosen font for osu!framework design logic.
This means that if you were previously relying on the presence of OpenSans and do not want to switch to Roboto, you will need to re-add the font resources in your project. Instructions on doing this can be found here.
As it appears to be often one word, the enum member CounterClockwise
has been renamed to Counterclockwise
.
IParseable
is only implemented on the Bindable<>
and BindableList<>
classes. Code that relied on this functionality should instead cast to IParseable
.
Priority is now decided by the construction order of InputHandler
s in CreateAvailableInputHandlers
.
Previously this method was public
for convenience, but as it implicitly set the Default
value of bindables it touched, could cause unexpected behaviour.
- For initialising defaults, switch to using
SetDefault
fromInitialiseDefaults
- For changing configuration values externally, use
SetValue
instead ofSet
// previously
config.Set(ConfigType.Name, true);
// now
config.SetValue(ConfigType.Name, true);
If you were supporting it via GameHost
initialisation, you may need to remove a parameter. Internally, we still support it in a minimal way for Xamarin platforms.
Until now this font wasn't following the convention we use everywhere else. If you are referencing it directly, please update your strings to point to the new name.
For strings which have romanisable content, RomanisableString
should be used instead.
If you have custom implementations, you will need to update the overridden signatures.
Getting the current value of some strings (ie. on UI Components) will now require an explicit call to .ToString()
.
This may mean that as a framework consumer you need to re-think how you are performing operations between drawables. The easiest way to avoid issue is to use Schedule
to force the target code to be on the correct thread.
Colour
now uniformly affects the entire graph as expected.
To avoid aggregate adjustment implementations from having to implement both Aggregate{Volume,Balance,Frequency,Tempo}
and GetAggregate()
, the latter has been made an extension method returning one of the aforementioned four values for the appropriate property of the adjustment.
Possible compilation failures related to GetAggregate()
should be resolved by adding a using statement for the osu.Framework.Audio
namespace to the files affected.
The events were specifically exposed to provide feedback on user input, but were triggering on programmatic manipulation of the textbox's text. See https://github.com/ppy/osu-framework/pull/3839/files.
This is a fringe use-case, as the samples were never included with the framework, but if you happened to be providing them in your game resources, you will need to update your TextBox
implementation to trigger them again. Check the osu!-side changes for an example of how to do this.
Mutating bindables of models attached to a CachedModelDependencyContainer
can lead to crashes or otherwise unexpected behaviour, and is now disallowed.
Performance logging is now toggled by opening "Global Statistics" overlay via Ctrl+F2. To manually toggle it, you may access GameHost.PerformanceLogging
, but note that a change to this bindable will be overridden by toggling the statistics overlay.
To better cover edge-case scenarios where layout wasn't properly refreshed, the process of layout invalidation has changed.
public class MyDrawable : Drawable
{
- private readonly Cached myLayoutValue = new Cached();
-
- protected override bool OnInvalidate(Invalidation invalidation, Drawable source, bool shallPropagate)
- {
- var result = base.OnInvalidate(invalidation, source, shallPropagate);
-
- if ((invalidation & (Invalidation.DrawSize | Invalidation.Presence)) > 0) /* any invalidation type of your choice */
- result &= !myLayoutValue.Invalidate();
-
- return result;
- }
+ /* can also be a LayoutValue<T> */
+ private readonly LayoutValue myLayoutValue = new LayoutValue(Invalidation.DrawSize | Invalidation.Presence);
+
+ public MyDrawable()
+ {
+ AddLayout(myLayoutValue);
+ }
}
In cases where InvalidateFromChild()
affected a Cached
member, the following adjustment is necessary:
public class MyDrawable : Drawable
{
- private readonly Cached myLayoutValue = new Cached();
-
- protected override void InvalidateFromChild(Invalidation invalidation, Drawable source)
- {
- if ((invalidation & (Invalidation.DrawSize | Invalidation.Presence)) > 0) /* any invalidation type of your choice */
- myLayoutValue.Invalidate();
- }
+ /* can also be a LayoutValue<T> */
+ private readonly LayoutValue myLayoutValue = new LayoutValue(Invalidation.DrawSize | Invalidation.Presence, InvalidationSource.Child);
+
+ public MyDrawable()
+ {
+ AddLayout(myLayoutValue);
+ }
}
In cases where both Invalidate()
and InvalidateFromChild()
affected the same Cached
member, the following adjustment is required:
public class MyDrawable : Drawable
{
- private readonly Cached myLayoutValue = new Cached();
-
- protected override bool OnInvalidate(Invalidation invalidation, Drawable source, bool shallPropagate)
- {
- var result = base.OnInvalidate(invalidation, source, shallPropagate);
-
- if ((invalidation & Invalidation.DrawSize) > 0) /* any invalidation type of your choice */
- result &= !myLayoutValue.Invalidate();
-
- return result;
- }
-
- protected override void InvalidateFromChild(Invalidation invalidation, Drawable source)
- {
- if ((invalidation & Invalidation.Presence) > 0) /* any invalidation type of your choice */
- myLayoutValue.Invalidate();
- }
+ /* can also be a LayoutValue<T> */
+ private readonly LayoutValue myLocalLayoutValue = new LayoutValue(Invalidation.DrawSize);
+ private readonly LayoutValue myChildLayoutValue = new LayoutValue(Invalidation.Presence, InvalidationSource.Child);
+
+ public MyDrawable()
+ {
+ AddLayout(myLocalLayoutValue);
+ AddLayout(myChildLayoutValue);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ /* wherever your re-validation was previously done */
+ if (!myChildLayoutValue.IsValid)
+ {
+ myLocalLayoutValue.Invalidate();
+ myChildLayoutValue.Validate();
+ }
+
+ if (!myLocalLayoutValue.IsValid)
+ {
+ /* your custom validation */
+ myLocalLayoutValue.Validate();
+ }
+ }
}
In cases where custom logic that can't be described as layout was done in Invalidate()
, the following adjustment is necessary:
public class MyDrawable : Drawable
{
- protected override bool OnInvalidate(Invalidation invalidation, Drawable source, bool shallPropagate)
- {
- var result = base.OnInvalidate(invalidation, source, shallPropagate);
-
- if ((invalidation & (Invalidation.DrawSize | Invalidation.Presence)) > 0) /* any invalidation type of your choice */
- {
- performCustomAction();
- result = true;
- }
-
- return result;
- }
+ protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
+ {
+ var result = base.OnInvalidate(invalidation, source);
+
+ if ((invalidation & (Invalidation.DrawSize | Invalidation.Presence)) > 0) /* any invalidation type of your choice */
+ {
+ performCustomAction();
+ result = true;
+ }
+
+ return result;
+ }
private void performCustomAction()
{
/* custom invalidation logic here */
}
}
The ItemsAdded
event failed to provide enough context when items are moved around or inserted into the list.
It has been replaced by the CollectionChanged
NotifyCollectionChangedEventHandler, which provides context such as the newly-added items and the indices at which the addition took place.
This event is triggered by the following methods with Action = NotifyCollectionChangedAction.Add
:
BindableList<T>.Add(T item)
BindableList<T>.Insert(int index, T item)
BindableList<T>.AddRange(IEnumerable<T> items)
The following is an example migration utilising the new CollectionChanged
event:
BindableList<int> list = new BindableList<int>();
- list.ItemsAdded += items =>
- {
- foreach (var item in items)
- Console.WriteLine($"Added: {item}");
- }
+ list.CollectionChanged += (_, args) =>
+ {
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ foreach (var item in args.NewItems.Cast<int>())
+ Console.WriteLine($"Added: {item});
+ break;
+ }
+ }
Note that CollectionChanged
should not be used in conjunction with the ItemsAdded
event.
As above, the ItemsRemoved
revent has also been replaced by the CollectionChanged
NotifyCollectionChangedEventHandler.
This event is triggered by the following methods with Action = NotifyCollectionChangedAction.Remove
:
BindableList<T>.Clear()
BindableList<T>.Remove(T item)
BindableList<T>.RemoveRange(int index, int count)
BindableList<T>.RemoveAt(int index)
BindableList<T>.RemoveAll(Predicate<T> match)
The following is an example migration utilising the new CollectionChanged
event:
BindableList<int> list = new BindableList<int>();
- list.ItemsRemoved += items =>
- {
- foreach (var item in items)
- Console.WriteLine($"Removed: {item}");
- }
+ list.CollectionChanged += (_, args) =>
+ {
+ switch (args.Action)
+ {
+ // Handles both clear and remove events
+ case NotifyCollectionChangedAction.Remove:
+ foreach (var item in args.OldItems.Cast<int>())
+ Console.WriteLine($"Removed: {item});
+ break;
+ }
+ }
Changed to match other methods and the class name it was creating.
Events affected:
Drawable.OnDoubleClick()
Drawable.OnDrag()
Drawable.OnDragEnd()
Drawable.OnMouseUp()
Drawable.OnKeyUp()
Drawable.OnJoystickRelease()
IKeyBindingHandler<T>.OnReleased()
A drawable may return false
for an input "begin" event to allow the event to propagate further through the hierarchy. Previously, such a drawable could then return true
for the input "end" event and prevent the event from propagating to the drawables which the input "begin" event was propagated to.
This could lead to incorrect implementations where drawables are left in weird states after handling input events.
By returning void
, the input "end" events can no longer be blocked by other drawables in the hierarchy and are guaranteed to be invoked if their respective input "begin" event was previously invoked.
The following table illustrates the events for which the relationship is satisfied:
begin event | end event(s) |
---|---|
OnDragStart() |
OnDrag() , OnDragEnd()
|
OnMouseDown() |
OnMouseUp() |
OnKeyDown() |
OnKeyUp() |
OnJoystickPress() |
OnJoystickRelease() |
OnPressed() |
OnReleased() |
All existing utility classes have been moved to the namespace osu.Framework.Utils
.
BasicTextBox
is provided as a drop-in replacement that comes with the framework design language.
Renamed to BasicPasswordTextBox
and comes with the framework design language.
The value provided in the constructor for Bindable<T>
is now used as both the initial and default value
The following lines of code are now identical.
var bindable1 = new Bindable<int>(10) { Default = 10 };
var bindable2 = new Bindable<int>(10);
As seen in https://github.com/ppy/osu/issues/6795, some users are turning on performance logging and leaving it on. This is not intended, as it adds a noticeable overhead to retrieve and write the stack traces to disk.
This change allows the setting to reset each game execution, rather than be saved to a user's configuration file.
osu!framework now targets .NET Standard 2.1. Consumers of the framework will need to install the .NET Core 3.0 SDK and change their projects to target netstandard2.1
/netcoreapp3.0
.
GetResponseString
and GetResponseData
methods are provided as a replacement. This change was done in order to indicate that these operations are expensive(they do memory allocations in the heap on each call).
BasicButton
is provided as a drop-in replacement that comes with the framework design language.
It was inaccurate for certain Quad
s. Use Area
as a replacement.
Provide an array of char
s as SpriteText.FixedWidthExcludeCharacters
instead.
Glyphs must now always be retrieved through an ITexturedGlyphLookupStore
. The SpriteText.CreateTextBuilder()
method is provided to allow overriding the store which glyphs are retrieved from.
Please switch to using BlendingParameters
static properties instead, whcih provide the same functionality. This change was made to expose full control over blend equations and simplify the blending class structure, which previously spanned three related types.
Usages of Cached
without initialising (via new Cached()
) will need to be updated.
Animation
will automatically auto-size on axes which are not relative sized (via RelativeSizeAxes
) and for which Size
has not been set.
There are now three modes of operation for paths:
// Auto size
new Path()
// Static size
new Path
{
AutoSizeAxes = Axes.None,
Size = new Vector2(100)
}
// Relative size
new Container
{
Size = new Vector2(100),
new Path
{
AutoSizeAxes = Axes.None,
RelativeSizeAxes = Axes.Both
}
}
MouseButtonX -> ExtraMouseButtonX
For each component, there are two ways to resolve resulting errors:
-
ContextMenuContainer
-
BasicContextMenuContainer
is provided as a drop-in replacement that comes with the framework design language. - Implement your own local
ContextMenuContainer
. The following is consumable code that matches the removed implementation. Rename it to suit your project and update your references:public class MyContextMenuContainer : ContextMenuContainer { // If you have a custom menu, provide it here. protected override Menu CreateMenu() => new BasicMenu(Direction.Vertical); }
-
-
Menu
-
BasicMenu
is provided as a drop-in replacement that comes with the framework design language. - Implement your own local
Menu
. The following is consumable code that matches the removed implementation. Rename it to suit your project and update your references:public class MyMenu : Menu { public MyMenu(Direction direction, bool topLevelMenu = false) : base(direction, topLevelMenu) { } protected override Menu CreateSubMenu() => new MyMenu(Direction.Vertical); protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new MyDrawableMenuItem(item); // If you have a custom scroll container, provide it here. protected override ScrollContainer<Drawable> CreateScrollContainer(Direction direction) => new BasicScrollContainer(direction); private class MyDrawableMenuItem : DrawableMenuItem { public MyDrawableMenuItem(MenuItem item) : base(item) { } protected override Drawable CreateContent() => new SpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding(5), Font = new FontUsage(size: 17), }; } }
-
-
DropdownMenu
/DropdownMenuItem
. This is only applicable if you already implement a customDropdown<T>
.- The following is consumable code that matches the removed implementation. Rename it to suit your project and update your references:
public class MyDropdown<T> : Dropdown<T> { // If you have a custom header, provide it here. protected override DropdownHeader CreateHeader() => null; protected override DropdownMenu CreateMenu() => new MyDropdownMenu(); private class MyDropdownMenu : DropdownMenu { // If you have a custom menu, provide it here. protected override Menu CreateSubMenu() => new BasicMenu(Direction); // If you have a custom scroll container, provide it here. protected override ScrollContainer<Drawable> CreateScrollContainer(Direction direction) => new BasicScrollContainer(direction); protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new MyDrawableDropdownMenuItem(item); private class MyDrawableDropdownMenuItem : DrawableDropdownMenuItem { public MyDrawableDropdownMenuItem(MenuItem item) : base(item) { } // If you have custom content, provide it here. protected override Drawable CreateContent() => new SpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding(5), Font = new FontUsage(size: 17), }; } } }
- The following is consumable code that matches the removed implementation. Rename it to suit your project and update your references:
If you were manually setting this, you can do so via .NET methods.
In a push to make the provided framework components design agnostic, ScrollContainer
is no longer provided. As a framework consumer you have a few options to resolve this:
- Use
BasicScrollContainer
The framework now provides BasicScrollContainer
and BasicScrollContainer<T>
. These are drop-in replacements but come with the framework design language, so it is only recommended as a temporary solution.
- Implement your own local
ScrollContainer
Here is consumable code to provide a local implementation that matches the removed one. Rename to suit your project and update your references:
public class MyScrollContainer : ScrollContainer<Drawable>
{
public MyScrollContainer(Direction scrollDirection = Direction.Vertical)
: base(scrollDirection)
{
}
protected override ScrollbarContainer CreateScrollbar(Direction direction) => new MyScrollbar(direction);
protected class MyScrollbar : ScrollbarContainer
{
private const float dim_size = 10;
private readonly Color4 hoverColour = Color4.White;
private readonly Color4 defaultColour = Color4.Gray;
private readonly Color4 highlightColour = Color4.Black;
private readonly Box box;
public MyScrollbar(Direction scrollDir)
: base(scrollDir)
{
Colour = defaultColour;
Blending = BlendingMode.Additive;
CornerRadius = 5;
const float margin = 3;
Margin = new MarginPadding
{
Left = scrollDir == Direction.Vertical ? margin : 0,
Right = scrollDir == Direction.Vertical ? margin : 0,
Top = scrollDir == Direction.Horizontal ? margin : 0,
Bottom = scrollDir == Direction.Horizontal ? margin : 0,
};
Masking = true;
Child = box = new Box { RelativeSizeAxes = Axes.Both };
ResizeTo(1);
}
public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None)
{
Vector2 size = new Vector2(dim_size)
{
[(int)ScrollDirection] = val
};
this.ResizeTo(size, duration, easing);
}
protected override bool OnHover(HoverEvent e)
{
this.FadeColour(hoverColour, 100);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
this.FadeColour(defaultColour, 100);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!base.OnMouseDown(e)) return false;
//note that we are changing the colour of the box here as to not interfere with the hover effect.
box.FadeColour(highlightColour, 100);
return true;
}
protected override bool OnMouseUp(MouseUpEvent e)
{
if (e.Button != MouseButton.Left) return false;
box.FadeColour(Color4.White, 100);
return base.OnMouseUp(e);
}
}
}
For existing code, LinearBatch(size, 1)
and QuadBatch(size, 1)
may be used to replace vertex buffer usages. Code must be updated to always invoke batch.Add(vertex)
with the vertices that should be drawn.
VertexBuffer
may still be derived for custom implementations.
VisibilityContainer
no longer implements IStateful<Visibility>
, instead exposing a single Bindable<Visibility> State
.
Texture.DrawTriangle()
and Texture.DrawQuad()
have been removed (#2475)
Use DrawNode.DrawTriangle()
and DrawNode.DrawQuad()
instead.
Global TrackStore
and ResourceStore
s can no longer access resources that are out of their respective namespaces.
Previously, it was possible to access any resource as a part of TrackStore
and SampleStore
via their Get
methods because the entire resource store would be nested inside them.
This nesting has been removed, so you can now only access files that are part of their specified namespaces (folders).
- For
TrackStore
, this will be theTracks
namespace. - For
SampleStore
, this will be theSamples
namespace.
To bring naming in-line with the purpose of audio stores, the following classes have been renamed:
SampleManager
-> SampleStore
TrackManager
-> TrackStore
In line with this, return values of GetSampleStore
and GetTrackStore
now return ISampleStore
and ITrackStore
respectively, hiding the underlying manager logic.
The public variables relating to these stores have been renamed to reflect this change:
AudioManager.Track
-> AudioManager.Tracks
AudioManager.Sample
-> AudioManager.Samples
ITrackStore
now has a GetVirtual
method which will create a virtual track. This will handle adding the track correctly to the audio subsystem. If one wishes to create a custom type of virtual tracks (a super-edge-case) you could implement the ITrackStore
interface and manually add your component to AudioManager
via AddItem
.
TabControl can now select nothing #2430
While this does not match most OS implementations of a tab control, this was deemed useful for o!f usage scenarios.
Previously, this code would throw an exception.
tabControl.Current.Value = null;
Now it is allowed.
Dropdown can now select nothing #2428
Previously, this code would throw an exception.
dropdownMenu.Current.Value = null;
Now it is allowed. Note that implementations of dropdown should be updated to handle this (common scenario is that the DropdownHeader
would not correctly handle a string.Empty
case if it was using AutoSize in the Y axis).
TabItem.IsRemovable
is true by default #2425
This felt like a more sane default. Any existing usage of custom TabItem
s where IsRemovable
was overridden, or any existing usage where removal is explicitly not wanted need to be updated.
Game.FrameStatisticsMode
is now a bindable named Game.FrameStatistics
#2399
This allows consumers to hook value changed events so they can, for instance, save the (framework) FPS display's state to config and handle hotkey-based changes.
Visual TestCase
s are now TestScene
s #2365
The term TestCase
is used by testing frameworks to denote single methods inside a test class. We were using it on the class itself as a prefix, which got quite confusing. This resolves the conflict and feels more correct as these are visual tests – ie. "scenes".
Note that the TestCase
prefix is still supported by tooling for the time being. We still recommend you update (via a simple rename) as soon as possible.
Drawable.ApplyDrawNode()
has been removed #2314
The direction of application of DrawNode
states has been inversed.
Previously:
class MyCustomDrawable : Drawable
{
private bool state;
protected override DrawNode CreateDrawNode() => new CustomDrawNode();
protected override void ApplyDrawNode(DrawNode node)
{
base.ApplyDrawNode(node);
var n = (CustomDrawNode)n;
n.State = state;
}
private class CustomDrawNode : DrawNode
{
public bool State;
}
}
Now:
class MyCustomDrawable : Drawable
{
private bool state;
protected override DrawNode CreateDrawNode() => new CustomDrawNode(this);
private class CustomDrawNode : DrawNode
{
protected new MyCustomDrawable Source => (MyCustomDrawable)base.Source;
private bool state;
public CustomDrawNode(MyCustomDrawable source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
state = Source.state;
}
}
}
The namespace of EdgeEffectParameters
and EdgeEffectType
has changed #2314
They now reside in osu.Framework.Graphics.Effects
.
Font fallback order changes #2296
Games adding their own fonts can now do so directly to Game.Fonts
. Fallback (framework-side) fonts are now always at the lowest priority.
Previously:
// this completely overrides the framework default. will need to change once we make a proper FontStore.
dependencies.Cache(Fonts = new FontStore(new GlyphStore(Resources, @"Fonts/FontAwesome")));
Fonts.AddStore(new GlyphStore(Resources, @"Fonts/osuFont"));
now:
Fonts.AddStore(new GlyphStore(Resources, @"Fonts/osuFont"));
FontAwesome and SpriteIcon is provided by the framework #2293
If you manually added either of these to your project, please remove them.
Usage:
new SpriteIcon
{
Icon = FontAwesome.Refresh,
};
Parameter order of AddUntilStep
/ AddWaitStep
reversed #2256
In order to standardise parameter order, these two methods' parameters have been reversed. Old parameter order methods are available temporarily (as [Obsolete]
) to ease migration.
New attribute [SetUpSteps]
added #2266
Finally allows steps to be added that will run before every [Test]
method.
Implicit operator removed from Bindable
#2152
In order to avoid accidental misuse / misunderstandings, .Value
must always be added to Bindable
when requesting its value. One such case which used to be confusing:
Bindable<Drawable> bindableDrawable = new Bindable<Drawable>();
if (bindableDrawable != null)
{
// true
}
if (bindableDrawable.Value != null)
{
// false
}
Drawable drawable = null;
if (bindableDrawable == drawable)
{
// ???
}
Bindable.ValueChanged now provides ValueChangedEvent
#2012
It is now possible to access the old value. This was a common requirement in bindable usage which we had issues with until now.
bindable.ValueChanged += e =>
{
Console.WriteLine($"Value changed from {e.OldValue} to {e.NewValue}");
}
Introduction of FontUsage
#2043
While old methods of setting font attributes will still work, they are now marked as [Obsoleted]
. You should use Font = new FontUsage(size: 200)
going forward. For convenience, you can adjust existing fonts like so:
var font = new FontUsage(family: "SourceCodePro");
var fontWithSize = font.With(size: 24);
- Create your first project
- Learning framework key bindings
- Adding resource stores
- Adding custom key bindings
- Adding custom fonts