编写生成器
如何用SDK的基类构建生成器,以及它要在编辑器中正常工作必须遵守什么
生成器根据几个参数生成关卡内容。作者不必亲手放置每一个物体
添加生成器就是添加一个类。宿主根据约定构建它的表单、估算和撤销。没有人为它写界面,也不需要编辑任何列表
内置生成器能做什么:生成器
三种类型
| 类型 | 产出 | 基类 | 入口 |
|---|---|---|---|
Level | 新的Level和LevelMeta | BaseLevelGenerator<TParams> | Create(parameters) |
Content | 在当前作用域中新增物体和资源 | BaseContentGenerator<TParams> | Run(context, parameters) |
Modifier | 修改已存在的物体 | BaseModifier<TParams> | Run(context, parameters) |
Content和Modifier共用同一个入口。它们的区别在于意图和GeneratorRequirements。修改器默认要求有选中项
对于生成物体的生成器,有BaseSpawnGenerator<TParams>。它负责创建每个物体、设置父级并放到时间上。具体的类只需要处理摆放的数学计算
没有注册表
GeneratorRegistry在第一次被访问时通过反射找到所有生成器。两个生成器的NameKey相同会当场失败
只扫描SDK自身的程序集。所以新的生成器放在SDK仓库中,以拉取请求的形式加入。更多:参与SDK开发
示例
一排物体,基于与内置RadialGenerator相同的类构建:
using BH.SDK.Generators;
using BH.SDK.Generators.Spawn;
using BH.SDK.Rules;
public class RowGenerator : BaseSpawnGenerator<RowGenerator.Parameters>
{
public override string NameKey => "gen_geometry_row";
public override GeneratorHints Hints { get; } = new GeneratorHints.Builder()
.Section(GeneratorSections.Main, SpawnParameters.MainFields)
.Section(GeneratorSections.Main, nameof(Parameters.Count), nameof(Parameters.Spacing))
.Section(GeneratorSections.Additional, SpawnParameters.AdditionalFields)
.Section(GeneratorSections.Additional, nameof(Parameters.StartX), nameof(Parameters.Y))
.Range(nameof(Parameters.Count), 1, 256)
.Range(nameof(Parameters.Spacing), 0f, ValueRules.MaxPos)
.Range(nameof(Parameters.StartX), ValueRules.MinPos, ValueRules.MaxPos)
.Range(nameof(Parameters.Y), ValueRules.MinPos, ValueRules.MaxPos)
.Range(nameof(SpawnParameters.Size), ValueRules.MinSca, ValueRules.MaxSca)
.Build();
protected override void Generate(GeneratorContext context, Parameters parameters)
{
for (var i = 0; i < parameters.Count; i++)
{
var obj = Spawn(context, parameters, quot;row_{i}", context.Span);
AddPosition(obj, parameters.StartX + i * parameters.Spacing, parameters.Y, obj.Span.StartFrame);
}
}
// Spawn adds a size key and a colour key, AddPosition adds the third
protected override GeneratorCost EstimateTyped(GeneratorContext context, Parameters parameters)
=> new GeneratorCost(parameters.Count, parameters.Count * 3);
public class Parameters : SpawnParameters
{
public int Count = 8;
public float Spacing = 2f;
public float StartX;
public float Y;
}
}
NameKey的形式是本地化键。宿主通过自己的字符串表显示名称
必须遵守的规则
- 只通过
GeneratorContext修改关卡(Create、Edit、Delete、SetValue等)。它把每次改动记录到GeneratorChangeLog中,撤销完全依赖于它。直接操作模型能通过编译,却会悄悄破坏撤销 - 每个字段都列在某个分组中,每个数字都有
Range。反射得到的字段顺序没有保证。数字没有边界时,宿主就无法对它钳制。有测试强制检查范围 - 参数是公开的可变字段,并有无参构造函数。表单绑定到它们,预设序列化它们。不要遮蔽继承的字段:一切都以字段名为键
- 估算与实际运行一致。宿主在运行前显示估算。如果运行会超出
LevelRules.MaxObjects,宿主会拒绝运行 - 随机性来自
context.CreateRandom(),而不是System.Random。同一个种子在任何运行环境上都生成相同的关卡 - 把创建的内容挂到
context.Parent下。宿主正是这样把一次运行的全部内容归为一个物体 - 如果访问
context.Game或context.Audio,声明GeneratorRequirements.LevelScope。当前作用域是预制件时,两者都为null - 当某种参数组合会删除或改写作者当前所看窗口之外的内容时,重写
IsDangerousTyped。宿主随后会请求确认
外部数据
SDK没有音频解码器、FFT或图片加载器。需要这类数据的生成器:
- 声明
GeneratorRequirements.ExternalAnalysis - 实现
External/中的接口:IWaveformInput、IBeatFramesInput、IPixelTextureInput等
宿主在运行前填好数据。没有拿到任何数据时,生成器必须什么也不生成
完整的约定见Generators/README.md