基本信息
每个 Ruleset 都有一些确定其属性的基本信息,这些信息会为 osu!lazer 所利用,并呈现在用户界面层次上。
为了演示与撰写方便,这个系列教程的多数内容会以 CloneWith/ruleset-typer 为原型进行讲述与展开(或者是作为模版进行改写),主要因为这个项目相对简单、结构也比较清晰,是比较容易入手的。
对于其他的项目,实际情况可能有所不同,但主要结构都是差不多的。
示例代码
由于基本信息都集成在一个类里,并且类型多样,初见时可能会觉得无所适从。不过可以尝试将这些信息分为几个类别单独去了解学习。
这里先给出示例 Ruleset Ottoman
的核心代码,接下来将会分开说明。目前我们假定:
- Ottoman 是一个简单的 Ruleset
- Ottoman 没有自己的专谱,将 osu!std 谱面进行转换而游玩
- Ottoman 使用的是水平滚动式布局(也就是与太鼓之类的差不多)
- Ottoman 实现了一些常见的模组
namespace osu.Game.Rulesets.Ottoman
{
public class OttomanRuleset : Ruleset
{
public override string Description => "Play with Short Dolly?";
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableOttomanRuleset(this, beatmap, mods);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OttomanBeatmapConverter(beatmap, this);
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OttomanDifficultyCalculator(RulesetInfo, beatmap);
public override IEnumerable<Mod> GetModsFor(ModType type)
{
switch (type)
{
case ModType.DifficultyReduction:
return new Mod[]
{
new OttomanModEasy(),
new OttomanModNoFail(),
new MultiMod(new OttomanModHalfTime(), new OttomanModDaycore()),
};
case ModType.DifficultyIncrease:
return new Mod[]
{
new OttomanModHardRock(),
new MultiMod(new OttomanModSuddenDeath(), new OttomanModPerfect()),
new MultiMod(new OttomanModDoubleTime(), new OttomanModNightcore()),
new OttomanModHidden(),
new OttomanModFlashlight(),
new ModAccuracyChallenge(),
};
case ModType.Automation:
return new Mod[]
{
new MultiMod(new OttomanModAutoplay(), new OttomanModCinema()),
};
case ModType.Conversion:
return new Mod[]
{
new OttomanModDifficultyAdjust(),
new OttomanModConstantSpeed(),
};
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp(), new ModWindDown()),
new OttomanModMuted(),
new ModAdaptiveSpeed(),
};
default:
return Array.Empty<Mod>();
}
}
public override string ShortName => "ottoman";
public override string PlayingVerb => "Showing Dolly";
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
{
new KeyBinding(InputKey.A, OttomanAction.ButtonA),
new KeyBinding(InputKey.B, OttomanAction.ButtonB),
};
public override Drawable CreateIcon() => new SpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = ShortName[0].ToString(),
Font = OsuFont.Default.With(size: 18),
};
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
}
}
属性
ShortName
public override string ShortName => "ottoman";
ShortName
可以理解为 Ruleset 的名称,会在鼠标悬停在顶栏的对应 Ruleset 图标上时,作为“标题”显示(当然没有相对应的 LongName
就是了)。
Description
public override string Description => "Play with Short Dolly?";
Description
会作为“副标题”显示在 Ruleset 信息的第二行,起到描述说明的作用。
PlayingVerb
public override string PlayingVerb => "Showing Dolly";
PlayingVerb
会在玩家游玩这个 Ruleset 时,显示在正在游玩
面板、顶栏右侧个人档案等等的用户卡片上(例如玩 osu!std 时会显示的 Clicking Circles
)。
RulesetAPIVersionSupported
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
RulesetAPIVersionSupported
会定义这个版本 Ruleset 需要的最低版本。低于这个版本的 osu!lazer 无法加载这个 Ruleset,而高于此版本的一般可以加载。
这个数值一般不需要更改(因为 CURRENT_RULESET_API_VERSION
是随依赖的 osu.Game
版本变化的),修改了反而可能出一些奇怪的问题。
在不同版本号的 osu!lazer 之间(尤其是大版本之间,如 2023
与 2024
)可能会引入一些 Ruleset API 相关的更改,从而会导致 Ruleset 的部分或所有功能无法正常使用。遇到这种情况时,osu!lazer 一般会显示错误通知,并会记录到 runtime
日志中。
因此在开发 Ruleset 时,要注意不同版本 osu!lazer(NuGet 包)的变化,并勤看看更新日志(在 osu! 官网或者 GitHub 仓库的提交记录),找找可能的破坏性更改。
方法
GetModsFor()
public override IEnumerable<Mod> GetModsFor(ModType type)
{
switch (type)
{
case ModType.DifficultyReduction:
return new Mod[]
{
new OttomanModEasy(),
new OttomanModNoFail(),
new MultiMod(new OttomanModHalfTime(), new OttomanModDaycore()),
};
case ModType.DifficultyIncrease:
return new Mod[]
{
new OttomanModHardRock(),
new MultiMod(new OttomanModSuddenDeath(), new OttomanModPerfect()),
new MultiMod(new OttomanModDoubleTime(), new OttomanModNightcore()),
new OttomanModHidden(),
new OttomanModFlashlight(),
new ModAccuracyChallenge(),
};
case ModType.Automation:
return new Mod[]
{
new MultiMod(new OttomanModAutoplay(), new OttomanModCinema()),
};
case ModType.Conversion:
return new Mod[]
{
new OttomanModDifficultyAdjust(),
new OttomanModConstantSpeed(),
};
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp(), new ModWindDown()),
new OttomanModMuted(),
new ModAdaptiveSpeed(),
};
default:
return Array.Empty<Mod>();
}
}
获取 Ruleset 可以使用的模组,按不同的模组分类呈现在选歌页面可以唤起的模组菜单中。此部分的更多内容将在单独的文章中展示。
GetDefaultKeyBindings()
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
{
new KeyBinding(InputKey.A, OttomanAction.ButtonA),
new KeyBinding(InputKey.B, OttomanAction.ButtonB),
};
获取默认的键位绑定设置。在首次加载 Ruleset 时,会使用对应的 InputKey
设置初始化按键设置;同样地,在重置键位时也会用到这里的配置。
建议你在这个函数中定义好所有玩家要用到的键位,以免使用上的不便。
如果你在测试键位设置,碰巧又改了这部分代码(在首次测试之后),那么新的默认键位设置不会立刻生效,需要你在设置中手动恢复默认值。
CreateIcon()
public override Drawable CreateIcon() => new SpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = ShortName[0].ToString(),
Font = OsuFont.Default.With(size: 18),
};
生成 Ruleset 的图标,会在各种能用到的地方(多见于顶栏模式列表,以及选歌页面)显示。
在这段代码中使用的是以元素/组件为单位手动生成图标的方式,将 Ruleset 名称的首字母作为图标显示了。对于组件设计的更多内容,会在单独的系列文章中提及。
除此之外,你也可以选择使用 FontAwesome
(osu!lazer 有引用这个图标库),或者使用材质库(TextureStore
,这样就能够使用自定义图像)中的东西来显示图标。
CreateDrawableRulesetWith()
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableOttomanRuleset(this, beatmap, mods);
生成一个表面完整的游戏实例(包括游戏界面、输入处理等等),Drawable
就在暗示这部分要处理的东西与用户界面有关。
这个函数需要带上谱面与模组设置作为参数,以便游戏界面进一步处理。
CreateBeatmapConverter()
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OttomanBeatmapConverter(beatmap, this);
用于转换已有谱面到本 Ruleset 的形式(物件等等),需要带上谱面作为参数,用于转换。
CreateDifficultyCalculator()
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OttomanDifficultyCalculator(RulesetInfo, beatmap);
用于计算谱面的难度,需要带上谱面作为参数。
后面的三个方法基本上是对对应处理类的包装,使用了一些接口来简化工作流程。
对于这些类的详细信息会在后续文章中讲述。