跳到主要内容

从零到一写第一个小组件

关于 C# 的技术细节

这篇文章假设你已经掌握了基本的开发流程,并且对 C# 的语言特性有一定的了解,因此对一些软件开发方面的名词不会过多解释。

如果你没有理解一些内容,请考虑在 C# 文档中查阅相关信息。

信息

这篇文章曾是一篇文风奇差无比、拙劣捧哏充斥其间的梗文,现已经过完全重写。

如果读者你对这篇文章的原版感兴趣(并且你真的特别想读),请到这个仓库的提交记录里自行查找。

关于这篇文章的心路历程,参见我的博客(链接待补充)。

你好

提到小组件,大家会想到什么呢?相信用过图形界面的各位,对这个术语应该是很熟悉的。

简单来说,小组件 (Widget) 是图形用户界面的一个组成部分,用来显示一些信息,以及实现部分交互的功能。在这篇文章中,笔者我将会着手带你写一个简单的小组件。

在众多的小组件类型中,最简单的是文本显示类(并驾齐驱的还有各类图标显示等等),因此我们以做一个这样的组件为目标。

类初始化

在 osu!lazer 的框架结构中,一个小组件的实现通常通过一个类来实现,这里拿我们在 Ruleset 开发中的 Ottoman 项目举个例子。没读过那里的内容也没关系,这篇文章讲述的内容是相对基础与独立的。

选址

在考虑组件类实现的功能之前,需要先确定组件类要定义的位置。

初次上手,建议你先 Fork 一下 osu!lazer 本体的源码仓库,下载到本地并在其上做更改,这样你新写的组件可以轻易在游戏界面的各处使用。在驾轻就熟之后,你也可以尝试在其他地方去做:

  • osu!framework
  • 独立的 Ruleset 项目
说明

之所以使用 osu!lazer 的源码,是因为测试时直接构建运行就好了,步骤简单方便。不过相应的构建时间会有点长,不够“轻量”。

实际上由于 Ruleset 输出的是类库文件,除非使用自动化脚本把输出库文件移动到 lazer 目录,否则每次手动操作替换还是有点麻烦的。

如果你想的话,也可以在整个解决方案里开一个这样的项目,然后加依赖与引用就行了。当然这个方法我没多测,需要后续研究。

基础代码

确定选址之后,就可以创建一个类文件,基本结构如下:

osu.Game/Graphics/ShortDollyWidget.cs
using osu.Framework.Graphics.Containers;

namespace osu.Game.Graphics
{
public partial class ShortDollyWidget : CompositeDrawable
{
}
}

CompositeDrawable 是在组件开发中经常继承(实现)的一个类,其中包含我们经常使用的属性与方法。

问题

在上面这段代码中,如果主类不使用 partial 修饰,会出现警告:

OFSG001: Types that are candidates for dependency injection should be made partial to benefit from compile-time optimisations.

经过一小段时间的研究,可能原因是出于:

  • 构建工具会生成含有这个类的文件,partial 有助于使其与原有类共存。经过一些调查,可能与类中对象的标注 (Annotation) 有关。
自动生成的类示例
// <auto-generated/>
#nullable enable
#pragma warning disable CS4014

namespace osu.Game.Tournament.Screens.Board
{
partial class BoardScreen : global::osu.Framework.Allocation.ISourceGeneratedDependencyActivator
{
public override void RegisterForDependencyActivation(global::osu.Framework.Allocation.IDependencyActivatorRegistry registry)
{
if (registry.IsRegistered(typeof(global::osu.Game.Tournament.Screens.Board.BoardScreen)))
return;
base.RegisterForDependencyActivation(registry);
registry.Register(typeof(global::osu.Game.Tournament.Screens.Board.BoardScreen), (t, d) =>
{
((global::osu.Game.Tournament.Screens.Board.BoardScreen)t).sceneManager = global::osu.Framework.Utils.SourceGeneratorUtils.GetDependency<global::osu.Game.Tournament.TournamentSceneManager?>(d, typeof(global::osu.Game.Tournament.Screens.Board.BoardScreen), null, null, true, true);
((global::osu.Game.Tournament.Screens.Board.BoardScreen)t).load(global::osu.Framework.Utils.SourceGeneratorUtils.GetDependency<global::osu.Framework.Graphics.Textures.TextureStore>(d, typeof(global::osu.Game.Tournament.Screens.Board.BoardScreen), null, null, false, false));
}, null);
}
}
}
  • 依赖注入更灵活?
  • 代码组织?
  • 测试方便?

上面这些内容出于 AI,暂时没摸透,有懂哥的话可以在评论区交流交流。

这部分内容作为 osu!framework 的一个底层特性,会在另外的部分解释。

加载元素

一个小组件是由若干元素(子组件)组成的,同时拥有其自己的属性。在创建组件类的实例时,这些内容需要被按需设置。

一种典型的情况是使用带 BackgroundDependencyLoader 属性标注 (Annotation) 的方法进行加载。出于其功能与可见范围,我们一般将其命名为 load()

osu.Game/Graphics/ShortDollyWidget.cs
namespace osu.Game.Graphics
{
public partial class ShortDollyWidget : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
// Dolly goes here!
}
}
}
BackgroundDependencyLoader 原理简述

带有 [BackgroundDependencyLoader] 标注的函数,在类初始化后会先行执行,准备好后续步骤需要的一些东西。

一个组件类中仅可有一个带有这样标签的函数,且这个函数必须是 private 可见。

详情请见这篇文章(待补充)。

在这个函数中,我们可以想到更改组件的锚点 (Anchor)、原点 (Origin)、子元素 (Children),这些内容可以帮助定下组件的大致样式。

与此同时,osu!lazer 也提供了很多能直接使用的组件类,例如文本显示类 OsuSpriteText

ShortDollyWidget.cs
/// Truncated
[BackgroundDependencyLoader]
private void load()
{
// Dolly goes here!
Anchor = Anchor.Centre;
Origin = Anchor.Centre;

InternalChildren = new Drawable[]
{
// 请不要使用
// new SpriteText
new OsuSpriteText
{
Text = "Dolly",
}
}
}
/// Truncated

InternalChildren 列出了这个组件中包含的子组件,它们将会按各自定义的属性进行显示。

为什么不是 SpriteText

SpriteText 类定义在 osu!framework 中,是其他各种文本显示组件的基本组成部分,直接使用其显示文本会显得比较危险,同时也不利于管理,因此在项目层面 Ban 掉了这个类的直接使用(写在 CodeAnalysisBannedSymbols.txt 里,因此说是项目层面)。

针对这样的情况,开发者由此衍生出了 OsuSpriteText 类,默认使用带阴影的默认字体,在整个项目范围内广泛使用。与此相似的还有赛事客户端项目中的 TournamentSpriteText 类。

效果展示

到此为止,组件类方面的工作已经大致完成。但要将其显示在哪里,我们还要在另一个组件的 ChildrenInternalChildren 中引用它,按照上面的方法即可,这里不再赘述。

在写完这些代码之后,构建项目,转到你添加到的组件所在界面,应该能看到如下图的文本:

道理

用过 osu!lazer 赛事客户端的读者应该熟悉,笔者将这个组件加到了 TournamentSceneManager 的子组件列表里(虽然实际开发中不推荐这么做,这里只是为了示例简单需要)。

看完这些内容,你是否也有很多疑惑呢?我们会在后续的文章中详细解释它们,敬请期待。