0719 更新

This commit is contained in:
OLDREDSTONE 2025-07-19 18:49:07 +08:00
parent c7b6c35ddc
commit e3d6c8c572
18 changed files with 1594 additions and 20 deletions

View File

@ -28,8 +28,8 @@
- [<img src="/indexicon/settheme.png"/><p>场景与主题</p>](/pages/settheme.md)
- [<img src="/indexicon/rowfx.png"/><p>轨道视效</p>](/pages/rowfx.md)
- 进阶:节奏设计
- [<img src="/indexicon/moreclassicbeats.png"/><p>普通拍的衍生节奏型</p>](/pages/moreclassicbeats.md)
- [<img src="/indexicon/moreoneshotbeats.png"/><p>单发拍的衍生节奏型</p>](/pages/moreoneshotbeats.md)
- [<img src="/indexicon/moreclassicbeats.png"/><p>普通拍的衍生节奏型</p>](/pages/moreclassicbeats.md)
- [<img src="/indexicon/moreoneshotbeats.png"/><p>单发拍的衍生节奏型</p>](/pages/moreoneshotbeats.md)
- [<img src="/indexicon/beatpatterndesign.png"/><p>采音和节奏设计</p>](/pages/beatpatterndesign.md)
- [<img src="/indexicon/chartdesign.png"/><p>关卡设计</p>](/pages/chartdesign.md)
- [<img src="/indexicon/audiodesign.png"/><p>音效设计</p>](/pages/audiodesign.md)
@ -53,6 +53,9 @@
- [<img src="/indexicon/custommethod.png"/><p>自定义方法</p>](/pages/custommethod.md)
- [<img src="/indexicon/condition.png"/><p>条件</p>](/pages/condition.md)
- [<img src="/indexicon/variable.png"/><p>变量</p>](/pages/variable.md)
- 高级:第三方工具
- [<img src="/indexicon/3rdparty_coderd.png"/><p>[Python]CodeRD</p>](/pages/3rdparty_coderd.md)
- [<img src="/indexicon/3rdparty_rdtk.png"/><p>[C#]RhythmBase</p>](/pages/3rdparty_rdtk.md)
- 附录
- [<img src="/indexicon/events.png"/><p>事件列表</p>](/pages/appendix_events.md)
- [<img src="/indexicon/vfxs.png"/><p>预设特效列表</p>](/pages/appendix_vfxs.md)
@ -62,6 +65,5 @@
- [<img src="/indexicon/custommethods.png"/><p>自定义方法表</p>](/pages/appendix_custommethods.md)
- [<img src="/indexicon/specifications.png"/><p>制谱规范</p>](/pages/appendix_levelrules.md)
- [<img src="/indexicon/comparison.png"/><p>英汉对照</p>](/pages/appendix_translations.md)
- [<img src="/indexicon/coding.png"/><p>其他编写关卡的手段</p>](/pages/appendix_coding.md)
- [<img src="/indexicon/tools.png" width="64px"/><p>工具</p>](/pages/appendix_tools.md)
- [<img src="/indexicon/credits.png"/><p>制作人员</p>](/pages/credits.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

BIN
indexicon/3rdparty_rdtk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

BIN
indexicon/tools.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

BIN
indexicon/twoplayers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

View File

@ -1,3 +1,4 @@
[项目地址](https://github.com/RDCN-Community-Developers/CodeRD)
# 代码编写关卡
点击[此处](../resources/compiler.exe)下载。项目作者:硫酸铜。项目来源:[PyRD - 为视障谱师写节奏医生谱子的一种全新的尝试](https://github.com/qwedc001/pyrd)。

769
pages/3rdparty_rdtk.md Normal file
View File

@ -0,0 +1,769 @@
[项目地址](https://github.com/RDCN-Community-Developers/RhythmToolkit)
[English](/pages/3rdparty_rdtk_EN.md) | 中文
# RhythmBase 使用教程
本项目为节奏医生关卡开发者服务,旨在为开发人员提供更加系统、直观的关卡编辑媒介。
感谢节奏医生饭制部玩家对这个项目的支持。
欢迎来到 RhythmBase 教程。本指南将帮助您开始在项目中设置和使用 RhythmBase。
## 安装 Nuget 包
要安装 RhythmBase NuGet 包,请按照以下步骤操作:
1. 打开 Visual Studio 或使用命令行工具。
2. 在 Visual Studio 中,依次点击 **工具** > **NuGet 包管理器** > **程序包管理器控制台**
3. 在控制台中输入以下命令:
```
Install-Package RhythmBase -Version 1.2.0-rc2
```
4. 等待安装完成,并确保项目已引用所需的 NuGet 包。
5. 如果使用 .NET CLI请输入
```
dotnet add package RhythmBase -version 1.2.0-rc2
```
## 编写
### 创建关卡
关卡是一个事件集合, 如果使用 C# 12 或更高版本, 可以使用[集合表达式](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/collection-expressions)以创建空关卡。
```cs
using RhythmBase.RhythmDoctor.Components;
using RDLevel emptyLevel = [];
Console.WriteLine(emptyLevel); // "" Count = 0
```
也可以使用自带的模板以创建具有基础事件的关卡。
此模板即为节奏医生编辑器默认创建的关卡模板。
```cs
using RhythmBase.RhythmDoctor.Components;
using RDLevel defaultLevel = RDLevel.Default;
Console.WriteLine(defaultLevel); // "" Count = 3
```
### 读取和写入
可以直接以文件路径读取和导出文件。将以默认读写设置读写文件。
导出时不会打包为关卡包。
```cs
using RhythmBase.RhythmDoctor.Components;
// 直接读取关卡文件
using RDLevel rdlevel1 = RDLevel.Read(@"your\level.rdlevel");
// 读取关卡包文件
using RDLevel rdlevel2 = RDLevel.Read(@"your\level.rdzip");
// 读取关卡压缩包
using RDLevel rdlevel3 = RDLevel.Read(@"your\level.zip");
// 写入关卡文件
rdlevel1.Write(@"your\outLevel.rdlevel");
```
可以添加自定义读写设置 `LevelReadOrWriteSettings` 以读写关卡。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Settings;
// 编写自定义读写设置
LevelReadOrWriteSettings settings = new()
{
// 对未激活事件的处理方式
InactiveEventsHandling = InactiveEventsHandling.Store,
// 对读取出现异常的事件的处理方式
// 常见于精灵事件未与精灵轨道绑定等
UnreadableEventsHandling = UnreadableEventHandling.Store,
// 是否启用缩进
Indented = true,
};
using RDLevel rdlevel1 = RDLevel.Read(@"your\level.rdlevel", settings);
```
也可以生成 json 对象或 json 字符串以进行进一步操作。
```cs
using Newtonsoft.Json.Linq;
using RhythmBase.RhythmDoctor.Components;
JObject jobject = rdlevel.ToJObject();
string json = rdlevel.ToRDLevelJson(settings);
```
`LevelReadOrWriteSettings` 添加了 `BeforeReading`, `AfterReading`, `BeforeWriting`, `AfterWriting` 事件,分别会在关卡读取之前、之后,写入之前、之后触发。
可在这些事件上添加监听以达到特定效果。
```cs
using RhythmBase.RhythmDoctor.Settings;
settings.AfterWriting += Settings_AfterReading;
// 将在写入结束时触发
void Settings_AfterReading(object? sender, EventArgs e)
{
throw new NotImplementedException();
}
rdlevel.Write(@"your\outLevel.rdlevel", settings);
```
> 在读取关卡压缩包文件时请使用 `using` 语句或主动调用 `RDLevel.Dispose()` 方法以保证被解压的临时文件被及时销毁。
### 查找和获取事件
`OrderedEventCollection` 类型用于存储事件集合, `RDLevel` 继承此类型。
可以使用针对节奏医生关卡事件的查询操作的扩展方法, 用以简化查询操作。
例如通过事件类型及其父类型、事件实现的接口、节拍范围、自定义谓词等。
提供 `AddRange()`, `RemoveRange()`, `Where()`, `RemoveAll()`, `First()`, `FirstOrDefault()`, `Last()`, `LastOrDefault()`, `TakeWhile()` 等扩展方法。
推荐使用这些经过效率优化的方法。
```cs
using RhythmBase.RhythmDoctor.Extensions;
using RhythmBase.RhythmDoctor.Components;
// 查找在第 3 到第 5 小节、在事件栏的第 0 到第 2 行的移动轨道事件
var list = rdlevel.Where<MoveRow>(
i => 0 <= i.Y && i.Y < 3, // 在事件栏的第 0 到第 2
3..5 // 第 3 到第 5 小节
);
```
`Row``Decoration` 也继承 `OrderedEventCollection`, 所以轨道和精灵也支持这些扩展方法。
```cs
using RhythmBase.RhythmDoctor.Extensions;
using RhythmBase.RhythmDoctor.Components;
// 查找在第 11 小节第 1 拍到第 13 小节第 1 拍的普通拍子事件
var list = rdlevel.Decorations[0].Where<Tint>(
new Beat(11, 1), // 查找起点为第 11 小节第 1 拍
new Beat(13, 1) // 查找终点为第 13 小节第 1 拍
);
```
### 创建节拍
`RDBeat` 是一个结构体, 它缓存三个信息 `BeatOnly`, `BarBeat``TimeSpan`
可以创建一个不与关卡相关的 `RDBeat` 实例, 但因为缺失和关卡的联系, 其功能可能不完善。
可以查看它的 `IsEmpty` 属性以了解此实例是否可用。
在没有关卡联系的情况下, 调用它的 `ToString()` 方法会显示此实例拥有的信息和缺失的信息。
```cs
using RhythmBase.RhythmDoctor.Components;
// 创建与关卡无关联的节拍
RDBeat beat1 = new(11);
RDBeat beat2 = new(2, 3);
RDBeat beat3 = new(TimeSpan.FromSeconds(11.45));
Console.WriteLine(beat1); // [10,?,?]
Console.WriteLine(beat2); // [?,(2, 3),?]
Console.WriteLine(beat3); // [?,?,00:00:11.4500000]
```
可以依靠 `BeatCalculator` 实例或 `RDLevel` 实例创建一个和关卡关联的 `RDBeat` 实例。
其中 `BeatCalculator` 伴随 `RDLevel` 的创建而创建, 可通过 `RDLevel.Calculator` 访问。
在有关卡联系的情况下, 调用它的 `ToString()` 方法会显示 `BarBeat` 属性。
只有与关卡关联, 三个属性才能建立联系, 节拍才能参与所有运算。否则只有相应属性存在数据才能运算。
关卡内事件和书签的节拍属性都是与关卡相关联的, 而移出的事件会断开与关卡的关联。
```cs
using RhythmBase.RhythmDoctor.Components;
// 创建与关卡有关联的节拍
RDBeat beat1 = rdlevel.BeatOf(11);
RDBeat beat2 = rdlevel.Calculator.BeatOf(2, 3);
RDBeat beat3 = beat1 - 10 + TimeSpan.FromSeconds(11.45);
Console.WriteLine(beat1); // [2,3]
Console.WriteLine(beat2); // [2,3]
Console.WriteLine(beat3); // [3,4.083334]
```
在节拍之间参与运算时, 若都有所链接的关卡, 需确保其指向的关卡相同。
可调用 `FromSameLevel()``FromSameLevelOrNull()` 方法检查其是否指向相同关卡。
可调用 `WithoutLink()` 返回一个脱离关卡的新节拍实例。
```cs
using RhythmBase.RhythmDoctor.Components;
RDBeat beat1 = rdlevel.BeatOf(1);
RDBeat beat2 = beat1.WithoutLink();
Console.WriteLine(beat1.FromSameLevel(beat2)); // False
Console.WriteLine(beat1.FromSameLevelOrNull(beat2)); // True
```
`BeatCalculator` 也带有时间转换的方法, 可以对不同的时间单位进行转换。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Utils;
(float, float) barbeat = rdlevel.Calculator.TimeSpanToBarBeat(TimeSpan.FromSeconds(19.19)); // (4, 8.983334)
```
`RDLevel` 带有一个和此实例关联的节拍数为 1 的默认节拍。
```cs
using RhythmBase.RhythmDoctor.Components;
RDBeat @default = RDLevel.DefaultBeat;
```
`RDRange` 是一个与 `Range` 相似的数据类型, 用以表示一个节拍范围。
常用于查询事件。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Extension;
var result = rdlevel.Where(new RDRange(rdlevel.DefaultBeat + 10, null));
```
### 扩展数据类型
`RD` 开头, 带有 `Point`, `Size`, `Rect`, `RotatedRect` 的名称的类型都是 `RhythmBase` 里与平面几何相关的数据类型。
后缀带有 `I` 的为整数类型, 其所有数据属性都为 `int`, 如 `RDPointI.X`
后缀带有 `N` 的为非空类型, 其所有数据属性都为不可空类型, 如 `RDSizeN.Height`
后缀带有 `E` 的为表达式类型, 其所有数据属性都为 `RDExpression`, 如 `RDRectE.Size`
`RotatedRect``Angle` 不受命名规则 `I` 约束, 其始终为浮点数类型。
`RDExpression` 用以存储节奏医生表达式并尝试解析和求值(没做完), 以字符串创建, 并支持简单的运算。
底层为字符串拼接, 所以当然版本运算会导致表达式嵌套多层括号是正常现象。
```cs
using RhythmBase.RhythmDoctor.Components;
RDExpression exp1 = new("i2+1");
RDExpression exp2 = new(30);
RDExpression exp3 = new("25.5");
RDExpression result = exp1 - exp2 * exp3;
Console.WriteLine(result); // i2+1-765
```
### 创建事件和增删事件
所有事件都直接或间接实现 `IBaseEvent` 接口并继承 `BaseEvent` 抽象类型。
可以将这些接口与抽象类型作为查询扩展方法的泛型参数以筛选事件。
例如,
`BaseRowAction`, `BaseDecorationAction` 分别是轨道事件和精灵事件,
`IRoomEvent` 是拥有多房间属性的事件。
创建事件所使用的节拍参数可以与关卡无关联, 当事件被添加进关卡时会创建与关卡的关联, 而移除关卡时也会切断关联。
若不提供节拍参数则默认为关卡的第 1 拍。
当调用事件的 `ToString()` 方法时, 将会以事件的节拍, 事件的类型, 事件的可显示数据的形式返回一个字符串。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
Comment comment = new() { Beat = new(12), Text = "My_comment." };
Console.WriteLine(comment); // [11,?,?] Comment My_comment.
rdlevel.Add(comment);
Console.WriteLine(comment); // [2,4] Comment My_comment.
rdlevel.Remove(comment);
Console.WriteLine(comment); // [11,?,?] Comment My_comment.
```
特别地,添加、更改、移除 `SetCrotchetsPerBar` 事件时会更新此事件之后的时间线,所以无需担心修改其数值时会影响事件的顺序、排布等;它们会按其自身的绝对节拍固定在相应的位置。关卡也会尝试增加新的 `SetCrotchetsPerBar` 事件或移除相同 `CrotchetsPerBar` 属性的相邻事件以维持其他片段的稳定。
轨道和精灵事件需要在相应的轨道或精灵上调用 `Add()` 进行添加, 而移除可以在轨道, 精灵或关卡的任意一处调用 `Remove()` 方法移除。
重复添加不会有任何效果。
事件类型 `Comment``TintRows` 不受此限制。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RDLevel rdlevel = RDLevel.Default;
MoveRow tr = new();
Console.WriteLine(rdlevel); // "" Count = 3
rdlevel.Add(tint); // "" Count = 3
rdlevel.Rows[0].Add(tr);
Console.WriteLine(rdlevel); // "" Count = 4
rdlevel.Remove(tr);
Console.WriteLine(rdlevel); // "" Count = 3
```
### 自定义事件
如果此程序集没有需要的事件类型,可以继承 `CustomEvent`, `CustomRowEvent``CustomDecorationEvent` 以实现类型。
```cs
using Newtonsoft.RhythmDoctor.Json.Linq;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Components;
// 创建 MyEvent 类型
// 继承自 CustomEvent 类型
public class MyEvent : CustomEvent
{
// 重写属性
public override Tabs Tab => Tabs.Actions;
// 实现的属性都需要和 CustomEvent.Data 字段内的数据绑定和判空。
// 实现一个 RDPointE 类型属性
public RDPointE? MyProperty
{
get
{
// 在 Data 字段内获取所需要的内容并判空
var value = Data["myProperty"];
return value?.ToObject<RDPointE?>() ?? new RDPointE(0, 0);
}
set
{
// 将内容保存在 Data 字段内
Data["myProperty"] =
value.HasValue ?
new JArray(
value?.X ?? null,
value?.Y ?? null) :
null;
}
}
// 在构造函数内初始化类型
public MyEvent()
{
// 初始化 RealType 属性。
ActureType = nameof(MyEvent);
}
}
```
编写好类型后可以像正常事件一样被读写。
值得注意的是,`Type` 仍然是 `EventType.CustomEvent`, 而 `ActureType` 是自定义类型名。
```cs
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Components;
MyEvent myEvent = new();
rdlevel.Add(myEvent);
myEvent.Beat = new(8);
Console.WriteLine(myEvent.Type); // CustomEvent
Console.WriteLine(myEvent.ActureType); // MyEvent
```
另外,如果读取关卡时意外出现未知事件类型,其也会被读取为相应的 `CustomEvent`, `CustomRowEvent``CustomDecorationEvent` 类型事件。
### 事件类型与枚举
事件都拥有属性 `EaseType`, 可以通过 `EventTypeUtils` 内的方法转换得到对应的类型。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Utils;
Console.WriteLine(EventType.Tint.ToType()); // RhythmBase.Events.Tint
Console.WriteLine(EventTypeUtils.ToType("Tint")); // RhythmBase.Events.Tint
Console.WriteLine(EventTypeUtils.ToEnum(typeof(Tint))); // Tint
Console.WriteLine(EventTypeUtils.ToEnum<Tint>()); // Tint
Console.WriteLine(string.Join(", ", EventTypeUtils.ToEnums(typeof(IBarBeginningEvent)))); // PlaySong,SetCrotchetsPerBar, SetHeartExplodeVolume
Console.WriteLine(string.Join(", ", EventTypeUtils.ToEnums<IBarBeginningEvent>())); // PlaySong,SetCrotchetsPerBar, SetHeartExplodeVolume
```
`EventTypeUtils` 也包含一些事件类型的归类,如:
```cs
using RhythmBase.RhythmDoctor.Utils;
Console.WriteLine(string.Join(",\n", EventTypeUtils.DecorationTypes));
// Comment,
// CustomDecorationEvent,
// Move,
// PlayAnimation,
// SetVisible,
// Tile,
// Tint
Console.WriteLine(string.Join(",\n", EventTypeUtils.EventTypeEnumsForCameraFX));
// MoveCamera,
// ShakeScreen,
// FlipScreen,
// PulseCamera
Console.WriteLine(string.Join(",\n", EventTypeUtils.EventTypeEnumsForUtility));
// Comment,
// TagAction,
// CallCustomMethod
```
### 富文本和对话组件
富文本组件位于 `RhythmBase.Components.RichText` 命名空间下,可以通过 `+` 运算组合自定义颜色的富文本。同时支持富文本的序列化和反序列化。
`RDLine<>` 是一个完整的富文本。
`RDPhrase<>` 是富文本的一个样式片段,其遵循单个样式。
使用实现了 `IRDRichStringStyle<>` 的结构体以指明此富文本遵循的样式规则。下例的 `RDRichStringStyle` 即为仅带有颜色样式的富文本。
都可以从 string 隐式转换。注意,转换的文字为不带样式的富文本。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.Global.Components.RichText;
RDLine<RDRichStringStyle> line = RDLine<RDRichStringStyle>.Deserialize("Hel<color=#00FF00>lo");
Console.WriteLine(line.ToString()); // Hello
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo</color>
line +=
new RDPhrase<RDRichStringStyle>(" Rhythm")
{
Style = new()
{
Color = RDColor.Lime
}
};
line += " Doctor!";
Console.WriteLine(line.ToString()); // Hello Rhythm Doctor!
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo Rhythm</color> Doctor!
```
`RDLine<>``RDPhrase<>` 都可以使用索引访问和修改其片段。
```cs
using RhythmBase.Global.Components.RichText;
RDLine<RDRichStringStyle> line = RDLine<RDRichStringStyle>.Deserialize("Hel<color=#00FF00>lo Rhythm</color> Doctor!");
Console.WriteLine(line[6..].ToString()); // Rhythm Doctor!
Console.WriteLine(line[6..].Serialize()); // <color=lime>Rhythm</color> Doctor!
line[5] = " and Welcome to ";
Console.WriteLine(line.ToString()); // Hello and Welcome to Rhythm Doctor!
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo</color> and Welcome to <color=lime>Rhythm</color> Doctor!
return;
```
此包内也提供一整套适配节奏医生对话格式的对话组件,用以模块化构造对话事件的文本内容,减少错误率。
```cs
using RhythmBase.Global.Components.RichText;
RDDialogueExchange exchange =
[
new RDDialogueBlock()
{
Character = "Paige",
Expression = "neutral",
Content = RDLine<RDDialoguePhraseStyle>.Deserialize("Hel<color=#00FF00>lo [2]<shake>Rhythm</color> Doctor</shake>!"),
},
new RDDialogueBlock()
{
Character = "Ian",
Content = "Hello Paige!",
},
new RDDialogueBlock()
{
Character = "Paige",
Expression = "happy",
Content = new RDPhrase<RDDialoguePhraseStyle>("What a good day!")
{
Events =
[
new RDDialogueTone(RDDialogueToneType.VerySlow,6),
new RDDialogueTone(RDDialogueToneType.Static,11),
],
Style = new RDDialoguePhraseStyle()
{
Volume = 0.5f,
Bold = true,
},
}
}
];
Console.WriteLine(exchange.Serialize());
// Paige_neutral:Hel<color=lime>lo [2]<shake>Rhythm</color> Doctor</shake>!
// Ian:Hello Paige!
// Paige_happy:<volume=0.5><bold>What a[vslow] good[static] day!</volume></bold>
```
### 缓动
引入 `RhythmBase.Global.Components.Easing` 之后,可以很方便地使用 `EaseType` 枚举常量。
同时也可以使用扩展方法 `Calculate()` 快速计算过渡值。
```cs
using RhythmBase.Global.Components.Easing;
double var1 = EaseType.InSine.Calculate(0.25);
double var2 = EaseType.Linear.Calculate(0.5,4,9);
Console.WriteLine(var1); // 0.07612046748871326
Console.WriteLine(var2); // 6.5
```
`EaseValue` 是一个简单但强大的结构体。得益于 mfgujhgh 的算法,通过使用静态方法 `Fit()` 处理任意数据,`EaseValue` 可以使用给定的 `EaseType` 选择列表拟合出可被节奏医生事件使用的曲线。
```cs
using RhythmBase.Global.Components.Easing;
// 使用点集和阈值拟合
EaseValue data1 = EaseValue.Fit([
(0, 0),
(1, 1)
], 3f);
// 使用初始值、点集、可选缓动类型列表和阈值拟合
EaseValue data2 = EaseValue.Fit(0, [
(0, 0),
(1, 1)
], [EaseType.Linear, EaseType.InSine], 3f);
// 从缓动数据中获取指定时刻的值
float value = data1.GetValue(2.5f);
```
`RhythmBase.Extensions.EasePropertyExtensions` 为实现了 `IEaseEvent` 的类型添加了一个 `GetEaseProperties` 方法,用于获取这一系列事件的各个属性的缓动曲线。
```cs
using RhythmBase.Global.Components;
using RhythmBase.Global.Components.Easing;
using RhythmBase.Global.Events;
using RhythmBase.RhythmDoctor.Extensions;
var deco = level.Decorations[0];
Move[] moves =
[
new(){ Beat = level.BeatOf(1), Position = new(0, (RDExpression?)null), Duration = 1,Ease = EaseType.Linear, Angle = "2" },
new(){ Beat = level.BeatOf(2.1f), Position = new(10, 90), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(2.2f), Position = new(90, 10), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3), Position = new(10, 70), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.5f), Position = new(10, 10), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.8f), Position = new(30, 50), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.9f), Position = new(20, (RDExpression?)null), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(4.1f), Position = new(70, 20), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(4.4f), Position = new((RDExpression?)null, 0), Duration = 1,Ease = EaseType.Linear },
];
deco.AddRange(moves);
var eases = EasePropertyExtensions.GetEaseProperties(moves);
foreach(var e in eases)
Console.WriteLine(e);
// [Position, RhythmBase.Components.Easing.EasePropertyPoint]
// [Scale, RhythmBase.Components.Easing.EasePropertySize]
// [Angle, RhythmBase.Components.Easing.EasePropertyFloat]
// [Pivot, RhythmBase.Components.Easing.EasePropertyPoint]
```
实现 `IEaseProperty<>` 接口的类型,例如 `EasePropertyColor``EasePropertyFloat``EasePropertyPoint``EasePropertySize`,用于存储来自缓动事件列表的内容。
同样也可以获取相应时刻的值。
> 在调用之前请检查属性的值类型,并应用相应的类型转换。
```cs
using RhythmBase.Global.Components.Easing;
var result = ((EasePropertyPoint)eases["Position"]).GetValue(rdlevel.BeatOf(3.2f));
Console.WriteLine(result); // [59.759995, 21.840006]
```
### 表达式(重写中)
`RhythmBase.Components.RDLang.RDLang` 提供一个 `TryRun()` 方法,用于运行节奏医生表达式。
> 注意,若表达式不正确则会返回 `false`,同时结果为 `0`
`RDLang` 同时有一个静态字段 `Variables`,用于存储所有常用变量与方法。在执行 `TryRun` 之前修改字段会影响执行时的值。
`RDLang` 也支持三个常用方法 `Rand()``atLeastRank()``atLeastNPerfects()`,这些方法都可以在 `RDVariables` 上正常访问。
```cs
using RhythmBase.RhythmDoctor.Components.RDLang;
RDLang.Variables.i[1] = 9;
RDLang.TryRun("numMistakesP2 = 3", out float result); // 3
RDLang.TryRun("numMistakesP2+i1", out result); // 12
RDLang.TryRun("atLeastRank(A)", out result); // 1
```
由于此库不支持动态播放关卡,可使用以下字段对后两个函数的效果进行模拟:
- `atLeastRank()`
使用 `RDVariables.SimulateCurrentRank` 属性更改模拟关卡评级状态。
当表达式访问 `atLeastRank()` 方法时使用此值进行模拟。
- `atLeastNPerfects()`
使用 `RDVariables.SimulateAtLeastNPerfectsSuccessRate` 属性更改模拟成功击拍百分比。
当表达式访问 `atLeastNPerfects()` 方法时使用此值进行模拟。
### 事件组
像一个新事件一样声明并使用它,它会按照您指定的方式生成关卡事件!
像下面这样继承 `Group` 类,实现新的逻辑后,您可以像其他事件一样自由地操纵它。
这与 `CustomEvent` 不同,它会用指定的事件序列写入关卡,而不是它本身。
```cs
using Newtonsoft.Json.Linq;
using RhythmBase.RhythmDoctor.Events;
public class GroupData1
{
public RDSize Size;
public int RowIndex;
}
public class MoveCameraRectangle : Group<GroupData1>
{
public RDSize Size
{
get => Data.Size;
set => Data.Size = value;
}
public Row Row
{
get => Rows?[Data.RowIndex] ?? [];
set => Data.RowIndex = value.Index;
}
public MoveCameraRectangle() { }
public override IEnumerable<BaseEvent> GenerateEvents()
{
yield return new MoveCamera() { Beat = new(1), Rooms = new(true, 0), CameraPosition = new(50 - Size.Width / 2, 50 - Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(2), Rooms = new(true, 0), CameraPosition = new(50 + Size.Width / 2, 50 - Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(3), Rooms = new(true, 0), CameraPosition = new(50 + Size.Width / 2, 50 + Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(4), Rooms = new(true, 0), CameraPosition = new(50 - Size.Width / 2, 50 + Size.Height / 2), Duration = 1 };
yield return SetParent(new MoveRow() { Beat = new(1), RowPosition = new(50, 50), CustomPosition = true, Duration = 0 }, Row);
yield return SetParent(new MoveRow() { Beat = new(1.001f), RowPosition = new(50 - Size.Width / 2, 50 - Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(2), RowPosition = new(50 + Size.Width / 2, 50 - Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(3), RowPosition = new(50 + Size.Width / 2, 50 + Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(4), RowPosition = new(50 - Size.Width / 2, 50 + Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
}
}
```
值得注意的是,这样的操作会比较消耗资源。所以 `LevelReadOrWriteSettings` 会默认禁用这个选项。
其底层逻辑大致为,它会为事件序列中的每个事件附加一个特殊的标签以标记它是“被生成”的,以供下次读取事件时清理这些被生成的事件。对于其中已有标签的事件,它会有一层额外逻辑以封装这些事件。
```cs
using RhythmBase.Global.Settings;
using RhythmBase.RhythmDoctor.Components;
Console.WriteLine(level);
LevelReadOrWriteSettings settings = new()
{
EnableGroupEvent = true,
InactiveEventsHandling = InactiveEventsHandling.Retain,
Indented = true
};
using RDLevel level = RDLevel.Default;
level.Decorations.Add(new Decoration() { Room = RDRoomIndex.Room1 });
var re1 = new MoveCameraRectangle() { Beat = new(4), Size = new RDSize(80, 80) };
var re2 = new MoveCameraRectangle() { Beat = new(9), Y = 2, Size = new RDSize(20, 20) };
level.Add(re1);
level.Add(re2);
level.Write(dstPath, settings);
// 会生成这些事件:
// {"bar":1,"beat":1,"type":"MoveCamera","rooms":[0],"cameraPosition":[10,10],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[50,50],"duration":0,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1.001,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[10,10],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":2,"type":"MoveCamera","rooms":[0],"cameraPosition":[90,10],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":2,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[90,10],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":3,"type":"MoveCamera","rooms":[0],"cameraPosition":[90,90],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":3,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[90,90],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":4,"type":"MoveCamera","rooms":[0],"cameraPosition":[10,90],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":4,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[10,90],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1,"type":"MoveCamera","rooms":[0],"cameraPosition":[40,40],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[50,50],"duration":0,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1.001,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[40,40],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":2,"type":"MoveCamera","rooms":[0],"cameraPosition":[60,40],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":2,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[60,40],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":3,"type":"MoveCamera","rooms":[0],"cameraPosition":[60,60],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":3,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[60,60],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":4,"type":"MoveCamera","rooms":[0],"cameraPosition":[40,60],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":4,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[40,60],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1,"type":"Comment","tab":"Song","show":false,"text":"$RhythmBase_GroupData$\r\n/* Generated by RhythmBase */\r\n@MoveCameraRectangle\r\n@MoveCameraRectangle2\r\n@RhythmBase.RhythmDoctor.Events.Group\r\n@RhythmBase.RhythmDoctor.Events.Group`1\r\n{\"size\":[80.0,80.0],\"rowIndex\":0}\r\n{\"size\":[20.0,20.0],\"rowIndex\":0}\r\n","color":"F2E644","y":-1},
// {"bar":1,"beat":4,"type":"TagAction","Tag":"$RhythmBase_GroupEvent$0000000000000000","y":0,"tag":"","Action":"Run"},
// {"bar":2,"beat":1,"type":"TagAction","Tag":"$RhythmBase_GroupEvent$0000000000000001","y":2,"tag":"","Action":"Run"},
```
## 案例
### 合并采音关卡与视效关卡
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Extensions;
// 读取视效关卡文件
using RDLevel vfxLevel = RDLevel.Read(@"vfx.rdlevel");
// 读取采音关卡文件
using RDLevel audioLevel = RDLevel.Read(@"beat.rdlevel");
// 移除视效关卡内所有轨道
RowEventCollection[] vfxrows = [.. vfxLevel.Rows];
foreach (var row in vfxrows)
vfxLevel.Rows.Remove(row);
// 复制采音关卡所有轨道的内容到新的关卡内
foreach (var row in audioLevel.Rows)
{
// 复制轨道信息
Row row2 = new()
{
Rooms = row.Rooms,
Character = row.Character,
Sound = row.Sound,
RowType = row.RowType
};
vfxLevel.Rows.Add(row2);
// 复制轨道内的事件
BaseBeat[] evts = [.. row.Where<BaseBeat>()];
foreach (var evt in evts)
row2.Add(evt);
i++;
}
// 复制需要的音效事件
foreach (var sound in audioLevel.Where(e =>
e.Tab == Tabs.Sounds && // 事件位于音效栏中
e is not BaseRowAction && // 音效事件内包含轨道事件,如果在这里添加轨道事件会导致引用出错
e is not PlaySong && // 播放音乐事件相同的情况下不需要复制
e is not SetCrotchetsPerBar)) // 复制事件的时间计算是无关四分音符数的,所以可以不用添加
{
vfxLevel.Add(sound);
}
// 写入到新的关卡文件中
vfxLevel.Write(@"result.rdlevel");
```

763
pages/3rdparty_rdtk_EN.md Normal file
View File

@ -0,0 +1,763 @@
[Repository](https://github.com/RDCN-Community-Developers/RhythmToolkit)
English | [中文](/pages/3rdparty_rdtk.md)
# RhythmBase Tutorial
This project serves Rhythm Doctor level developers, aiming to provide a more systematic and intuitive level editing medium for developers.
Thanks to the Rhythm Doctor fan community for their support of this project.
Welcome to the RhythmBase tutorial. This guide will help you get started with setting up and using RhythmBase in your project.
## Install NuGet Package
To install the RhythmBase NuGet package, follow these steps:
1. Open Visual Studio or use the command line tools.
2. In Visual Studio, go to **Tools** > **NuGet Package Manager** > **Package Manager Console**.
3. In the console, enter the following command:
```
Install-Package RhythmBase -Version 1.2.0-rc2
```
4. Wait for the installation to complete and ensure your project references the required NuGet package.
5. To install using the .NET CLI, use:
```
dotnet add package RhythmBase -version 1.2.0-rc2
```
## Coding
### Creating a Level
A level is a collection of events.
```cs
using RhythmBase.RhythmDoctor.Components;
using RDLevel emptyLevel = [];
Console.WriteLine(emptyLevel); // "" Count = 0
```
You can also use the built-in template to create a level with basic events.
This template is the same as the default level template created by the Rhythm Doctor Level Editor.
```cs
using RhythmBase.RhythmDoctor.Components;
using RDLevel defaultLevel = RDLevel.Default;
Console.WriteLine(defaultLevel); // "" Count = 3
```
### Reading and Writing
You can directly import and export files using file paths. The default read/write settings will be used.
Exporting does not package the level as a `.rdzip` file.
```cs
using RhythmBase.RhythmDoctor.Components;
// Directly read a level file
using RDLevel rdlevel1 = RDLevel.Read(@"your\level.rdlevel");
// Read a level pack file
using RDLevel rdlevel2 = RDLevel.Read(@"your\level.rdzip");
// Read a compressed level pack
using RDLevel rdlevel3 = RDLevel.Read(@"your\level.zip");
// Write a level file
rdlevel1.Write(@"your\outLevel.rdlevel");
```
You can add custom read/write settings with `LevelReadOrWriteSettings` when reading or writing levels.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Settings;
// Create custom read/write settings
LevelReadOrWriteSettings settings = new()
{
// Handling of inactive events
InactiveEventsHandling = InactiveEventsHandling.Store,
// Handling of unreadable events
// Common when sprite events are not bound to sprite tracks, etc.
UnreadableEventsHandling = UnreadableEventHandling.Store,
// Enable indentation
Indented = true,
};
using RDLevel rdlevel1 = RDLevel.Read(@"your\level.rdlevel", settings);
```
You can also generate a JSON object or JSON string for further operations.
```cs
using Newtonsoft.Json.Linq;
using RhythmBase.RhythmDoctor.Components;
JObject jobject = rdlevel.ToJObject();
string json = rdlevel.ToRDLevelJson(settings);
```
`LevelReadOrWriteSettings` provides `BeforeReading`, `AfterReading`, `BeforeWriting`, and `AfterWriting` events, which are triggered before/after reading or writing a level.
You can add listeners to these events for custom behaviors.
```cs
using RhythmBase.RhythmDoctor.Settings;
settings.AfterWriting += Settings_AfterReading;
// This will be triggered after writing is finished
void Settings_AfterReading(object? sender, EventArgs e)
{
throw new NotImplementedException();
}
rdlevel.Write(@"your\outLevel.rdlevel", settings);
```
> When reading compressed level pack files, please use the `using` statement or actively call the `RDLevel.Dispose()` method to ensure that temporary files extracted during decompression are properly cleaned up.
### Finding and Retrieving Events
The `OrderedEventCollection` type is used to store collections of events, and `RDLevel` inherits from this type.
You can use extension methods designed for querying Rhythm Doctor level events to simplify your queries.
For example, you can filter by event type and its base types, interfaces implemented by events, beat ranges, custom predicates, and more.
Methods such as `AddRange()`, `RemoveRange()`, `Where()`, `RemoveAll()`, `First()`, `FirstOrDefault()`, `Last()`, `LastOrDefault()`, and `TakeWhile()` are provided.
It is recommended to use these optimized methods for better performance.
```cs
using RhythmBase.RhythmDoctor.Extensions;
using RhythmBase.RhythmDoctor.Components;
// Find MoveRow events between measures 3 and 5, and in event rows 0 to 2
var list = rdlevel.Where<MoveRow>(
i => 0 <= i.Y && i.Y < 3, // In event rows 0 to 2
3..5 // From bar 3 to 5
);
```
`Row` and `Decoration` also inherit from `OrderedEventCollection`, so rows and decorations also support these extension methods.
```cs
using RhythmBase.RhythmDoctor.Extensions;
using RhythmBase.RhythmDoctor.Components;
// Find the AddClassicBeat event in the decoration between beat (11,1) and (13,1)
var list = rdlevel.Decorations[0].Where<Tint>(
new Beat(11, 1), // Start searching from bar 11, beat 1
new Beat(13, 1) // End searching at bar 13, beat 1
);
```
### Creating a Beat
`RDBeat` is a struct that stores three pieces of information: `BeatOnly`, `BarBeat`, and `TimeSpan`.
You can create a `RDBeat` instance that is not associated with a level, but due to the lack of a level context, its functionality may be limited.
You can check its `IsEmpty` property to determine whether the instance is valid.
When not associated with a level, calling its `ToString()` method will display the information it contains and indicate any missing information.
```cs
using RhythmBase.RhythmDoctor.Components;
// Create a beat not associated with a level
RDBeat beat1 = new(11);
RDBeat beat2 = new(2, 3);
RDBeat beat3 = new(TimeSpan.FromSeconds(11.45));
Console.WriteLine(beat1); // [10,?,?]
Console.WriteLine(beat2); // [?,(2, 3),?]
Console.WriteLine(beat3); // [?,?,00:00:11.4500000]
```
You can create a beat associated with a level using a `BeatCalculator` instance or an `RDLevel` instance.
A `BeatCalculator` is created along with an `RDLevel` and can be accessed via `RDLevel.Calculator`.
When associated with a level, calling its `ToString()` method will display the `BarBeat` property.
Only when associated with a level can all three properties be linked, and the beat can participate in all operations. Otherwise, only the properties with data can be used in calculations.
The beat properties of events and bookmarks within a level are all associated with the level, while events removed from the level will lose this association.
```cs
using RhythmBase.RhythmDoctor.Components;
// Create a beat associated with a level
RDBeat beat1 = rdlevel.BeatOf(11);
RDBeat beat2 = rdlevel.Calculator.BeatOf(2, 3);
RDBeat beat3 = beat1 - 10 + TimeSpan.FromSeconds(11.45);
Console.WriteLine(beat1); // [2,3]
Console.WriteLine(beat2); // [2,3]
Console.WriteLine(beat3); // [3,4.083334]
```
When performing operations between beats, if both are linked to a level, you must ensure they refer to the same level.
You can call the `FromSameLevel()` or `FromSameLevelOrNull()` methods to check if they refer to the same level.
You can call `WithoutLink()` to return a new beat instance detached from any level.
```cs
using RhythmBase.RhythmDoctor.Components;
RDBeat beat1 = rdlevel.BeatOf(1);
RDBeat beat2 = beat1.WithoutLink();
Console.WriteLine(beat1.FromSameLevel(beat2)); // False
Console.WriteLine(beat1.FromSameLevelOrNull(beat2)); // True
```
`BeatCalculator` also provides methods for time conversion, allowing you to convert between different time units.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Utils;
(float, float) barbeat = rdlevel.Calculator.TimeSpanToBarBeat(TimeSpan.FromSeconds(19.19)); // (4, 8.983334)
```
`RDLevel` provides a default beat associated with the instance, with a beat count of 1.
```cs
using RhythmBase.RhythmDoctor.Components;
RDBeat @default = RDLevel.DefaultBeat;
```
`RDRange` is a data type similar to `Range`, used to represent a range of beats.
It is commonly used for querying events.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Extension;
var result = rdlevel.Where(new RDRange(rdlevel.DefaultBeat + 10, null));
```
### Extended Data Types
Types in RhythmBase that start with `RD` and contain names like `Point`, `Size`, `Rect`, or `RotatedRect` are data types related to planar geometry.
Types ending with `I` are integer types, where all data properties are of type `int`, such as `RDPointI.X`.
Types ending with `N` are non-nullable types, where all data properties are non-nullable, such as `RDSizeN.Height`.
Types ending with `E` are expression types, where all data properties are of type `RDExpression`, such as `RDRectE.Size`.
The `Angle` property of `RotatedRect` is always a floating-point type and is not affected by the `I` naming convention.
`RDExpression` is used to store Rhythm Doctor expressions and attempts to parse and evaluate them (not fully implemented yet).
It is created from a string and supports simple operations.
The underlying implementation is string concatenation, so it is normal for nested parentheses to appear when performing multiple operations.
```cs
using RhythmBase.RhythmDoctor.Components;
RDExpression exp1 = new("i2+1");
RDExpression exp2 = new(30);
RDExpression exp3 = new("25.5");
RDExpression result = exp1 - exp2 * exp3;
Console.WriteLine(result.ExpressionValue); // i2+1-765
```
### Creating and Modifying Events
All events directly or indirectly implement the `IBaseEvent` interface and inherit from the `BaseEvent` abstract type.
You can use these interfaces and abstract types as generic parameters for query extension methods to filter events.
For example,
`BaseRowAction` and `BaseDecorationAction` are track events and sprite events, respectively,
while `IRoomEvent` represents events with multi-room properties.
The beat parameter used when creating an event can be unbound from any level; when the event is added to a level, it will be associated with that level, and when removed, the association will be broken.
If no beat parameter is provided, it defaults to the first beat of the level.
When calling the event's `ToString()` method, it will return a string in the format of the event's beat, event type, and displayable data.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
Comment comment = new() { Beat = new(12), Text = "My_comment." };
Console.WriteLine(comment); // [11,?,?] Comment My_comment.
rdlevel.Add(comment);
Console.WriteLine(comment); // [2,4] Comment My_comment.
rdlevel.Remove(comment);
Console.WriteLine(comment); // [11,?,?] Comment My_comment.
```
In particular, adding, modifying, or removing a `SetCrotchetsPerBar` event will update the timeline after this event, so you don't need to worry about changes affecting the order or arrangement of events; they will remain fixed at their absolute beat positions. The level will also attempt to add new `SetCrotchetsPerBar` events or remove adjacent events with the same `CrotchetsPerBar` property to maintain the stability of other segments.
Row and decoration events need to be added using `Add()` on the corresponding row or decoration, while removal can be done from the row, decoration, or level using the `Remove()` method.
Repeated additions have no effect.
The event types `Comment` and `TintRows` are not subject to this restriction.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RDLevel rdlevel = RDLevel.Default;
MoveRow tr = new();
Console.WriteLine(rdlevel); // "" Count = 3
rdlevel.Add(tint); // "" Count = 3
rdlevel.Rows[0].Add(tr);
Console.WriteLine(rdlevel); // "" Count = 4
rdlevel.Remove(tr);
Console.WriteLine(rdlevel); // "" Count = 3
```
### Custom Events
If the required event type is not available in this assembly, you can implement your own by inheriting from `CustomEvent`, `CustomRowEvent`, or `CustomDecorationEvent`.
```cs
using Newtonsoft.RhythmDoctor.Json.Linq;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Components;
// Create a MyEvent type
// Inherit from CustomEvent
public class MyEvent : CustomEvent
{
// Override property
public override Tabs Tab => Tabs.Actions;
// All implemented properties need to be bound to and checked for null in the CustomEvent.Data field.
// Implement an RDPointE type property
public RDPointE? MyProperty
{
get
{
// Get the required content from the Data field and check for null
var value = Data["myProperty"];
return value?.ToObject<RDPointE?>() ?? new RDPointE(0, 0);
}
set
{
// Save the content in the Data field
Data["myProperty"] =
value.HasValue ?
new JArray(
value?.X ?? null,
value?.Y ?? null) :
null;
}
}
// Initialize the type in the constructor
public MyEvent()
{
// Initialize the ActureType property.
ActureType = nameof(MyEvent);
}
}
```
After writing your type, it can be read and written like a normal event.
Note that `Type` is still `EventType.CustomEvent`, while `ActureType` is the custom type name.
```cs
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Components;
MyEvent myEvent = new();
rdlevel.Add(myEvent);
myEvent.Beat = new(8);
Console.WriteLine(myEvent.Type); // CustomEvent
Console.WriteLine(myEvent.ActureType); // MyEvent
```
Additionally, if an unknown event type is encountered when reading a level, it will also be read as the corresponding `CustomEvent`, `CustomRowEvent`, or `CustomDecorationEvent` type event.
### Event Types and Enums
All events have the `EaseType` property, and you can use methods in `EventTypeUtils` to convert and obtain the corresponding type.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Utils;
Console.WriteLine(EventType.Tint.ToType()); // RhythmBase.Events.Tint
Console.WriteLine(EventTypeUtils.ToType("Tint")); // RhythmBase.Events.Tint
Console.WriteLine(EventTypeUtils.ToEnum(typeof(Tint))); // Tint
Console.WriteLine(EventTypeUtils.ToEnum<Tint>()); // Tint
Console.WriteLine(string.Join(", ", EventTypeUtils.ToEnums(typeof(IBarBeginningEvent)))); // PlaySong,SetCrotchetsPerBar, SetHeartExplodeVolume
Console.WriteLine(string.Join(", ", EventTypeUtils.ToEnums<IBarBeginningEvent>())); // PlaySong,SetCrotchetsPerBar, SetHeartExplodeVolume
```
`EventTypeUtils` also includes some event type classifications, such as:
```cs
using RhythmBase.RhythmDoctor.Utils;
Console.WriteLine(string.Join(",\n", EventTypeUtils.DecorationTypes));
// Comment,
// CustomDecorationEvent,
// Move,
// PlayAnimation,
// SetVisible,
// Tile,
// Tint
Console.WriteLine(string.Join(",\n", EventTypeUtils.EventTypeEnumsForCameraFX));
// MoveCamera,
// ShakeScreen,
// FlipScreen,
// PulseCamera
Console.WriteLine(string.Join(",\n", EventTypeUtils.EventTypeEnumsForUtility));
// Comment,
// TagAction,
// CallCustomMethod
```
### Rich Text and Dialogue Components
The rich text components are located in the `RhythmBase.Components.RichText` namespace. You can use the `+` operator to combine rich text with custom colors. Serialization and deserialization of rich text are also supported.
`RDLine<>` represents a complete rich text line.
`RDPhrase<>` is a style fragment of rich text, which follows a single style.
You can use a struct that implements `IRDRichStringStyle<>` to specify the style rules for the rich text. In the example below, `RDRichStringStyle` is a rich text style that only includes color.
All can be implicitly converted from a string. Note that the converted text will be plain rich text without any style.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.Global.Components.RichText;
RDLine<RDRichStringStyle> line = RDLine<RDRichStringStyle>.Deserialize("Hel<color=#00FF00>lo");
Console.WriteLine(line.ToString()); // Hello
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo</color>
line +=
new RDPhrase<RDRichStringStyle>(" Rhythm")
{
Style = new()
{
Color = RDColor.Lime
}
};
line += " Doctor!";
Console.WriteLine(line.ToString()); // Hello Rhythm Doctor!
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo Rhythm</color> Doctor!
```
Both `RDLine<>` and `RDPhrase<>` support index access and modification of their fragments.
```cs
using RhythmBase.Global.Components.RichText;
RDLine<RDRichStringStyle> line = RDLine<RDRichStringStyle>.Deserialize("Hel<color=#00FF00>lo Rhythm</color> Doctor!");
Console.WriteLine(line[6..].ToString()); // Rhythm Doctor!
Console.WriteLine(line[6..].Serialize()); // <color=lime>Rhythm</color> Doctor!
line[5] = " and Welcome to ";
Console.WriteLine(line.ToString()); // Hello and Welcome to Rhythm Doctor!
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo</color> and Welcome to <color=lime>Rhythm</color> Doctor!
return;
```
This package also provides a complete set of dialogue components adapted to the Rhythm Doctor dialogue format, allowing you to modularly construct the text content of dialogue events and reduce error rates.
```cs
using RhythmBase.Global.Components.RichText;
RDDialogueExchange exchange =
[
new RDDialogueBlock()
{
Character = "Paige",
Expression = "neutral",
Content = RDLine<RDDialoguePhraseStyle>.Deserialize("Hel<color=#00FF00>lo [2]<shake>Rhythm</color> Doctor</shake>!"),
},
new RDDialogueBlock()
{
Character = "Ian",
Content = "Hello Paige!",
},
new RDDialogueBlock()
{
Character = "Paige",
Expression = "happy",
Content = new RDPhrase<RDDialoguePhraseStyle>("What a good day!")
{
Events =
[
new RDDialogueTone(RDDialogueToneType.VerySlow,6),
new RDDialogueTone(RDDialogueToneType.Static,11),
],
Style = new RDDialoguePhraseStyle()
{
Volume = 0.5f,
Bold = true,
},
}
}
];
Console.WriteLine(exchange.Serialize());
// Paige_neutral:Hel<color=lime>lo [2]<shake>Rhythm</color> Doctor</shake>!
// Ian:Hello Paige!
// Paige_happy:<volume=0.5><bold>What a[vslow] good[static] day!</volume></bold>
```
### Easing
After importing `RhythmBase.Global.Components.Easing`, you can easily use the `EaseType` enum constants.
You can also use the extension method `Calculate()` to quickly compute eased values.
```cs
using RhythmBase.Global.Components.Easing;
double var1 = EaseType.InSine.Calculate(0.25);
double var2 = EaseType.Linear.Calculate(0.5, 4, 9);
Console.WriteLine(var1); // 0.07612046748871326
Console.WriteLine(var2); // 6.5
```
`EaseValue` is a simple yet powerful struct. Thanks to mfgujhgh's algorithm, by using the static method `Fit()` to process arbitrary data, `EaseValue` can fit a curve usable by Rhythm Doctor events with a given selection of `EaseType`.
```cs
using RhythmBase.Global.Components.Easing;
// Fit using a set of points and a threshold
EaseValue data1 = EaseValue.Fit([
(0, 0),
(1, 1)
], 3f);
// Fit using an initial value, a set of points, an optional list of ease types, and a threshold
EaseValue data2 = EaseValue.Fit(0, [
(0, 0),
(1, 1)
], [EaseType.Linear, EaseType.InSine], 3f);
// Get the value at a specific time from the easing data
float value = data1.GetValue(2.5f);
```
`RhythmBase.Extensions.EasePropertyExtensions` adds a `GetEaseProperties` method to types that implement `IEaseEvent`, which is used to obtain the easing curves for each property in a series of events.
```cs
using RhythmBase.Global.Components;
using RhythmBase.Global.Components.Easing;
using RhythmBase.Global.Events;
using RhythmBase.RhythmDoctor.Extensions;
var deco = level.Decorations[0];
Move[] moves =
[
new(){ Beat = level.BeatOf(1), Position = new(0, (RDExpression?)null), Duration = 1, Ease = EaseType.Linear, Angle = "2" },
new(){ Beat = level.BeatOf(2.1f), Position = new(10, 90), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(2.2f), Position = new(90, 10), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3), Position = new(10, 70), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.5f), Position = new(10, 10), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.8f), Position = new(30, 50), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.9f), Position = new(20, (RDExpression?)null), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(4.1f), Position = new(70, 20), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(4.4f), Position = new((RDExpression?)null, 0), Duration = 1, Ease = EaseType.Linear },
];
deco.AddRange(moves);
var eases = EasePropertyExtensions.GetEaseProperties(moves);
foreach(var e in eases)
Console.WriteLine(e);
// [Position, RhythmBase.Components.Easing.EasePropertyPoint]
// [Scale, RhythmBase.Components.Easing.EasePropertySize]
// [Angle, RhythmBase.Components.Easing.EasePropertyFloat]
// [Pivot, RhythmBase.Components.Easing.EasePropertyPoint]
```
Types that implement the `IEaseProperty<>` interface, such as `EasePropertyColor`, `EasePropertyFloat`, `EasePropertyPoint`, and `EasePropertySize`, are used to store the content from a list of easing events.
You can also get the value at a specific time.
> Before calling, please check the value type of the property and apply the appropriate type conversion.
```cs
using RhythmBase.Global.Components.Easing;
var result = ((EasePropertyPoint)eases["Position"]).GetValue(rdlevel.BeatOf(3.2f));
Console.WriteLine(result); // [59.759995, 21.840006]
```
### RDCode (Under Revision)
`RhythmBase.Components.RDLang.RDLang` provides a `TryRun()` method for evaluating Rhythm Doctor expressions.
> Note: If the expression is incorrect, it will return `false` and the result will be `0`.
`RDLang` also has a static field `Variables` for storing all commonly used variables and methods. Modifying this field before executing `TryRun` will affect the values during execution.
`RDLang` also supports three common methods: `Rand()`, `atLeastRank()`, and `atLeastNPerfects()`. These methods can also be accessed via `RDVariables`.
```cs
using RhythmBase.RhythmDoctor.Components.RDLang;
RDLang.Variables.i[1] = 9;
RDLang.TryRun("numMistakesP2 = 3", out float result); // 3
RDLang.TryRun("numMistakesP2+i1", out result); // 12
RDLang.TryRun("atLeastRank(A)", out result); // 1
```
Since this library does not support dynamic level playback, you can use the following fields to simulate the effects of the last two functions:
- `atLeastRank()`
Use the `RDVariables.SimulateCurrentRank` property to change the simulated level rank state.
When the expression accesses the `atLeastRank()` method, this value will be used for simulation.
- `atLeastNPerfects()`
Use the `RDVariables.SimulateAtLeastNPerfectsSuccessRate` property to change the simulated percentage of perfect hits.
When the expression accesses the `atLeastNPerfects()` method, this value will be used for simulation.
### Event Groups
Declare and use it like a new event, and it will generate level events according to your specifications!
By inheriting from the `Group` class and implementing new logic as shown below, you can freely manipulate it just like other events.
Unlike `CustomEvent`, it writes the specified sequence of events into the level instead of itself.
```cs
using Newtonsoft.Json.Linq;
using RhythmBase.RhythmDoctor.Events;
public class GroupData1
{
public RDSize Size;
public int RowIndex;
}
public class MoveCameraRectangle : Group<GroupData1>
{
public RDSize Size
{
get => Data.Size;
set => Data.Size = value;
}
public Row Row
{
get => Rows?[Data.RowIndex] ?? [];
set => Data.RowIndex = value.Index;
}
public MoveCameraRectangle() { }
public override IEnumerable<BaseEvent> GenerateEvents()
{
yield return new MoveCamera() { Beat = new(1), Rooms = new(true, 0), CameraPosition = new(50 - Size.Width / 2, 50 - Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(2), Rooms = new(true, 0), CameraPosition = new(50 + Size.Width / 2, 50 - Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(3), Rooms = new(true, 0), CameraPosition = new(50 + Size.Width / 2, 50 + Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(4), Rooms = new(true, 0), CameraPosition = new(50 - Size.Width / 2, 50 + Size.Height / 2), Duration = 1 };
yield return SetParent(new MoveRow() { Beat = new(1), RowPosition = new(50, 50), CustomPosition = true, Duration = 0 }, Row);
yield return SetParent(new MoveRow() { Beat = new(1.001f), RowPosition = new(50 - Size.Width / 2, 50 - Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(2), RowPosition = new(50 + Size.Width / 2, 50 - Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(3), RowPosition = new(50 + Size.Width / 2, 50 + Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(4), RowPosition = new(50 - Size.Width / 2, 50 + Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
}
}
```
Note that this operation can be resource-intensive, so `LevelReadOrWriteSettings` disables this option by default.
The underlying logic is that it attaches a special tag to each event in the sequence to mark it as "generated", so that these generated events can be cleaned up the next time events are read. For events that already have tags, there is an extra layer of logic to encapsulate them.
```cs
using RhythmBase.Global.Settings;
using RhythmBase.RhythmDoctor.Components;
LevelReadOrWriteSettings settings = new()
{
EnableGroupEvent = true,
InactiveEventsHandling = InactiveEventsHandling.Retain,
Indented = true
};
using RDLevel level = RDLevel.Default;
level.Decorations.Add(new Decoration() { Room = RDRoomIndex.Room1 });
var re1 = new MoveCameraRectangle() { Beat = new(4), Size = new RDSize(80, 80) };
var re2 = new MoveCameraRectangle() { Beat = new(9), Y = 2, Size = new RDSize(20, 20) };
level.Add(re1);
level.Add(re2);
level.Write(dstPath, settings);
// The following events will be generated:
// {"bar":1,"beat":1,"type":"MoveCamera","rooms":[0],"cameraPosition":[10,10],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[50,50],"duration":0,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1.001,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[10,10],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":2,"type":"MoveCamera","rooms":[0],"cameraPosition":[90,10],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":2,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[90,10],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":3,"type":"MoveCamera","rooms":[0],"cameraPosition":[90,90],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":3,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[90,90],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":4,"type":"MoveCamera","rooms":[0],"cameraPosition":[10,90],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":4,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[10,90],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1,"type":"MoveCamera","rooms":[0],"cameraPosition":[40,40],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[50,50],"duration":0,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1.001,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[40,40],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":2,"type":"MoveCamera","rooms":[0],"cameraPosition":[60,40],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":2,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[60,40],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":3,"type":"MoveCamera","rooms":[0],"cameraPosition":[60,60],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":3,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[60,60],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":4,"type":"MoveCamera","rooms":[0],"cameraPosition":[40,60],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":4,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[40,60],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1,"type":"Comment","tab":"Song","show":false,"text":"$RhythmBase_GroupData$\r\n/* Generated by RhythmBase */\r\n@MoveCameraRectangle\r\n@MoveCameraRectangle2\r\n@RhythmBase.RhythmDoctor.Events.Group\r\n@RhythmBase.RhythmDoctor.Events.Group`1\r\n{\"size\":[80.0,80.0],\"rowIndex\":0}\r\n{\"size\":[20.0,20.0],\"rowIndex\":0}\r\n","color":"F2E644","y":-1},
// {"bar":1,"beat":4,"type":"TagAction","Tag":"$RhythmBase_GroupEvent$0000000000000000","y":0,"tag":"","Action":"Run"},
// {"bar":2,"beat":1,"type":"TagAction","Tag":"$RhythmBase_GroupEvent$0000000000000001","y":2,"tag":"","Action":"Run"},
```
## Examples
### Merging Audio and Visual Levels
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Extensions;
// Read the visual effects level file
using RDLevel vfxLevel = RDLevel.Read(@"vfx.rdlevel");
// Read the audio level file
using RDLevel audioLevel = RDLevel.Read(@"beat.rdlevel");
// Remove all rows from the visual effects level
RowEventCollection[] vfxrows = [.. vfxLevel.Rows];
foreach (var row in vfxrows)
vfxLevel.Rows.Remove(row);
// Copy all rows from the audio level into the new level
foreach (var row in audioLevel.Rows)
{
// Copy row information
Row row2 = new()
{
Rooms = row.Rooms,
Character = row.Character,
Sound = row.Sound,
RowType = row.RowType
};
vfxLevel.Rows.Add(row2);
// Copy events within the row
BaseBeat[] evts = [.. row.Where<BaseBeat>()];
foreach (var evt in evts)
row2.Add(evt);
}
// Copy necessary sound events
foreach (var sound in audioLevel.Where(e =>
e.Tab == Tabs.Sounds && // Event is in the Sounds tab
e is not BaseRowAction && // Sound events contain row events; adding row events here would cause reference errors
e is not PlaySong && // No need to copy PlaySong if the music is the same
e is not SetCrotchetsPerBar)) // The timing of these events is independent of the number of crotchets per bar, so they don't need to be added
{
vfxLevel.Add(sound);
}
// Write to a new level file
vfxLevel.Write(@"result.rdlevel");
```

View File

@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DQUJxb0puSFFOVWpz)
<iframe single src="https://docs.qq.com/sheet/DQUJxb0puSFFOVWpz"></iframe>

View File

@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DUGpSTXFZQkNGWWdn)
<iframe single src="https://docs.qq.com/sheet/DUGpSTXFZQkNGWWdn"></iframe>

View File

@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DZU1LTmJGVkhhQ2Vp)
<iframe single src="https://docs.qq.com/sheet/DZU1LTmJGVkhhQ2Vp"></iframe>

View File

@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DZGlHSExJSllSZ0tI)
<iframe single src="https://docs.qq.com/sheet/DZGlHSExJSllSZ0tI"></iframe>

View File

@ -1,5 +1,11 @@
# 更新信息
#### 2025-07-19
- 补充了目录图标
- 整理并添加了新的板块
- 为嵌入页面添加了跳转外链功能
- 更换了插图
#### 2025-06-28
- 添加了~~吐槽~~样式
- 修复了批量替换遗留问题

View File

@ -207,7 +207,7 @@ g 1 3 3 1 #df0{
考虑到还是会有一些情况下事件并非都是重复的,而是零零散散真的有一大堆。这种情况下把事件摆放清楚会在作谱的过程中事倍功半。
**1、**每一行放些什么事件最好能由明确的区分,方便后期梳理自己设计了一些什么。比如把贯穿全程的浮动文字歌词都放在同一行,把移动轨道都放在同一行等。
1. 每一行放些什么事件最好能由明确的区分,方便后期梳理自己设计了一些什么。比如把贯穿全程的浮动文字歌词都放在同一行,把移动轨道都放在同一行等。
```rdview 19 4
g 0 0 19 1 #f00[]{
@ -320,7 +320,7 @@ g 0 3 18 1 #f80[]{
};
```
**2、**如果有一串同类型的事件紧密排布例如每1/4格或者1/2格放一个那么可以考虑不放在同一行而是上下交错排开。
2. 如果有一串同类型的事件紧密排布例如每1/4格或者1/2格放一个那么可以考虑不放在同一行而是上下交错排开。
```rdview 16 4
ea.MoveRow [];
@ -350,10 +350,10 @@ ea.MoveRow 15 1[];
ea.Comment 4 3[];
```
**3、**对于事件总体数量不太高的情况,如果有操作房间和轨道类型的特效,可以考虑把他们放在对应数量的行里,找起来会更加方便。
3. 对于事件总体数量不太高的情况,如果有操作房间和轨道类型的特效,可以考虑把他们放在对应数量的行里,找起来会更加方便。
**4、**如果你发现在音乐初始位置需要叠放大量特效来给各个轨道和主题等做初始化,那么也最好把同类特效叠在一起。这分为两种情况:一种是相同的特效叠在一起;另一种是操作相同的对象的叠在一起。这两种都是非常直观的。
4. 如果你发现在音乐初始位置需要叠放大量特效来给各个轨道和主题等做初始化,那么也最好把同类特效叠在一起。这分为两种情况:一种是相同的特效叠在一起;另一种是操作相同的对象的叠在一起。这两种都是非常直观的。
**5、**如果需要花费一个比较长的时间跨度来制作一个谱面,那么最好放置一些注释来解释每一个小段的设计思路,以免在咕咕了一阵子之后完全失去了方向。**注释**事件在可以在红色、紫色、绿色、黄色的版块里找到。
5. 如果需要花费一个比较长的时间跨度来制作一个谱面,那么最好放置一些注释来解释每一个小段的设计思路,以免在咕咕了一阵子之后完全失去了方向。**注释**事件在可以在红色、紫色、绿色、黄色的版块里找到。
![](../images/tag-comment.png)

View File

@ -172,6 +172,7 @@ div.image-tip img {
div.image-tip {
pointer-events: none;
& p {
pointer-events: auto;
height: 0;
@ -182,7 +183,8 @@ div.image-tip {
justify-self: center;
transition: all 0.2s;
}
& img{
& img {
pointer-events: auto;
}
}
@ -410,12 +412,12 @@ main.hidden {
}
.sidebar-nav>ul>li>ul>li>a:has(>img):hover {
transition: transform .4s cubic-bezier(.16,-0.17,.65,-0.5);
transition: transform .4s cubic-bezier(.16, -0.17, .65, -0.5);
transform: translateX(-103px);
}
.sidebar-nav>ul>li>a:has(>img):hover {
transition: transform .4s cubic-bezier(.16,-0.17,.65,-0.5);
transition: transform .4s cubic-bezier(.16, -0.17, .65, -0.5);
transform: translateX(-83px);
}
@ -538,10 +540,25 @@ body.sticky .sidebar-toggle {
.markdown-section:has(iframe[single]) {
height: 100%;
&>p {
display: block;
position: absolute;
padding: 10px;
border-radius: 0 10px 0 10px;
opacity: 50%;
top: 0;
transition: background-color 0.3s, opacity 0.3s;
&:hover {
opacity: 100%;
transition: opacity 0.3s;
}
}
}
.markdown-section iframe {
border: 1px solid #eee;
border: 1px solid #000;
width: 100%;
height: 100%;
min-width: 100%
@ -918,7 +935,8 @@ section.cover blockquote>p>a {
border-bottom-right-radius: 10px;
filter: drop-shadow(0 10px 10px rgba(0, 0, 0, 0.5));
transition: transform 0.3s, background-color 0.3s, color 0.3s;
&:hover{
&:hover {
transform: translate(0, -5px) rotate(.3deg);
transition: transform 0.3s, background-color 0.3s, color 0.3s;
}
@ -929,10 +947,6 @@ section.cover blockquote>p>a {
margin-left: 0;
}
.markdown-section iframe {
margin: 1em 0
}
.markdown-section em {
color: #7f8c8d
}
@ -1130,6 +1144,7 @@ div.bv {
height: 0;
padding-bottom: 56.25%;
margin-bottom: 30px;
&>iframe {
position: absolute;
background-color: transparent;
@ -1137,6 +1152,7 @@ div.bv {
left: 0;
width: 100%;
height: 100%;
margin: 1em 0;
filter: drop-shadow(0 10px 10px rgba(0, 0, 0, 0.5));
}
}
@ -1224,27 +1240,30 @@ article.markdown-section:has(.intro)>.notice {
article.markdown-section:has(.intro)>.notice[onclick] {
cursor: pointer;
transition: all 0.5s;
&:hover {
transition: all 0.5s;
}
}
div.medium-zoom-overlay{
div.medium-zoom-overlay {
opacity: 0.5 !important;
}
del{
del {
text-decoration: none;
background-color: #5C5D67;
color: #5C5D67;
transition: background-color 0.1s, color 0.1s;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #7B7C84;
color: #7B7C84;
transition: background-color 0.1s, color 0.1s;
}
&:active {
background-color: inherit;
color: inherit;

View File

@ -225,3 +225,9 @@ article.markdown-section:has(.intro)>.notice {
div.medium-zoom-overlay{
background-color: var(--background-color) !important;
}
.markdown-section:has(iframe[single]) {
&>p{
background-color: var(--background-color);
}
}