46 Commits
Author SHA1 Message Date
OLDREDSTONE 7921ecc981 Merge branch 'new' of https://git.obugs.cn/RDCNWebs/rd.rdlevel.cn into new 2025-11-02 12:47:05 +08:00
OLDREDSTONE 745dbaee3e 添加 0x4D2 更新的 credit 2025-11-02 12:46:54 +08:00
obugs e28eea0a77 Merge pull request 'main' (#37) from main into new
Reviewed-on: RDCNWebs/rd.rdlevel.cn#37
2025-11-02 11:03:10 +08:00
obugs 517c75bb66 Merge pull request '添加屏幕朗读事件的描述' (#36) from 0x4D2/rd.rdlevel.cn:main into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#36
2025-11-02 11:02:06 +08:00
0x4D2 10db121ad7 添加屏幕朗读事件
Signed-off-by: 0x4D2 <1706119930@qq.com>
2025-11-01 21:13:35 +08:00
0x4D2 d51204985f 上传文件至 images
Signed-off-by: 0x4D2 <1706119930@qq.com>
2025-11-01 20:53:33 +08:00
0x4D2 bc7518ba0b 上传文件至 images 2025-11-01 20:53:31 +08:00
OLDREDSTONE a0d296a818 目录变动 #30 2025-11-01 19:46:42 +08:00
OLDREDSTONE bb0cb7de3a 大致修改内容布局 #30 2025-11-01 19:30:36 +08:00
OLDREDSTONE ae32b0e7aa 双人模式措辞更改 #26 2025-11-01 18:03:36 +08:00
OLDREDSTONE dca2f87b4b 添加检索功能脚本 #35 2025-11-01 18:02:27 +08:00
OLDREDSTONE 7aa95ab0ba 删除 banner 2025-11-01 18:02:06 +08:00
OLDREDSTONE 7a2e16d714 添加检索功能接口 #35 2025-11-01 18:00:52 +08:00
OLDREDSTONE f588b3ce92 调整目录结构 #30 2025-11-01 17:56:34 +08:00
OLDREDSTONE 5d7d99b24c 更新侧边栏顺序 2025-11-01 16:49:14 +08:00
obugs fdad67efee Merge pull request 'webhoop' (#17) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#17
2025-10-27 19:21:48 +08:00
OLDREDSTONE d2f80e8390 webhoop 2025-10-27 19:20:48 +08:00
obugs 1884e5097e Merge pull request '1026' (#16) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#16
2025-10-26 22:32:57 +08:00
OLDREDSTONE 8faac81f8f 1026 2025-10-26 22:32:11 +08:00
obugs 718be9e9bf Merge pull request '样式更新:表格行颜色交替显示优化,提升可读性' (#15) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#15
2025-10-21 22:03:52 +08:00
OLDREDSTONE 139f1eef71 样式更新:表格行颜色交替显示优化,提升可读性 2025-10-21 22:00:11 +08:00
obugs 4d37019e1a Merge pull request '添加精灵相关内容' (#14) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#14
2025-10-18 14:11:32 +08:00
OLDREDSTONE 90af884c2c 添加精灵相关内容 2025-10-18 14:10:53 +08:00
obugs 9114b1664b Merge pull request '日常维护' (#13) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#13
2025-10-18 11:41:22 +08:00
OLDREDSTONE 96a90c364d 日常维护 2025-10-18 11:39:30 +08:00
obugs 56e78752b2 Merge pull request '更新字体' (#12) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#12
2025-10-16 11:58:26 +08:00
OLDREDSTONE f902bd4ec6 更新字体 2025-10-16 11:57:01 +08:00
obugs 7c35d418c0 Merge pull request 'banner 又加回来了' (#11) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#11
2025-10-11 23:17:47 +08:00
OLDREDSTONE 91737a0f95 banner 又加回来了 2025-10-11 23:06:03 +08:00
obugs 520f68be8f Merge pull request '添加了 7th beat games 授权' (#10) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#10
2025-09-27 16:51:00 +08:00
OLDREDSTONE ca02d2e195 添加了 7th beat games 授权 2025-09-27 16:48:46 +08:00
obugs 4e92254eb8 Merge pull request '删除 Banner' (#9) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#9
2025-08-31 19:23:40 +08:00
OLDREDSTONE 30d84f613b 删除 Banner 2025-08-31 19:20:45 +08:00
obugs 5e92f750a6 Merge pull request '对不起巧麦(' (#5) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#5
2025-08-01 22:12:12 +08:00
OLDREDSTONE 4f08e1e53a 对不起巧麦( 2025-08-01 22:11:00 +08:00
obugs 1d9514bd7b Merge pull request '添加了本地状态记录' (#4) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#4
2025-07-21 00:49:43 +08:00
OLDREDSTONE ac39d732b7 添加了本地状态记录 2025-07-21 00:48:56 +08:00
obugs c323edf184 Merge pull request 'others' (#3) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#3
2025-07-19 21:18:47 +08:00
OLDREDSTONE 239e15c01d Merge branch 'others' of https://git.obugs.cn/RDCNWebs/rd.rdlevel.cn into others 2025-07-19 21:17:58 +08:00
OLDREDSTONE bfa44b33c4 添加了使用协议 2025-07-19 21:17:48 +08:00
obugs a702602ef5 Merge pull request '0719 更新' (#2) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#2
2025-07-19 18:56:04 +08:00
obugs fac884caf2 Merge branch 'main' into others 2025-07-19 18:55:41 +08:00
OLDREDSTONE e3d6c8c572 0719 更新 2025-07-19 18:49:07 +08:00
obugs 5e91af323b Merge pull request 'others' (#1) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#1
2025-06-28 16:50:20 +08:00
OLDREDSTONE c7b6c35ddc 0628 更新 2025-06-28 16:48:49 +08:00
OLDREDSTONE 5dab36288b 修改了 README 2025-06-26 14:10:14 +08:00
64 changed files with 2686 additions and 693 deletions
+69
View File
@@ -0,0 +1,69 @@
项目综合授权条款
1. 源代码与原创资源
路径:
所有源码文件、
/indexicon/ 下的所有文件
所有以下未提及部分
授权:MIT License
2. 文档内容
路径:
/pages/ 下的所有 .md 文件
授权: Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC 4.0)
3. 其他作者的美术
路径:
/resources/ 下的所有文件
/images/intro-qqgroup.png
/images/otto-dark.gif
/images/otto-light.gif
/images/otto-puzzled-original.png
/images/otto-puzzled.png
/images/otto-rd.png
/images/otto-rd2-1.png
/images/otto-rd2-xmas-compressed.png
/images/otto-rd2-xmas.png
/images/otto-traditional.png
/images/otto-wyym.png
授权: Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC 4.0)
4. 受限游戏美术
路径:
/images/ 内的截图、临摹图
授权:仅限本教程在线演示,禁止复制、再分发
5. 商业字体
字体文件未包含在仓库
说明:已购个人授权,不可再分发;本地部署请另行购买或改用免费可商用字体
Copyright (c) 2025 节奏医生饭制部制作组
Comprehensive License Notice
1. Source Code & Original Assets
Scope:
All source files
All files in /indexicon/
All other unmentioned parts
License: MIT License
2. Documentation
Scope:
All .md files in /pages/
License: Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC 4.0)
3. Other Authors' Art
Scope:
/resources/ directory
/images/intro-qqgroup.png
/images/otto-dark.gif
/images/otto-light.gif
/images/otto-puzzled-original.png
/images/otto-puzzled.png
/images/otto-rd.png
/images/otto-rd2-1.png
/images/otto-rd2-xmas-compressed.png
/images/otto-rd2-xmas.png
/images/otto-traditional.png
/images/otto-wyym.png
License: Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC 4.0)
4. Restricted Game Art
Scope:
Screenshots or derivative art in /images/
License: Display-only for this tutorial; no copying or redistribution
5. Commercial Fonts
Font files are NOT included in the repository
Note: personal license only; for local builds obtain separate license or replace with free-for-commercial-use fonts
Copyright (c) 2025 节奏医生饭制部制作组
+20 -10
View File
@@ -1,20 +1,30 @@
# 节奏医生中文编辑器教程网站
> 此版本网页正在维护。
## 预览和编辑
# 节奏医生中文编辑器教程
### [Visual Studio Code](https://code.visualstudio.com/)
《节奏医生》编辑器中文教程旨在以教学的形式帮助您快速了解编辑器的界面,基础功能,设计技巧和做谱规范。未经授权不得以图文,视频等形式转载,也不得以本教程内素材进行二次创作。
此项目基于 [docsify](https://docsify.js.org/#/) 编辑,并使用 [RDView](https://github.com/OLDRedstone/RDView) 显示编辑器可交互模块。
编辑器视频教程点击[此链接](https://www.bilibili.com/video/BV1pr4y1k7gP)。教程内所有演示关卡下载放置于[百度网盘](https://pan.baidu.com/s/1Tb6ywVmge25ztMFnpa6q_Q),提取码:rdlv。
- 确保 [安装 markdown-rdview-support](https://github.com/OLDRedstone/markdown-rdview-support) 即可预览 RDView 内容。
如果成功安装,那么以下内容的 markdown 预览应当是一个事件栏。
> 如果你看到[更新信息](/pages/changelog.md)增加了,但是又感觉什么也没更新,那就试着按一下`Ctrl + F5`,再不行就清空浏览器缓存。
```rdview 7 5 1
es.PlaySong 0 0[];
es.SetCrotchetsPerBar_4 0 1[];
eb.AddClassicBeat 0 2[tick=6];
```
## 联系我们
- 确保 [安装 docsify](https://docsify.js.org/#/quickstart) 以实时预览页面。
![我们的 QQ 群号是:951070053](/images/intro-02.png)
```bash
npm i docsify-cli -g
```
如果你有做谱相关的问题,或是对本教程有什么意见和建议,可以点击链接加入 QQ 群 [951070053](https://jq.qq.com/?_wv=1027&k=nsPA09E4) 告知我们。你也可以在群里上传谱面给我们试玩,群友们都会热心为你提供谱面优化建议。真诚欢迎各位做谱萌新和大佬的加入。
- 运行以下命令以启动本地服务器实时预览。
(悄悄透露一句:官方也入驻了这个群哦~)
```bash
docsify s
```
戳这只看板娘可以获得随机 TIPS →
Game assets courtesy of [7th Beat Games](https://7thbe.at/). Game assets are excluded from this site's license.
-1
View File
@@ -1 +0,0 @@
<!-- _navbar.md -->
+16 -17
View File
@@ -11,39 +11,36 @@
- [<img src="/indexicon/Intro.png"/><p>序</p>](/pages/intro.md)
- [<img src="/indexicon/changelog.png"/><p>更新信息</p>](/pages/changelog.md)
- 入门:基础
- 基础:熟悉编辑器
- [<img src="/indexicon/changegameversion.png"/><p>修改游戏版本</p>](/pages/changegameversion.md)
- [<img src="/indexicon/importandexport.png"/><p>导入与导出</p>](/pages/importandexport.md)
- [<img src="/indexicon/createbuiltincharacter.png"/><p>创建游戏内置角色</p>](/pages/createbuiltincharacter.md)
- [<img src="/indexicon/editorinterface.png"/><p>编辑器界面与快捷键</p>](/pages/editorinterface.md)
- 入门:基础机制
- [<img src="/indexicon/importandexport.png"/><p>导入与导出</p>](/pages/importandexport.md)
- 基础:游戏机制
- [<img src="/indexicon/createbuiltincharacter.png"/><p>创建游戏内置角色</p>](/pages/createbuiltincharacter.md)
- [<img src="/indexicon/classicbeat.png"/><p>普通拍子</p>](/pages/classicbeat.md)
- [<img src="/indexicon/oneshotbeat.png"/><p>单发拍子</p>](/pages/oneshotbeat.md)
- [<img src="/indexicon/holdbeat.png"/><p>长按</p>](/pages/holdbeat.md)
- 基础:文字
- [<img src="/indexicon/floatingtext.png"/><p>浮动文字</p>](/pages/floatingtext.md)
- [<img src="/indexicon/captions.png"/><p>其他文字事件</p>](/pages/captions.md)
- 基础:轨道特效
- 基础:基本特效
- [<img src="/indexicon/moverow.png"/><p>轨道移动</p>](/pages/moverow.md)
- [<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/settheme.png"/><p>场景与主题</p>](/pages/settheme.md)
- [<img src="/indexicon/captions.png"/><p>文字事件</p>](/pages/captions.md)
- [<img src="/indexicon/vfx.png"/><p>视觉效果</p>](/pages/vfx.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)
- [<img src="/indexicon/twoplayers.png" width="64px"/><p>双人模式设计</p>](/pages/twoplayers.md)
- [<img src="/indexicon/accessibledesign.png"/><p>无障碍设计</p>](/pages/accessibledesign.md)
- 进阶:视觉特效
- [<img src="/indexicon/room.png"/><p>房间</p>](/pages/room.md)
- [<img src="/indexicon/camera.png"/><p>镜头移动</p>](/pages/camera.md)
- [<img src="/indexicon/roomfx.png"/><p>房间视效</p>](/pages/roomfx.md)
- [<img src="/indexicon/camerafx.png"/><p>镜头带来的额外视效</p>](/pages/camerafx.md)
- [<img src="/indexicon/camerafx.png"/><p>摄像机视效</p>](/pages/camerafx.md)
- [<img src="/indexicon/hallofmirror.png"/><p>镜厅</p>](/pages/hallofmirror.md)
- [<img src="/indexicon/vfx.png"/><p>视觉干扰</p>](/pages/vfx.md)
- [<img src="/indexicon/windowmove.png"/><p>窗口移动</p>](/pages/windowmove.md)
- [<img src="/indexicon/hand.png"/><p>手</p>](/pages/hand.md)
- 进阶:自制素材
- [<img src="/indexicon/character.png"/><p>自制角色与精灵资源</p>](/pages/spritesheet.md)
- [<img src="/indexicon/decoration.png"/><p>使用精灵</p>](/pages/decoration.md)
- [<img src="/indexicon/assets.png"/><p>场景资源</p>](/pages/assets.md)
@@ -52,6 +49,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)
@@ -61,6 +61,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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 4.9 MiB

+6 -4
View File
@@ -26,7 +26,7 @@
homepage: '/pages/intro.md',
loadSidebar: true,
loadNavbar: true,
coverpage: true,
coverpage: false,
maxLevel: 5,
subMaxLevel: 4,
mergeNavbar: true,
@@ -43,6 +43,7 @@
.replace(/'/g, "&#039;");
}
loadRDView()
const originalCodeRenderer = $docsify.markdown?.renderer?.code;
$docsify = {
...$docsify,
...{
@@ -52,10 +53,10 @@
if (lang.trim().startsWith('rdview')) {
return RDViewRender(lang, code);
}
return `<pre><code>${escapeHtml(code)}</code></pre>`;
return originalCodeRenderer ? originalCodeRenderer(code, lang) : `<pre data-lang="${lang}"><code>${escapeHtml(code)}</code></pre>`;
},
image: function (src, title, alt) {
return `<div class="image-tip"><img class="medium-zoom-image" src="${src}" alt="${alt}">${alt?`<p>${alt}</p>`:""}</div>`;
return `<div class="image-tip"><img class="medium-zoom-image" src="${src}" alt="${alt}">${alt ? `<p>${alt}</p>` : ""}</div>`;
},
}
},
@@ -85,7 +86,8 @@
<script src="/script/docsify-copy-code.min.js"></script>
<script src="/script/switchLightDarkMode.js"></script>
<script src="/script/otto.js"></script>
<script src="/script/custommethod-search.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-yaml.js"></script>
<script type="module" src="/script/special.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

@@ -1,3 +1,4 @@
[项目地址](https://github.com/RDCN-Community-Developers/CodeRD)
# 代码编写关卡
点击[此处](../resources/compiler.exe)下载。项目作者:硫酸铜。项目来源:[PyRD - 为视障谱师写节奏医生谱子的一种全新的尝试](https://github.com/qwedc001/pyrd)。
+769
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. 在控制台中输入以下命令:
```bash
Install-Package RhythmBase -Version 1.2.0-rc2
```
4. 等待安装完成,并确保项目已引用所需的 NuGet 包。
5. 如果使用 .NET CLI,请输入:
```bash
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
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");
```
+51
View File
@@ -44,3 +44,54 @@
举个例子,假如你的设备输入延迟比较大,又或者你用的是蓝牙耳机,更甚至是Steam远程畅玩或是和好友远程玩双人关卡,此时你会发现,你按下空格键,会等大概一秒钟才会听到空拍音效,节奏点也会因为网络延迟延后判断,通常这时候我们会通过调整输入偏移来解决,但即使是调整了输入偏移,你在按空拍的时候还是会因为网络延迟等原因延后一秒钟才会播放空拍音效。
这也就是为什么说空拍音效的响起会对后续节奏判断产生影响了,偏移量小的玩家还没什么,无非就是多响一声而已,但偏移量大的玩家呢?所以制作过程中尽量不要让玩家在没有节拍的位置误认为有节拍并按了一个空拍,毕竟如果延迟比较大的话,这种误导性影响还是挺大的。
### *7、 使用屏幕朗读事件
官方在v0.19.0无障碍更新中加入了屏幕朗读事件,以为视障玩家群体提供更多支持。
> 为你的关卡添加屏幕朗读事件是可选的,但是请确保你正确使用了事件。同时,你**不应该**:
>
> - 添加会干扰正常游玩的屏幕朗读事件(如场景描述与画面不符、对轨道的错误提示等);
> - 将屏幕朗读事件作为谱面玩法的一部分(除非在关卡描述中特别说明)。
在继续之前,请确保在`设置>易用性设置>朗读设置`中启用了`朗读`
阅读[关卡屏幕阅读器的适配测试指南](https://docs.qq.com/doc/DUVJVc1pjam1uT1R6)以了解如何配置屏幕朗读。
另外,推荐在启用朗读的情况下先游玩官谱以作参考。
#### 朗读轨道信息
![](../images/narraterowinfo.png)
**轨道**:选择要朗读哪条轨道的信息。
**类型**:“**患者上线**”表示新的轨道被引入;“**患者更新**”表示某条轨道的行为模式改变;“**患者离线**”表示某条轨道在当前谱面不再被使用。
“**医生上线**”“**医生离线**”与“**患者上线**”“**患者离线**”类似,只不过由电脑操控。
**仅声音提示**:启用该选项时,游戏将不会朗读该提示的类型。
**朗读X字拍子**:选择**开启**时,游戏将会朗读该轨道当前的X字拍子。选择**自定义**时,将会朗读玩家自定义的X字拍子,而不是轨道当前的X字拍子。例如,对于`----xx ---♥`,游戏会朗读“X拍56”;而当轨道没有X字拍子时,游戏会朗读“无X拍”。
**紊乱**:用于提示X字拍子连续变化的节拍(例如`X-WOT`)。
**玩家**:选择在双人模式下,该轨道由哪位玩家接管。
一般情况下,游戏会按以下的顺序来朗读:
`[玩家][类型][轨道角色][X字拍子]`
#### 朗读口述
**口述内容**:游戏朗读的内容。
**朗读分类**:该朗读事件的类别。下面是官方对朗读分类的说明:
- 提示:适用于常规屏幕文字提示,“状态标识”信息默认归入此类。
- 描述:“朗读口述”事件的默认分类,专用于呈现纯视觉信息(如角色动画、背景环境等)。玩家可按`F6`键切换此类播报。
- 字幕:"浮动文字"事件的默认分类,适用于语音内容字幕(如歌词字幕),支持多语言翻译。玩家可按`F7`键切换此类播报。
- 说明:专用于游戏控制说明。播报后,玩家可随时按`F1`键重复听取当前指引(直至下一条指引信息覆盖)。
+2
View File
@@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DQUJxb0puSFFOVWpz)
<iframe single src="https://docs.qq.com/sheet/DQUJxb0puSFFOVWpz"></iframe>
+3 -3
View File
@@ -8,7 +8,7 @@
### 带X拍子
在编辑器中翻译为X字拍子。心跳有时候会**跳过一些拍子**,但我们仍然需要在拍子抵达时抓住时机按键。当电图线路上出现**红色的X字**时,就意味着这些拍子会被跳过。这个时候你需要在头脑里继续数数,然后照常在第二拍处按键。
在编辑器中翻译为X字拍子。心跳有时候会**跳过一些拍子**,但我们仍然需要在拍子抵达时抓住时机按键。当电图线路上出现**红色的X字**时,就意味着这些拍子会被跳过。这个时候你需要在头脑里继续数数,然后照常在处按键。
`1-2 亲密`中被首次引入,且在大量关卡中出现。
@@ -22,13 +22,13 @@
### 摇摆普通拍子
也称心脏梗阻。他们的心跳会出现不规则的拍子,因为心跳信号的工作不正常,节拍也会呈现不规则的感觉。无论如何,也请在第拍按下空格。
也称心脏梗阻。他们的心跳会出现不规则的拍子,因为心跳信号的工作不正常,节拍也会呈现不规则的感觉。无论如何,也请在第拍按下空格。
`3-3 分离二重唱`中被首次引入。
### 网好卡菌
在编辑器中应设置为电击拍子。无论发生什么都要在音乐的第拍按下空格。
在编辑器中应设置为电击拍子。无论发生什么都要在音乐的第拍按下空格。
`1-X 东方不眠夜`中被首次引入。
+2
View File
@@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DUGpSTXFZQkNGWWdn)
<iframe single src="https://docs.qq.com/sheet/DUGpSTXFZQkNGWWdn"></iframe>
+2
View File
@@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DZU1LTmJGVkhhQ2Vp)
<iframe single src="https://docs.qq.com/sheet/DZU1LTmJGVkhhQ2Vp"></iframe>
+2
View File
@@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DZGlHSExJSllSZ0tI)
<iframe single src="https://docs.qq.com/sheet/DZGlHSExJSllSZ0tI"></iframe>
-39
View File
@@ -1,44 +1,5 @@
# 场景资源
## 背景和前景图片
在房间的前后关系里我们提到了背景图片和前景图片这两个概念,也就是说,除了使用编辑器内自带的一些主题之外,其实自己也是可以自定义画面的背景和前景图案的。这个功能同样十分灵活,但是如果想要完全靠自己来原创或是临摹背景图片的话,可能需要对画像素画有一些掌握。
选择紫色的版块,放置一个**设置背景**的事件,发现里面有两种形式,一种是纯色,这个比较好理解,唯独要注意的是**纯色的默认值是黑色而不是透明**,另一种是插入图片。
> 插入的图片必须是.png格式,如果你是自己画背景图,建议使用画布大小为352\*198像素。
颜色选项,看似意义不明,但其实也有一定的用处。如果你导入的图片只有纯白和透明两种颜色,这个功能就可以为纯白的部分染色,染成你自己设置的颜色。但是因为一次只能设置一种颜色,使用起来也有些限制。
缩放功能其实类似于操作系统里壁纸的填充方式,具体效果点开小箭头查看。
<details><summary><strong>填充方式</strong></summary>
<img src="../images/assets-fillmode.png" alt="填充方式图例" />
<ul>
<li><strong>填满:</strong>将画面拉伸到横纵全部填满屏幕。要注意的是,如果你塞进去一张高清图,编辑器也会把它缩小到352\*198像素来适应画面,而且这个缩放机制是像素化的,画面会变得非常模糊。</li>
<li><strong>拉伸:</strong>将画面等比拉伸直到左右垂直线对齐屏幕。</li>
<li><strong>等比拉伸:</strong>将画面等比拉伸直到上下水平线对齐屏幕。</li>
<li><strong>居中:</strong>就是按原画面大小放在正中间。</li>
<li><strong>平铺:</strong>像铺瓷砖一样铺满屏幕。</li>
</ul>
</details>
> 如果你想做一个动图效果,需要把.gif拆解成一连串的.png图片,在选择图片时同时选择所有的图点击确定。接下来就会多出一个设置帧数的选项,来确定动图的播放速度。帧数 = \(背景图总画面数 / 一个循环占节拍数量\) \* 曲速/60。
设置背景一样,但使用这个功能的主要目的还是为了能产生一个图片覆盖轨道和手的效果,这样在画面效果的处理上可以更加灵活。
## 平铺背景
唯独平铺选项会多冒出来一个速度设置,它可以让背景图片始终保持设置的速度移动。
* 在滚屏模式下,效果类似于<code>4-2</code>的天空在慢慢移动,速度和缓速则是调整背景从静止到目标速度的变化过程。
* 在脉冲模式下,背景的移动速度变化就是周期性的,用<code>间隔</code>来调整周期。另外,如果你想设计这样的移动背景,在作画的时候也需要考虑画面左右两端和上下两端的连续,否则画面运动起来会感觉到割裂。
举个例子,公路,山坡,白云,蓝天的运动速度都不一样,毕竟在画面中的远近关系不一样,把他们分别画在四个背景图片里,放在四个房间,然后设置四个移动房间事件,把位置都改为`(50%, 50%)`,把大小都改为`(100%, 100%)`,设置四个背景图片,使用平铺的方式,设置不同的水平移动速度,就得到了在公路上慢慢前进的效果。
![灵 魂 公 路](../images/assets-road.gif)
另外,使用小图片像地砖一样平铺才是更为原本的用法。
## 还原PV效果
有些音乐是自带PV/MV/BGA的,直接将原视频的风格或内容搬运到节奏医生中也不失为一种选择。但编辑器也有其自己的像素风格,有节奏医生自己的游戏特色,还原时也要讲究融合。
+1 -1
View File
@@ -22,7 +22,7 @@
## 设置游戏音效
除了节奏音效和按拍音效之外,游戏里还有一些地方会出现音效,主要是失误的时候(分为微小失误和严重失误),没有节拍却按了空格的时候(默认是“啵”一声,所以在编辑器里面叫“手掌-啵音”),第拍按下之后第八拍“咚”地一声回音(叫做心爆,这时候心脏图标也会小抖一下),以及跳过拍(嘎↘的声音),长按拍,冻结拍,灼热拍的音效。
除了节奏音效和按拍音效之外,游戏里还有一些地方会出现音效,主要是失误的时候(分为微小失误和严重失误),没有节拍却按了空格的时候(默认是“啵”一声,所以在编辑器里面叫“手掌-啵音”),第拍按下之后第八拍“咚”地一声回音(叫做心爆,这时候心脏图标也会小抖一下),以及跳过拍(嘎↘的声音),长按拍,冻结拍,灼热拍的音效。
这些音效在设置的时候是无法下拉列表选择更改的,只能通过键入游戏内置的音效文件名,或者导入自定义的音效来改变。目前已公开的内置音效文件名因为数量太多,集中收录在了[附录3](../appendix/3.html)中。
+85 -6
View File
@@ -1,4 +1,88 @@
# 镜头移动
# 房间与镜头
完全掌握了节奏医生的节奏设计之后,我们要把目光放在谱面的视觉呈现上了,这一节我们就从理解“房间”的概念开始。
编辑器总共支持四个房间,你可以想象成一栋房子有四层楼,每层楼最多住四个人,同时,每层楼的装修可以随意摆设互不干扰。这四层楼不一定刚刚好叠在一起,可以随时移动,互不交叠,只是高度不同。我们飞在空中从上往下看,就能看到目视所及范围内的大楼景象。同时,如果空中飘扬着其他物件,也会被我们的眼睛捕捉到,如果大楼的天花板和地板是透明的,那么地基里有什么东西也会被看到。
当我们使用**排列显示房间**事件时,房间就只能从上到下地排列在屏幕上,而如果使用**移动房间**事件,则可以随便移动。
## 排列显示房间
![排列房间设置(左)与实际效果(右)](../images/room-visible.png)
将左边的可见房间点亮为黄色,屏幕上就会显示这个房间,高度可以自定义,填写数据后房间高度将自动分配。详细的分配规则点击下方箭头展开。如果你懒得理解分配规则,只需永远保证 **不点亮多余的房间,并且将存在的房间高度总和设置到100%** 即可。
### 房间分配规则
- 如果全部不填,那么
所有房间高度为 `25%`
- 否则
- 计算已填房间高度的总和 `Sum`
- 如果 `Sum = 100%`,那么
按实际数据设置房间高度,未填写的视为 `0%`
- 如果 `Sum < 100%`,那么
- 如果有未填写房间高度的房间,设个数为 `n`,那么
未填写高度的房间高度 `height = (100% - Sum) / n`,即均分未分配的房间高度;
- 否则
每个房间的房间高度 `height = height * 100% / Sum`,即等比例拉伸房间高度至高度总和为 `100%`
- 如果 `Sum > 100%`,那么
每个房间的房间高度 `height = height * 100% / Sum`,即等比例缩小房间高度至高度总和为 `100%`
下面还留有一个时长和缓速的选项,这个和轨道移动的时所介绍的设置规律是完全相同的。如果填写0拍就是瞬间完成房间分配。
> 如果你想直观地看看各种移动方式的速度区别,可以[看看这里](https://easings.net/zh-cn)。
## 房间的前后关系
但实际上很多时候我们并不想就让房间这样死板地上下排布,甚至想让这些房间交叠在一起。要做到这一点,我们得先明确各个房间叠在一起的时候,它们的前后关系是怎样的。
![](../images/room-animation.gif)
首先看一个房间内,各种事件的由前往后关系是:
> [浮动文字]
[弹幕效果]
[前景图片]
[轨道4]
[轨道3]
[轨道2]
[轨道1]
[背景图片(图片)]
[背景图片(颜色)]
它们的前后顺序是不可改变的。
各个房间整体之间又有前后顺序,默认由前往后关系是:
> [显示状态牌]
[应用到:顶部]
[房间1]
[房间2]
[房间3]
[房间4]
四个房间的顺序是可以改变的,只需要在黄色版块内添加一个**重新排序房间**事件,把前后顺序拖动到想要的即可。
![](../images/room-layer.png)
## 激活其他房间
在刚打开编辑器的状态下,实际上只能看到房间1。并不是另外三个房间不存在,而是房间大小被设置成了`0%`。在黄色版块中找到**移动房间**事件,事件放置在工作区的哪一行,就是对应的哪个房间被移动。创建三个移动房间事件置于234行,将“大小”设置为`(100%,100%)`,这样所有的房间都以屏幕大小叠在一起了。
还记得[这里](../pages/settheme.md)提到了**设置主题**事件吗,我们再添加一个“花房·昼”。由于黄色版块不再是空白的,设置主题的事件的右上角出现了新的按钮,这是将事件设置到具体房间的功能。
![](../images/room-theme.png)
将带有花房·昼主题的事件选择房间4,预览效果会发现我们看到的依然是武士电子,因为现在不透明的房间1完全遮住了房间4。删掉武士电子主题,会发现背景全黑,依然看不到花房。这是因为默认状态下房间并不是透明背景,而是纯黑背景。
在紫色的版块找到**设置背景**事件,将背景颜色选择为纯透明,应用到123房间。再次预览,终于能看到房间4了。“应用到”选项是可以多选的。
![](../images/room-eventselectroom.png)
有时还会有第五个按钮“顶部”,如果你理解了本节最开头的比喻的话,应用到顶部的事件是直接在大楼与你之间的空中生效的。房间怎么移动都不会影响到顶部。
![](../images/room-eventsettings.png)
经此示例,相信你已经能理解房间的基本运作原理了。
## 概述
移动房间应该是整个编辑器内灵活性最高的功能之一了。它的灵活性本身并不是基于对编辑器功能的理解的,而是更类似于对摄影或是视频制作手法的理解。我们观看电影也总是会说“镜头语言”,这话同样适用于作谱。合理的镜头运用可以极大程度提升谱面的视觉观感。
@@ -54,11 +138,6 @@
![4-1N 的转场](../images/camera-minercutscene.gif)
**3、房间透明度**
**淡入淡出房间**事件倒是没有太多可说的,调节的是房间整体的透明度。同样也可以选择时长和缓速模式。
最基本的用法就是把一个房间的透明度从`0`调到`100`,另一个从`100`调到`0`,完成淡入淡出的转场。
**4、排列显示房间**
在[这一节](../pages/room.md)我们介绍过这个功能,但是使用这个软件自带的排列显示房间有一个略显僵硬的限制:房间的上下关系是不可变更的。如果想要自己定义谁在上谁在下,那就不能再使用这个默认功能了,需要我们添加移动房间功能来实现。
+163 -16
View File
@@ -1,4 +1,151 @@
# 其他文字事件
# 文字事件
## 浮动文字基本设置
如果我们要制作一首歌曲的谱面,一般而言都会考虑将歌词显示在画面中,这是一种简单粗暴,但能快速提升整体观感的做法。要在画面中加上字幕,我们需要在紫色的版块中找到**浮动文字**事件。
选择这个事件后,我们可以发现最开始的两行很有Word的感觉。
![](../images/floatingtext-fontslyle.png)
**aA**:可以调整字号大小,字号基本上没有上限,如果设置太大的话,文字就会**超出屏幕**。
**旋转**:可以旋转浮动文字框,注意是**整个文本框的旋转**,而不是每个字单独旋转。
**AB**:两个颜色分别是字的颜色和字符描边的颜色,刚才所说的字号里面,**只会包含字符本体的大小**,描边是没有算在里面的。
**↔**:指定浮动文字横向的基准位置。
**↕**:指定浮动文字纵向的基准位置。
![](../images/floatingtext-position.png)
**位置**:指的是基准的位置。可以按一下准星符号再在预览画面窗口中点选位置。有条件精准找到位置的话还是建议在`X``Y`栏中输入具体数值。**点击右边的`%`按键可以切换屏幕像素点和屏幕百分比两种输入方式**。
![不同基准在同一位置下的不同效果](../images/floatingtext-align.png)
**文字**:就是输入文本的地方了。纯英文文本是使用的像素字体,带有中文字符的情况下会全部变成矢量字体,如果你想要纯英文文本也显示矢量字体的话,可以回车键再在第二行输入一些中文,如下图所示。当文字框里没有中文时就无法输入中文,需在游戏外部打好要插入的文本,并通过复制粘贴的方式输入。
![](../images/floatingtext-example.png)
另外,目前已知中文繁体以及日语假名都可以在画面中正常显示。
![二次元歌曲狂喜](../images/floatingtext-jp.png)
**创建文本递进**:如果想要把一整句歌词按歌词节奏一个一个地显示出来,就需要在文本中使用“`/`”将它们隔开。如果想要一个一个地覆盖掉之前显示的内容,就需要在文本中使用`回车键`将他们隔开。之后点击下面的“创建文本递进”,在工作区里面可以拖动右箭头事件来调整时机。如果你做好了又不想要了,那么再把下面的开启改成关闭即可,无需手动删除。如果你又反悔了,还是想要,再改成开启,又会恢复记录。
```rdview 8 3
ea.FloatingText 1 1;
ea.AdvanceText 2 1;
ea.AdvanceText 3 1;
ea.AdvanceText 4 1;
ea.AdvanceText 5 1;
ea.AdvanceText 6 1;
ea.AdvanceText 7 1;
```
音节的消失方式选择**淡出**就是慢慢消失,**立即**就是瞬间消失,音节显示的时长是按创建文本递进的最后一个事件往后数拍子的。
```rdview 13 3
ea.FloatingText 1 1;
g 3 1 4 1;
g 7 1 0 1 #f80;
ea.AdvanceText 3 1;
g 8 1 4 1;
g 12 1 0 1 #f80;
ea.AdvanceText 8 1;
```
## 更丰富的效果和技巧
这个模块的基本功能就是上面那些,但是实际使用起来其实非常灵活。如果想要制作视觉效果更为丰富的字幕,就需要我们打开脑洞,多去深挖这些基础功能。下面我举几个例子给大家做一些启发。
**1、**如果想一次性显示多行,需要在换行的地方加上`\n`。输入`\/`才能打出“/”。
**2、**文本旋转效果是整个文本框的旋转,如果想要做到每个字都旋转,那么同样需要添加多个浮动文字模块,添加相同的旋转角度。如果想要让文本围绕一个中心旋转,那么直接用准星功能去找位置会方便一些。
**3、**如果在同一时刻同一位置有多个不同的浮动文字,那么字在屏幕中的上下层顺序只和你添加事件的先后顺序有关,先加入的事件在下层,后加入的事件在上层。这个机制无法进行修改。
**4、**如果想要给文本做颜色渐变的效果,可以在相同时间相同位置添加两个浮动文字,其中一个消失方式选择淡出,另一个选择立即,这样就获得了渐变效果。
**5、**把文字递进里面添加很多错误的文字,然后全部一个字一个字回车,就可以做到文字在同一位置的乱码效果。
**6、**在文本的最后放一个竖线“\|”或者下划线“\_”就可以做出打字的效果了。
![](../images/floatingtext-typing.gif)
```
ぼ|
ぼく|
僕|
僕の|
僕のゆ|
僕のゆめ|
僕の夢|
僕の夢
そ|
そう、|
そう、わ|
そう、わた|
そう、わたし|
そう、私|
そう、私の|
そう、私のゆ|
そう、私のゆめ|
そう、私の夢|
```
**7、**创建了一组文本递进之后,只需要双击浮动文字事件本身,整组字幕都会被选中。不需要一个个手动去点选了。
**8、**基准的左中右会影响文本递进效果。靠左时从左向右出现文本递进;靠中时从中间挤向两侧出现文本递进;靠右时从右向左出现文本递进。
**9、**在文本框的最后加一个空行,就可以通过文本递进的方式设置文本的结束时间,避免了一点点数格子的烦恼。
**10、**在文字中的个别字符前面加上`<color=#RRGGBBAA>`,后面加上`</color>`可以改变这几个字的颜色和透明度。关于颜色的介绍由于篇幅关系将放在下一小节。
![](../images/floatingtext-rainbow.png)
对应文本框内容为:
```html
<color=red>Wel</color><color=orange>come </color><color=yellow>to </color><color=green>Won</color><color=aqua>der</color><color=blue>land</color><color=purple>!</color>`<br>
<color=red>同</color><color=orange>赏</color><color=yellow>这</color><color=green>世</color><color=aqua>外</color><color=blue>景</color><color=purple>色</color>
```
## 演示关卡
你以为这里会有一个我们制作的关卡?其实并不。在这里我想介绍的是官方联动关卡`MD-2`,不知道你们在游玩这关感受爆炸难度的同时有没有注意到字幕的表现?如果你没有什么印象的话可以先看看下面这个视频:
<div class="bv"><iframe src="https://player.bilibili.com/player.html?aid=374450491&page=55&autoplay=0" title="BPM 与偏移的校对教程" width="100%" height="100%" scrolling="no" frameborder="no" framespacing="0" allowfullscreen="true"></iframe></div>
看完之后我们再来回忆一下其中一段歌词的表现:
> `夜深了 月光也换了颜色`<br>
> 【换了颜色】字体为黑色
>
> `要更迷幻更鲜艳眩目得赤裸`<br>
> 【鲜艳炫目】字体为亮蓝色
>
> `我也将披上褪浅的音色`<br>
> 【褪浅的音色】消失更早
>
> `逃离这个现实的现在时分的陷落`<br>
> 这句意外的正常
>
> `像逆行在通往大人的公路`<br>
> 【像逆行在】四个字是从右往左出现的,也就是位置基准改成了靠右
>
> `粉红的喇叭裤 墨镜里装星空`<br>
> 【粉红】两字为粉红色,【星空】两字为蓝色和紫色
>
> `忽闪忽灭的影子中`<br>
> 【忽】字反复出现和淡出了几次,【影子】是黑色
>
> `撞见了另一个我`<br>
> 【另一个我】有两份文字略微偏移重叠在一起
有没有发现不管是颜色效果还是显示效果,在设计上都和歌词本身有着些许联系,也就让平淡无奇的歌词字幕一下子变得生动起来。
## 显示对话
@@ -13,28 +160,28 @@
**需要注意的是:**
**1、**你可以一次性输入多行文本,这样在实际显示时就会在同一对话框内逐步刷新每一行台词;如果使用多个显示对话事件,每个事件只输入一行文本,那么对话框有可能在一个事件之后消失再生成,影响连贯性。
1. 你可以一次性输入多行文本,这样在实际显示时就会在同一对话框内逐步刷新每一行台词;如果使用多个显示对话事件,每个事件只输入一行文本,那么对话框有可能在一个事件之后消失再生成,影响连贯性。
**2、**显示对话事件一次只能显示一个对话框,如果前一个事件的台词没有走完下一个事件就开始了,那么前一个会直接被顶替掉。即使设置为屏幕上方和屏幕下方各一个也会被顶替。
2. 显示对话事件一次只能显示一个对话框,如果前一个事件的台词没有走完下一个事件就开始了,那么前一个会直接被顶替掉。即使设置为屏幕上方和屏幕下方各一个也会被顶替。
**3、**一个对话框只能显示两行文字,多出来的会无法显示,请注意分段。分段后需要再输入一次立绘人物,否则立绘就不再显示了。
3. 一个对话框只能显示两行文字,多出来的会无法显示,请注意分段。分段后需要再输入一次立绘人物,否则立绘就不再显示了。
**4、** 如果需要实现像官方教程一样的可以手动跳过的对话框效果,可以添加自定义方法`activeDialogues = true``activeDialoguesImmediately = true`
- 如果启用`activeDialogues = true`
在对话事件所在小节已经播放完毕而对话仍未结束时,游戏会等待至游戏音乐的下一个小节开始时继续播放;
- 如果启用`activeDialoguesImmediately = true`
- 如果对话事件结束时所在小节仍然未播放完毕,
游戏会等待至游戏音乐的下一个节拍开始时立即从时间轴的下一个小节继续播放。
- 如果对话事件所在小节已经播放完毕而对话仍未结束,
游戏会等待至游戏音乐的下一个节拍开始时继续播放。
4. 如果需要实现像官方教程一样的可以手动跳过的对话框效果,可以添加自定义方法`activeDialogues = true``activeDialoguesImmediately = true`
- 如果启用`activeDialogues = true`
在对话事件所在小节已经播放完毕而对话仍未结束时,游戏会等待至游戏音乐的下一个小节开始时继续播放;
- 如果启用`activeDialoguesImmediately = true`
- 如果对话事件结束时所在小节仍然未播放完毕,
游戏会等待至游戏音乐的下一个节拍开始时立即从时间轴的下一个小节继续播放。
- 如果对话事件所在小节已经播放完毕而对话仍未结束,
游戏会等待至游戏音乐的下一个节拍开始时继续播放。
**5、**编辑器会自动识别标点符号,并在标点符号的位置自动添加不同程度的语句停顿,非常智能。但要注意,数学上的小数点“.”是会被判定为英文的句号的,从而产生停顿,非常不智能。
5. 编辑器会自动识别标点符号,并在标点符号的位置自动添加不同程度的语句停顿,非常智能。但要注意,数学上的小数点“.”是会被判定为英文的句号的,从而产生停顿,非常不智能。
**6、**从外界复制粘贴进入的文本中,换行符是有问题的,要退格两次才能被删掉,否则台词会叠在一起;最好的做法当然是不要复制进多余的换行符号。
6. 从外界复制粘贴进入的文本中,换行符是有问题的,要退格两次才能被删掉,否则台词会叠在一起;最好的做法当然是不要复制进多余的换行符号。
![对话事件](../images/captions-dialoguecontent.png)
**7、**可以使用对话标签控制对话的显示效果,这些标签总是被包裹在中括号里的。支持的标签如下:
7. 可以使用对话标签控制对话的显示效果,这些标签总是被包裹在中括号里的。支持的标签如下:
- `[vslow]` - 非常慢
- `[slow]` - 慢
@@ -54,7 +201,7 @@
- `[swirlSpeed=X]`
- `[static]` - 让此前设置移动方式的标签失效,位于该标签后的文本将不受前面的标签影响。
**8、**可以使用颜色标签更改文字的颜色,只需按以下格式输入:`<color=文字颜色>要更改颜色的文字</color>`。代码中的颜色可以使用十六进制颜色值(不透明的 #RRGGBB 或包含透明度信息的 #RRGGBBAA ,也可以使用[Unity富文本的颜色名](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/StyledText.html)
**8、** 可以使用颜色标签更改文字的颜色,只需按以下格式输入:`<color=文字颜色>要更改颜色的文字</color>`。代码中的颜色可以使用十六进制颜色值(不透明的 #RRGGBB 或包含透明度信息的 #RRGGBBAA ,也可以使用[Unity富文本的颜色名](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/StyledText.html)
| 颜色(中文) | 颜色(英文) | RGB | 示例 |
| :--- | :--- | :--- | :--- |
+36
View File
@@ -1,5 +1,41 @@
# 更新信息
#### 2025-11-02
- [无障碍设计](../pages/accessibledesign.md)添加了屏幕朗读事件相关说明
#### 2025-10-27
- 添加 Webhook
#### 2025-10-26
- 文档补充
- 修复代码块渲染异常
- 首页添加互动模块引导
#### 2025-10-18
- [自制角色与精灵资源](../pages/spritesheet.md)页添加了音效和表情命名规则相关内容。
#### 2025-10-16
- 更新了字体版本
#### 2025-09-27
- 添加了 7th beat games 授权
#### 2025-07-21
- 添加了本地状态存储
#### 2025-07-19
- 补充了目录图标
- 整理并添加了新的板块
- 为嵌入页面添加了跳转外链功能
- 更换了插图
- 添加了使用协议
#### 2025-06-28
- 添加了~~吐槽~~样式
- 修复了批量替换遗留问题
- 修复了样式混淆
- 添加了[双人模式设计页面](../pages/twoplayers.md)
#### 2025-06-08
- 修复了一些死链
- 添加了新的 otto 插图(没错作者又更了)并修改了更换逻辑
+3 -2
View File
@@ -7,10 +7,11 @@
- [Sterix_Craft砹氪硒](https://space.bilibili.com/349090681)
- [T_Terrence](https://space.bilibili.com/455443751)
- [mfgujhgh](https://space.bilibili.com/1369651)
- [喜欢精灵宝可梦的黄黄](https://space.bilibili.com/589687399)
- [龙俟城](https://space.bilibili.com/589687399)
- [卢毅](https://space.bilibili.com/26101629)
- [RaYmondCheung](https://space.bilibili.com/1579208)
- [NoMathExpectation](https://space.bilibili.com/184862524)
- [0x4D2](https://space.bilibili.com/478595264)
- 视频制作:
- [吾夜犹明](https://space.bilibili.com/29219948)
- [山不转水到渠成](https://space.bilibili.com/4870582)
@@ -40,7 +41,7 @@
- 表格维护:
- [自定义方法表](/pages/appendix_custommethods.md)
- [RaYmondCheung](https://space.bilibili.com/1579208)
- [ChocoWeat](https://space.bilibili.com/588693542)
- [ChocoWheat](https://space.bilibili.com/588693542)
- [制谱规范](/pages/appendix_levelrules.md)
- [o0O0o0l0o0O0o](https://space.bilibili.com/272324550)
- [相关资源](/pages/appendix_resources.md)
+5
View File
@@ -90,6 +90,11 @@
`排位顺序`:同一时间设置多个自定义方法时,你可能需要让这些事件按一定顺序生效,这个时候就需要设置这个参数了。这个参数支持负数,数字越小,事件越早生效。
## 快捷查询
<div class="custommethods-search">
</div>
## 我知道你还想问什么
你也许会有这种疑问:为什么官方要设置一个“调用自定义方法”,像写代码一样完成一些明明是很成熟的功能和特效?为什么不把它们也做成普通的事件直接添加就好了?
+2 -2
View File
@@ -49,14 +49,14 @@
</div>
<!--
| 普通拍子轨道 | 人物  | 前六拍左侧 | 前六拍中段 | 第拍左侧 | 第拍右侧 | 心脏之前 | 心脏  |
| 普通拍子轨道 | 人物  | 前六拍左侧 | 前六拍中段 | 第拍左侧 | 第拍右侧 | 心脏之前 | 心脏  |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 轨道1 | -0.5 | 1 | 0.5 | 1 | 0 | -2 | -2.5 |
| 轨道2 | -10.5 | -9 | -9.5 | -9 | -10 | -12 | -12.5 |
| 轨道3 | -20.5 | -19 | -19.5 | -19 | -20 | -22 | -22.5 |
| 轨道4 | -30.5 | -29 | -29.5 | -29 | -30 | -32 | -32.5|
| 单发拍子轨道 | 人物  | 前六拍左侧 | 前六拍中段 | 第拍左侧 | 第拍右侧 | 心脏之前 | 心脏  |
| 单发拍子轨道 | 人物  | 前六拍左侧 | 前六拍中段 | 第拍左侧 | 第拍右侧 | 心脏之前 | 心脏  |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
-->
**删除**:删除这个精灵。
-148
View File
@@ -1,148 +0,0 @@
# 浮动文字
## 浮动文字基本设置
如果我们要制作一首歌曲的谱面,一般而言都会考虑将歌词显示在画面中,这是一种简单粗暴,但能快速提升整体观感的做法。要在画面中加上字幕,我们需要在紫色的版块中找到**浮动文字**事件。
选择这个事件后,我们可以发现最开始的两行很有Word的感觉。
![](../images/floatingtext-fontslyle.png)
**aA**:可以调整字号大小,字号基本上没有上限,如果设置太大的话,文字就会**超出屏幕**。
**旋转**:可以旋转浮动文字框,注意是**整个文本框的旋转**,而不是每个字单独旋转。
**AB**:两个颜色分别是字的颜色和字符描边的颜色,刚才所说的字号里面,**只会包含字符本体的大小**,描边是没有算在里面的。
**↔**:指定浮动文字横向的基准位置。
**↕**:指定浮动文字纵向的基准位置。
![](../images/floatingtext-position.png)
**位置**:指的是基准的位置。可以按一下准星符号再在预览画面窗口中点选位置。有条件精准找到位置的话还是建议在`X``Y`栏中输入具体数值。**点击右边的`%`按键可以切换屏幕像素点和屏幕百分比两种输入方式**。
![不同基准在同一位置下的不同效果](../images/floatingtext-align.png)
**文字**:就是输入文本的地方了。纯英文文本是使用的像素字体,带有中文字符的情况下会全部变成矢量字体,如果你想要纯英文文本也显示矢量字体的话,可以回车键再在第二行输入一些中文,如下图所示。当文字框里没有中文时就无法输入中文,需在游戏外部打好要插入的文本,并通过复制粘贴的方式输入。
![](../images/floatingtext-example.png)
另外,目前已知中文繁体以及日语假名都可以在画面中正常显示。
![二次元歌曲狂喜](../images/floatingtext-jp.png)
**创建文本递进**:如果想要把一整句歌词按歌词节奏一个一个地显示出来,就需要在文本中使用“`/`”将它们隔开。如果想要一个一个地覆盖掉之前显示的内容,就需要在文本中使用`回车键`将他们隔开。之后点击下面的“创建文本递进”,在工作区里面可以拖动右箭头事件来调整时机。如果你做好了又不想要了,那么再把下面的开启改成关闭即可,无需手动删除。如果你又反悔了,还是想要,再改成开启,又会恢复记录。
```rdview 8 3
ea.FloatingText 1 1;
ea.AdvanceText 2 1;
ea.AdvanceText 3 1;
ea.AdvanceText 4 1;
ea.AdvanceText 5 1;
ea.AdvanceText 6 1;
ea.AdvanceText 7 1;
```
音节的消失方式选择**淡出**就是慢慢消失,**立即**就是瞬间消失,音节显示的时长是按创建文本递进的最后一个事件往后数拍子的。
```rdview 13 3
ea.FloatingText 1 1;
g 3 1 4 1;
g 7 1 0 1 #f80;
ea.AdvanceText 3 1;
g 8 1 4 1;
g 12 1 0 1 #f80;
ea.AdvanceText 8 1;
```
## 更丰富的效果和技巧
这个模块的基本功能就是上面那些,但是实际使用起来其实非常灵活。如果想要制作视觉效果更为丰富的字幕,就需要我们打开脑洞,多去深挖这些基础功能。下面我举几个例子给大家做一些启发。
**1、**如果想一次性显示多行,需要在换行的地方加上`\n`。输入`\/`才能打出“/”。
**2、**文本旋转效果是整个文本框的旋转,如果想要做到每个字都旋转,那么同样需要添加多个浮动文字模块,添加相同的旋转角度。如果想要让文本围绕一个中心旋转,那么直接用准星功能去找位置会方便一些。
**3、**如果在同一时刻同一位置有多个不同的浮动文字,那么字在屏幕中的上下层顺序只和你添加事件的先后顺序有关,先加入的事件在下层,后加入的事件在上层。这个机制无法进行修改。
**4、**如果想要给文本做颜色渐变的效果,可以在相同时间相同位置添加两个浮动文字,其中一个消失方式选择淡出,另一个选择立即,这样就获得了渐变效果。
**5、**把文字递进里面添加很多错误的文字,然后全部一个字一个字回车,就可以做到文字在同一位置的乱码效果。
**6、**在文本的最后放一个竖线“\|”或者下划线“\_”就可以做出打字的效果了。
![](../images/floatingtext-typing.gif)
```
ぼ|
ぼく|
僕|
僕の|
僕のゆ|
僕のゆめ|
僕の夢|
僕の夢
そ|
そう、|
そう、わ|
そう、わた|
そう、わたし|
そう、私|
そう、私の|
そう、私のゆ|
そう、私のゆめ|
そう、私の夢|
```
**7、**创建了一组文本递进之后,只需要双击浮动文字事件本身,整组字幕都会被选中。不需要一个个手动去点选了。
**8、**基准的左中右会影响文本递进效果。靠左时从左向右出现文本递进;靠中时从中间挤向两侧出现文本递进;靠右时从右向左出现文本递进。
**9、**在文本框的最后加一个空行,就可以通过文本递进的方式设置文本的结束时间,避免了一点点数格子的烦恼。
**10、**在文字中的个别字符前面加上`<color=#RRGGBBAA>`,后面加上`</color>`可以改变这几个字的颜色和透明度。关于颜色的介绍由于篇幅关系将放在下一小节。
![](../images/floatingtext-rainbow.png)
对应文本框内容为:
```html
<color=red>Wel</color><color=orange>come </color><color=yellow>to </color><color=green>Won</color><color=aqua>der</color><color=blue>land</color><color=purple>!</color>`<br>
<color=red>同</color><color=orange>赏</color><color=yellow>这</color><color=green>世</color><color=aqua>外</color><color=blue>景</color><color=purple>色</color>
```
## 演示关卡
你以为这里会有一个我们制作的关卡?其实并不。在这里我想介绍的是官方联动关卡`MD-2`,不知道你们在游玩这关感受爆炸难度的同时有没有注意到字幕的表现?如果你没有什么印象的话可以先看看下面这个视频:
<div class="bv"><iframe src="https://player.bilibili.com/player.html?aid=374450491&page=55&autoplay=0" title="BPM 与偏移的校对教程" width="100%" height="100%" scrolling="no" frameborder="no" framespacing="0" allowfullscreen="true"></iframe></div>
看完之后我们再来回忆一下其中一段歌词的表现:
> `夜深了 月光也换了颜色`<br>
> 【换了颜色】字体为黑色
>
> `要更迷幻更鲜艳眩目得赤裸`<br>
> 【鲜艳炫目】字体为亮蓝色
>
> `我也将披上褪浅的音色`<br>
> 【褪浅的音色】消失更早
>
> `逃离这个现实的现在时分的陷落`<br>
> 这句意外的正常
>
> `像逆行在通往大人的公路`<br>
> 【像逆行在】四个字是从右往左出现的,也就是位置基准改成了靠右
>
> `粉红的喇叭裤 墨镜里装星空`<br>
> 【粉红】两字为粉红色,【星空】两字为蓝色和紫色
>
> `忽闪忽灭的影子中`<br>
> 【忽】字反复出现和淡出了几次,【影子】是黑色
>
> `撞见了另一个我`<br>
> 【另一个我】有两份文字略微偏移重叠在一起
有没有发现不管是颜色效果还是显示效果,在设计上都和歌词本身有着些许联系,也就让平淡无奇的歌词字幕一下子变得生动起来。
+33 -6
View File
@@ -1,18 +1,45 @@
# 镜厅
> 预警放在最前面:观看本节教程图片可能会引发光敏性癫痫,在编辑器中使用此功能也请慎重处理。
游戏的画面是一帧一帧地播放的。为了确保游戏的画面一直在更新,在绘制一帧画面之前,游戏会将已经绘制过的画面重置为全黑。
**但是如果不重置呢?**
> 本节动图较多,如遇加载卡顿,请耐心等待。
## 基础效果
所谓的镜厅效果,就是画面在变化的时候并不清除变化过程中产生的效果,画面效果会一直叠加,然后逐渐变得混乱。
相信你已经有了初步的感觉。镜厅就是这样,原来一帧的画面仍然保留在屏幕上,只是游戏重新绘制的画面直接覆盖在了原画面之上,从而造成一种“拖影”的效果。
这本是一种“bug”,但如果稍加利用,或许会有出其不意的效果。
<div class="bv"><iframe src="https://player.bilibili.com/player.html?bvid=BV1vW411t7NF&page=18&autoplay=0" title="BPM 与偏移的校对教程" width="100%" height="100%" scrolling="no" frameborder="no" framespacing="0" allowfullscreen="true"></iframe></div>
## 抑制方法
镜厅一共能在两个地方找到,一是设置主题中的 “镜厅”,二是预设特效中的“镜厅”。**但最好使用预设特效中的**。
### 画面的覆盖
既然画面是一层一层叠在上面,如果要“修复这个 bug”,那就得咱们手动去将整个屏幕更新。
但是如果直接覆盖一个特定的颜色或者背景,那好像和直接设置背景没什么区别,镜厅就没有存在的意义了。
那半透明呢?
画面会被覆盖,但是覆盖程度取决于透明度。
我们来做个小实验:假设现在有一个像素的亮度为 100,每一帧向右移动 1 像素,在镜厅的效果下会导致它在每一帧一直留存,但是现在有一个手段,能够在每一帧画完之前整个画面的亮度降为原来的一半,那这个像素的实际亮度是怎样的呢?
| 帧 | 亮度 ||||
|-|-:|-:|-:|-:|
|1|100|
|2|50|100|
|3|25|50|100|
|4|12.5|25|50|100|
理解了这个之后,你就可以着手做出这样的效果:
![](../images/hallofmirror-trail.gif)
### 画面的扭曲
同样的,也可以
---
镜厅一共能在两个地方找到,一是设置主题中的 “镜厅”,二是预设特效中的“镜厅”。**但最好使用预设特效中的**。
如果只是直接使用镜厅加上预设特效,效果一般只会越来越阴间,所以我们一般会使用预设特效里的“晕影”和“闪烁晕影”或者半透明的背景或半透明的后背景板闪光(由于设备性能差别,镜厅的效果显示可能每个人会有所不同,我一般使用20的透明度)来起到抑制的效果,同时这样也能直接做出最简单的拖尾效果。
![](../images/hallofmirror-trail.gif)
拖尾效果(可以注意到镜厅留下了痕迹 ,如果想去除就需要再加上其他的效果,比如-1强度的高光)。这里再附上一些特效在遮罩后的组合(默认都已经打开了镜厅)。
+11 -7
View File
@@ -1,14 +1,14 @@
<div class="intro"></div>
<div onclick="location.href='https://docs.qq.com/form/page/DUFNwc1dQdHJtbXF5';" class="notice">
<p>节奏医生夏季打谱活动正在招募中!</p>
</div>
# 节奏医生中文编辑器教程
> 此网页正在维护中。如有任何问题,欢迎指出。
> 此网页正在维护中。
网页已开源至[此处](https://git.obugs.cn/RDCNWebs/rd.rdlevel.cn),欢迎投稿。
《节奏医生》编辑器中文教程旨在以教学的形式帮助您快速了解编辑器的界面,基础功能,设计技巧和做谱规范。未经授权不得以图文,视频等形式转载,也不得以本教程内素材进行二次创作。
> 页面内的大部分事件栏板块包含点击互动。尝试点击以获得更生动的演示。
也可以在[此处](../pages/appendix_tools.md)游玩事件栏板块。
《节奏医生》编辑器中文教程旨在以教学的形式帮助您快速了解编辑器的界面,基础功能,设计技巧和做谱规范。
编辑器视频教程点击[此链接](https://www.bilibili.com/video/BV1pr4y1k7gP)。教程内所有演示关卡下载放置于[百度网盘](https://pan.baidu.com/s/1Tb6ywVmge25ztMFnpa6q_Q),提取码:rdlv。
@@ -20,4 +20,8 @@
(悄悄透露一句:官方也入驻了这个群哦~)
> 戳这只看板娘可以获得随机 TIPS →
> 戳这只看板娘可以获得随机 TIPS →
本网站内容分模块授权,请务必阅读对应<a href='/LICENSE' download='LICENSE'>条款</a>后再使用。
Game assets courtesy of [7th Beat Games](https://7thbe.at/). Game assets are excluded from this site's license.
+6 -6
View File
@@ -1,4 +1,4 @@
# 普通拍的衍生节奏型
# 普通拍的衍生节奏型
## 分解为自由时长节拍
@@ -16,11 +16,11 @@ eb.PulseFreeTimeBeat 6 0[pulse=9];
eb.PulseFreeTimeBeat 7 0[pulse=9];
```
我们选中数字1对应的节拍,发现这个是设置起始脉冲位置的。你甚至可以将它改为第拍,然后把六个小的`>`删掉,这样就没有任何节拍提示。如果你设计的关卡是一个和节奏医生没什么关联的创意谱(梦幻联动?),且按键密度很大,则可以考虑这样使用。相比于工作区里面重叠摆放一大堆普通普通拍子,这样会看上去更清爽。但如果你设计的就是节奏医生,**请不要在完全无节奏提示的情况下直接响起第拍**。
我们选中数字1对应的节拍,发现这个是设置起始脉冲位置的。你甚至可以将它改为第拍,然后把六个小的`>`删掉,这样就没有任何节拍提示。如果你设计的关卡是一个和节奏医生没什么关联的创意谱(梦幻联动?),且按键密度很大,则可以考虑这样使用。相比于工作区里面重叠摆放一大堆普通普通拍子,这样会看上去更清爽。但如果你设计的就是节奏医生,**请不要在完全无节奏提示的情况下直接响起第拍**。
![](../images/moreclassicbeats-endpulse.png)
剩下的大于号`>`即意味着心跳向右移动一格,数到7的时候则需要按拍。点选之后,把它改成 `<`,心跳还能向左移动一格;点击自定义,还可以任意指定到第几个节拍。每一组自由拍**必须使用较大的节拍作为开头**,较小的则数量不限,直到数到第拍为止。
剩下的大于号`>`即意味着心跳向右移动一格,数到7的时候则需要按拍。点选之后,把它改成 `<`,心跳还能向左移动一格;点击自定义,还可以任意指定到第几个节拍。每一组自由拍**必须使用较大的节拍作为开头**,较小的则数量不限,直到数到第拍为止。
```rdview 10 2
eb.AddFreeTimeBeat 1 0[pulse=1];
@@ -41,7 +41,7 @@ eb.PulseFreeTimeBeat 9 0[pulse=9];
如果你想让一串自由节拍最后不需要按拍,而是节奏直接消失,就把这一组自由节拍的最后一个点选**垃圾桶**符号。
> 如果你设计了一段无法数到第拍的节奏且不放置垃圾桶,那么心电图就会一直停在轨道中间。这很诡异。
> 如果你设计了一段无法数到第拍的节奏且不放置垃圾桶,那么心电图就会一直停在轨道中间。这很诡异。
接下来介绍一些社区机制。社区玩家们通过合理设计自由拍,并加以适当的提示,创造出了一些全新的节奏类型。其中一些节奏被广泛使用,并在社区中形成共识,从而延续下来。使用这些机制制作自定义关卡无需额外设计教学环节。当然如果你担心玩家不了解社区,或是你有其它自己的想法,也可以为这些节奏类型自行设计节奏提示的方法,唯一的宗旨是保证提示恰当准确。
@@ -85,7 +85,7 @@ eb.PulseFreeTimeBeat 8 1[pulse=9];
不要使用内置的 `sndBeepMid` 音效!
八拍子也可以是摇摆拍,只需要将所有偶数拍选择并拖动即可变成摇摆拍:
八拍子也可以是摇摆拍,只需要将所有偶数拍选择并拖动即可变成摇摆拍:
```rdview 10 3
eb.AddClassicBeat 1 0[tick=6,swing=0.5];
@@ -99,7 +99,7 @@ eb.PulseFreeTimeBeat 7 1[pulse=9];
eb.PulseFreeTimeBeat 8.5 1[pulse=9];
```
在八拍子中,**激活拍子**与八拍子的对应关系如下图所示。不要给第1拍和第3拍设置X拍子,这样做它就相当于变回普通拍了。
在八拍子中,**激活拍子**与八拍子的对应关系如下图所示。不要给第1拍和第3拍设置X拍子,这样做它就相当于变回普通拍了。
![](../images/moreclassicbeats-8beat.png)
+91 -67
View File
@@ -1,6 +1,6 @@
# 单发拍的衍生节奏型
# 单发拍的衍生节奏型
## 单发拍进阶用法
## 单发拍进阶用法
本小节的内容可能会稍显冗长和晦涩,希望大家能保持耐心多多琢磨。
@@ -13,9 +13,9 @@ es.SayReadyGetSetGo_Go 5[];
eb.AddOneshotBeat 5 1[loop=1,tick=1.5,interval=4];
```
可以看到,虽然 Get-Set 与 Set-Go 之间的间隔不同,但依然保持了上述的对应关系。实际上,这就是`4-4`使用的单发拍
可以看到,虽然 Get-Set 与 Set-Go 之间的间隔不同,但依然保持了上述的对应关系。实际上,这就是`4-4`使用的单发拍。
`2-3`中也出现了摇摆单发拍,可以试着自己理解一下这关的单发拍提示:
`2-3`中也出现了摇摆单发拍,可以试着自己理解一下这关的单发拍提示:
```rdview 10 2
eb.AddOneshotBeat 1[];
@@ -26,7 +26,7 @@ eb.AddOneshotBeat 7[tick=0.75];
eb.AddOneshotBeat 8[tick=0.75];
```
如果我们在此基础上更进一步,会发生什么呢?来看看`4-4N`中出现过的这段单发拍
如果我们在此基础上更进一步,会发生什么呢?来看看`4-4N`中出现过的这段单发拍:
```rdview 12 4
g 1 1 4 2[offset=4]{
@@ -45,11 +45,11 @@ eb.AddOneshotBeat 9[tick=0.75];
eb.AddOneshotBeat 10.5[tick=0.75];
```
可以看到,图中的单发拍已不再局限于一个单发拍不断重复了,而是变成了若干个单发拍构成的单发拍组的不断重复,但前面讲过的与护士提示之间的对应关系仍保持不变。由此我们可以得到单发拍的核心规则:
可以看到,图中的单发拍已不再局限于一个单发拍不断重复了,而是变成了若干个单发拍构成的单发拍组的不断重复,但前面讲过的与护士提示之间的对应关系仍保持不变。由此我们可以得到单发拍的核心规则:
单发拍本质上是对一段节奏(循环节)的不断重复,这段节奏可以由一个或多个单发拍构成。在单发拍开始之前,会使用护士提示,以 Rea、Dy、Get、Set 的形式,重复一次(或多次)这段单发拍循环节,并在最后用 Go 表示单发拍的开始。
单发拍本质上是对一段节奏(循环节)的不断重复,这段节奏可以由一个或多个单发拍构成。在单发拍开始之前,会使用护士提示,以 Rea、Dy、Get、Set 的形式,重复一次(或多次)这段单发拍循环节,并在最后用 Go 表示单发拍的开始。
当对一段较为复杂的单发拍进行提示的时候,由于循环节由多个单发拍组成,护士必然会提示多个脉冲和按拍的位置,这时仍然需要遵循只在最后一对脉冲和按拍处使用 Get Set,其余部分均使用 Rea Dy。
当对一段较为复杂的单发拍进行提示的时候,由于循环节由多个单发拍组成,护士必然会提示多个脉冲和按拍的位置,这时仍然需要遵循只在最后一对脉冲和按拍处使用 Get Set,其余部分均使用 Rea Dy。
```rdview 13 4
g 1 1 4 2[offset=4]{
@@ -77,11 +77,11 @@ eb.AddOneshotBeat 12[tick=0.5];
## 复杂脉冲
正如`2-1`所说的那样,单发拍的脉冲位置并不重要,只要保证按拍位置不变,脉冲位置是可以任意变化的。
正如`2-1`所说的那样,单发拍的脉冲位置并不重要,只要保证按拍位置不变,脉冲位置是可以任意变化的。
同时,一个单发拍也可能拥有不止一个脉冲。若要实现这种效果,只需将几个单发拍的尾端对齐,首端放在不同位置即可。
同时,一个单发拍也可能拥有不止一个脉冲。若要实现这种效果,只需将几个单发拍的尾端对齐,首端放在不同位置即可。
以下单发拍均是合法的:
以下单发拍均是合法的:
```rdview 9 2
eb.AddOneshotBeat 1[];
@@ -96,9 +96,9 @@ eb.AddOneshotBeat 7.5[tick=0.5];
但这样一来,前面所说的“Rea、Get 对应单发拍脉冲”就没有意义了吗?其实不然,Rea、Get 的作用如下:
1. 护士提示的第一个词必然是 Rea 或 Get,也就意味着这个词代表单发拍循环节的开始,与末尾的 Go 对应。
1. 护士提示的第一个词必然是 Rea 或 Get,也就意味着这个词代表单发拍循环节的开始,与末尾的 Go 对应。
2. Rea、Get 可以辅助玩家掌握单发拍的节奏型。在实际游玩中,玩家通常是会以 Rea、Get 为基础来进行单发拍的演奏的。所以通常要使 Rea、Get 契合音乐或节拍设计,来达到方便玩家记忆或丰富游戏体验的效果。
2. Rea、Get 可以辅助玩家掌握单发拍的节奏型。在实际游玩中,玩家通常是会以 Rea、Get 为基础来进行单发拍的演奏的。所以通常要使 Rea、Get 契合音乐或节拍设计,来达到方便玩家记忆或丰富游戏体验的效果。
举个例子:Rea-Dy-Get-Set-Go 与 Rea-Dy--GetSet-Go,尽管两者的按拍时机相同,但不同的 Rea、Get 位置会给人带来不同的游玩体验。
@@ -134,13 +134,13 @@ g 1 4 4 1 [offset=4]{
};
```
## 跳过单发拍
## 跳过单发拍
游戏在`2-1N`中引入了一种全新的单发拍机制:跳过单发拍。这种单发拍的规则很简单:跳过单发拍之后的一次单发拍按拍会被跳过。跳过单发拍无需手动添加提示,只需在单发拍修饰符中选择跳过单发拍,相关的视效与音效提示会被自动添加到关卡中。
游戏在`2-1N`中引入了一种全新的单发拍机制:跳过单发拍。这种单发拍的规则很简单:跳过单发拍之后的一次单发拍按拍会被跳过。跳过单发拍无需手动添加提示,只需在单发拍修饰符中选择跳过单发拍,相关的视效与音效提示会被自动添加到关卡中。
当然,在被跳过的单发拍的这段空白时间里,也可以放置多个单发拍脉冲,以丰富谱面的节奏设计。跳过拍的“间隔”参数会影响跳过符号的动画时长。
当然,在被跳过的单发拍的这段空白时间里,也可以放置多个单发拍脉冲,以丰富谱面的节奏设计。跳过拍的“间隔”参数会影响跳过符号的动画时长。
需要注意的是,跳过单发拍会跳过下一次**击拍**,而不是下一个循环节。
需要注意的是,跳过单发拍会跳过下一次**击拍**,而不是下一个循环节。
```rdview 7 4
eb.AddOneshotBeat 1[type=skipshot];
@@ -152,19 +152,19 @@ eb.AddOneshotBeat 4.5 2[tick=1.5];
eb.AddOneshotBeat 5.25 2[tick=0.75];
```
另外,在剧情关卡中,跳过拍的位置如果按下空格会计算成失误,但编辑器中默认是不会失误的。
另外,在剧情关卡中,跳过拍的位置如果按下空格会计算成失误,但编辑器中默认是不会失误的。
## 方块单发拍
## 方块单发拍
常规的单发拍是针对一个节奏型的不断重复,而**方块单发拍**是“一次性”的单发拍,即没有重复,只需按拍一次。
常规的单发拍是针对一个节奏型的不断重复,而**方块单发拍**是“一次性”的单发拍,即没有重复,只需按拍一次。
方块单发拍最早出现于`MD-1`,在第五章更新后也出现在了`5-X`用于预警细分拍的出现。
方块单发拍最早出现于`MD-1`,在第五章更新后也出现在了`5-X`用于预警细分拍的出现。
选中一个单发拍,在修饰符中选择方块符号`◇`,将细分数量设为 1 ,便创建了一个方块单发拍。方块单发拍出现时需要添加特殊音效,在方块单发拍的声音栏选择“特殊”即可自动添加提示音。
选中一个单发拍,在修饰符中选择方块符号`◇`,将细分数量设为 1 ,便创建了一个方块单发拍。方块单发拍出现时需要添加特殊音效,在方块单发拍的声音栏选择“特殊”即可自动添加提示音。
![](../images/moreoneshotbeats-subdivision.png)
除此之外,方块单发拍也需要护士提示。这种护士提示与普通单发拍的提示类似,唯一的不同点在于最后的 Go 被替换为了 One。
除此之外,方块单发拍也需要护士提示。这种护士提示与普通单发拍的提示类似,唯一的不同点在于最后的 Go 被替换为了 One。
```rdview 5 3
eb.AddOneshotBeat 3[];
@@ -173,15 +173,15 @@ es.SayReadyGetSetGo_Set 2 1[];
es.SayReadyGetSetGo_Go 3 1[];
```
如果相同拍长的方块单发拍连续出现了多次,无需为每次出现添加完整的护士提示。可以逐步减少护士提示,直到完全没有护士提示,只剩下特殊提示音。
如果相同拍长的方块单发拍连续出现了多次,无需为每次出现添加完整的护士提示。可以逐步减少护士提示,直到完全没有护士提示,只剩下特殊提示音。
## 细分拍
## 细分拍
与方块单发拍的概念类似,**细分拍**也是“一次性”的单发拍,但是会产生多次等间距的心跳脉冲,按键的次数也变为多次。
与方块单发拍的概念类似,**细分拍**也是“一次性”的单发拍,但是会产生多次等间距的心跳脉冲,按键的次数也变为多次。
细分拍最早出现于`MD-1`,当时官方谱面没有官方名称,且最多仅有两个三角拍;后来第五章更新后三角拍首次出现在`5-1`并正式更名为**细分拍**,并出现了多个细分。
细分拍最早出现于`MD-1`,当时官方谱面没有官方名称,且最多仅有两个三角拍;后来第五章更新后三角拍首次出现在`5-1`并正式更名为**细分拍**,并出现了多个细分。
在修饰符中选择方块后,将细分数量设为大于 1 的值或者点击细分符号`△`,即可创建细分拍。特殊音效的添加与方块单发拍类似。最多可以将细分数量设置为 10 。
在修饰符中选择方块后,将细分数量设为大于 1 的值或者点击细分符号`△`,即可创建细分拍。特殊音效的添加与方块单发拍类似。最多可以将细分数量设置为 10 。
![](../images/moreoneshotbeats-amountofsubdivision.png)
@@ -192,7 +192,7 @@ eb.AddOneshotBeat 1 2[type=subdivision,subdivision=4];
eb.AddOneshotBeat 1 3[type=subdivision,subdivision=5];
```
在护士提示方面,不同于方块单发拍,细分拍需要在出拍前的半拍长开始数拍提示。
在护士提示方面,不同于方块单发拍,细分拍需要在出拍前的半拍长开始数拍提示。
```rdview 13
eb.AddOneshotBeat 1[type=subdivision,subdivision=2];
@@ -219,13 +219,13 @@ g 8 1 1 1[offset=1]{
![](../images/moreoneshotbeats-setnursesay.png)
> 注意:非必要情况下,不要关闭方块单发拍和细分拍本身的声音。这对于仅靠听觉游玩的玩家而言是必要的。
> 注意:非必要情况下,不要关闭方块单发拍和细分拍本身的声音。这对于仅靠听觉游玩的玩家而言是必要的。
### 单独的细分拍
### 单独的细分拍
`MD-1` 一样,你可以通过 `Rea Dy Get Set Two~Ten` 来提示单独出现的细分 2 至细分 10 。
单独细分的提示**与正常单发拍的提示类似**,Rea Dy 或 Get Set 之间的间隔同样用于提示细分拍的拍长,且数字提示**和 Go! 提示一样不需要提前**。
单独细分的提示**与正常单发拍的提示类似**,Rea Dy 或 Get Set 之间的间隔同样用于提示细分拍的拍长,且数字提示**和 Go! 提示一样不需要提前**。
```rdview 14 3
eb.AddOneshotBeat 3[type=subdivision,subdivision=2];
@@ -238,13 +238,13 @@ es.SayReadyGetSetGo_Set 7 1[];
es.SayReadyGetSetGo_4 9 1[];
```
## 冻结拍🧊
## 冻结拍🧊
**冻结拍**会将单发拍冻住,使下一拍相比正常按拍位置延后一些。
**冻结拍**会将单发拍冻住,使下一拍相比正常按拍位置延后一些。
冻结拍很早就存在于编辑器中,但尚未在官方关卡出现,第五章更新后终于在`5-2`首次露面,并且有了新的特效。
冻结拍很早就存在于编辑器中,但尚未在官方关卡出现,第五章更新后终于在`5-2`首次露面,并且有了新的特效。
在修饰符中选中冰块符号`🧊`,就创建了一个冻结拍。创建一个冻结拍后,会多出“延迟”与“间隔”选项,同时相比于更新前,冻结拍在编辑器内的样式也有所变化。
在修饰符中选中冰块符号`🧊`,就创建了一个冻结拍。创建一个冻结拍后,会多出“延迟”与“间隔”选项,同时相比于更新前,冻结拍在编辑器内的样式也有所变化。
![](../images/moreoneshotbeats-freezeshot.png)
@@ -253,25 +253,25 @@ eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
eb.AddOneshotBeat 5[type=freezeshot,delay=0.5,interval=2,tick=2];
```
对于一个冻结拍而言,一共有五个关键的时间点:左十字,右十字,出拍点(绿线),原按拍点(蓝线),实际按拍点(黄线)。
对于一个冻结拍而言,一共有五个关键的时间点:左十字,右十字,出拍点(绿线),原按拍点(蓝线),实际按拍点(黄线)。
```rdview 4 2
eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
```
- **左十字**和**右十字**会显示两次冰冻特效并分别播放不同的冰冻音效,用于预示拍延长的时间。
- **左十字**和**右十字**会显示两次冰冻特效并分别播放不同的冰冻音效,用于预示拍延长的时间。
- **绿线**代表单发拍会在此响一次。
- **蓝线**处会将人物冻成冰块,再显示一次冰冻特效并播放一次冰冻音效,表示拍延长的开始。
- **黄线**处拍会解冻并播放最后一次冰冻音效,也是玩家应该按拍的时机。
- **蓝线**处会将人物冻成冰块,再显示一次冰冻特效并播放一次冰冻音效,表示拍延长的开始。
- **黄线**处拍会解冻并播放最后一次冰冻音效,也是玩家应该按拍的时机。
- **绿线**与**蓝线**之间的间隔就是`拍长`的时间。
- **左十字**与**右十字**或者**蓝线**与**黄线**之间的间隔就是`延迟`的时间。
- **左十字**与**蓝线**或者**右十字**与**黄线**之间的间隔就是`间隔`的时间。
- 以前由于冻结拍尚不完善导致**绿线**不允许夹在**两个十字**之间,现在这种操作已经允许了。
- 以前由于冻结拍尚不完善导致**绿线**不允许夹在**两个十字**之间,现在这种操作已经允许了。
若设计冻结拍,还必须满足以下两个时间点正确:
若设计冻结拍,还必须满足以下两个时间点正确:
- **蓝线**所在时间应在正常单发拍的按拍位置上;
- **左十字**必须和上一个普通单发拍的按拍位置重合。
- **蓝线**所在时间应在正常单发拍的按拍位置上;
- **左十字**必须和上一个普通单发拍的按拍位置重合。
```rdview 5 3
eb.AddOneshotBeat 1[];
@@ -280,32 +280,44 @@ eb.AddOneshotBeat 1 1[grayscale=100,offset_grayscale=80];
eb.AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80];
```
## 灼热拍子🔥
如果需要在冻结拍上叠加多个拍子来凸显鼓点,可以将他们的按拍点放在同一位置。
与**冻结拍子**相反,**灼热拍子**会形成一股热浪,使下一拍的按拍位置提前一些。
```rdview 5 3
eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
g 2 -1 2 2 #0000[offset=0 1]{
eb.AddOneshotBeat 0.5 1[];
g 0 -2 2 3 #0000[offset=0 1]{
eb.AddOneshotBeat -1 3[tick=2.5];
};
};
```
第五章更新后,灼热拍子在`5-2N`首次出现。
## 灼热拍🔥
在修饰符中选中火焰符号`🔥`,就创建了一个灼热拍子。创建一个灼热拍子后,会多出“间隔”选项
与**冻结拍**相反,**灼热拍**会形成一股热浪,使下一拍的按拍位置提前一些
第五章更新后,灼热拍在`5-2N`首次出现。
在修饰符中选中火焰符号`🔥`,就创建了一个灼热拍。创建一个灼热拍后,会多出“间隔”选项。
![](../images/moreoneshotbeats-burnshot.png)
类似地,对于一个灼热拍,一共有四个关键的时间点:左十字,右十字,出拍点(绿线),按拍点。为作区分且方便表述,将会把按拍点称为红线。
类似地,对于一个灼热拍,一共有四个关键的时间点:左十字,右十字,出拍点(绿线),按拍点。为作区分且方便表述,将会把按拍点称为红线。
```rdview 5 2
eb.AddOneshotBeat 3[tick=0.5,type=burnshot,interval=2];
```
- **左十字**和**右十字**会显示两次热浪特效并分别播放不同的灼热音效,用于预示拍实际的时间。
- **左十字**和**右十字**会显示两次热浪特效并分别播放不同的灼热音效,用于预示拍实际的时间。
- **绿线**处会再显示一次热浪特效并播放一次灼热特效,同时单发拍会在此响一次。
- **红线**处拍会解除灼热状态并播放最后一次灼热音效,也是玩家应该按拍的时机。
- **红线**处拍会解除灼热状态并播放最后一次灼热音效,也是玩家应该按拍的时机。
- **左十字**与**右十字**或者**绿线**与**红线**之间的间隔就是`拍长`的时间。
- **左十字**与**绿线**或者**右十字**与**红线**之间的间隔就是`间隔`的时间。
若设计灼热拍,还必须满足以下两个时间点正确:
若设计灼热拍,还必须满足以下两个时间点正确:
- **绿线**所在时间应在正常单发拍的原定出拍位置上;
- **左十字**必须和上一个普通单发拍的原定出拍位置重合。
- **绿线**所在时间应在正常单发拍的原定出拍位置上;
- **左十字**必须和上一个普通单发拍的原定出拍位置重合。
```rdview 5 3
eb.AddOneshotBeat 1[];
@@ -314,7 +326,19 @@ eb.AddOneshotBeat 1 1[grayscale=100,offset_grayscale=80];
eb.AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80];
```
最后我们针对细分拍子、冻结拍子、灼热拍子做一个技术总结:
如果需要在灼热拍上叠加多个拍子来凸显鼓点,可以将他们的按拍点放在同一位置。
```rdview 5 3
eb.AddOneshotBeat 2[type=burnshot,delay=0.5,interval=2];
g 0 -1 3 2 #0000[offset=0 1]{
eb.AddOneshotBeat 1.5 1[tick=1.5];
g 0 -1 3 3 #0000[offset=0 1]{
eb.AddOneshotBeat 0.5 2[tick=2.5];
};
};
```
最后我们针对细分拍、冻结拍、灼热拍做一个技术总结:
```rdview 19 7
eb.AddOneshotBeat 1[loop=1];
@@ -346,28 +370,28 @@ g 13 5 1 1[offset=1]{
};
```
## 冲击拍
## 冲击拍
**冲击拍**属于一种单发拍,但在通常情况下相比正常的单发拍而言更快,护士在喊口令的同时会伴有新的哨声提示。
**冲击拍**属于一种单发拍,但在通常情况下相比正常的单发拍而言更快,护士在喊口令的同时会伴有新的哨声提示。
第五章更新后,冲击拍`5-X`首次出现。虽然`2-2`中最快的单发拍也相当快,但并不属于冲击拍
第五章更新后,冲击拍在`5-X`首次出现。虽然`2-2`中最快的单发拍也相当快,但并不属于冲击拍。
冲击拍的本质是**护士提示带有哨声的单发拍**,**正拍按拍**以及**快速**是其显著特征,这两点在`5-X`中也有所体现。
冲击拍的本质是**护士提示带有哨声的单发拍**,**正拍按拍**以及**快速**是其显著特征,这两点在`5-X`中也有所体现。
冲击拍除了正常的护士提示之外,开始前后还会分别会伴随四声哨子,顺序分别为**短低短低**和**高高高高**,每声哨子之间的间隔相同,且四声哨子 - 高音效与前四下按拍重合,如下图上半部分。
冲击拍除了正常的护士提示之外,开始前后还会分别会伴随四声哨子,顺序分别为**短低短低**和**高高高高**,每声哨子之间的间隔相同,且四声哨子 - 高音效与前四下按拍重合,如下图上半部分。
同时,仅当面临如下图下半部分特定的冲击像正常单发拍转化时,最后三个冲击拍的按拍会伴随三声哨子,顺序分别为**高高低**。
同时,仅当面临如下图下半部分特定的冲击像正常单发拍转化时,最后三个冲击拍的按拍会伴随三声哨子,顺序分别为**高高低**。
![](../images/moreoneshotbeats-blastbeat.png)
![](/images/13-23.png)
*请注意,灰色的护士提示事件只是为了方便理解单发拍机制,实际只需要放置红色护士语音事件即可。*
虽然`5-X`引入了冲击拍,但有几点仍需注意:
1. **冲击拍依旧是一种单发拍**,其护士提示方式与正常拍长的单发拍基本无异,只不过在拍长较短时需要多加两个 Rea Dy 延长预先提示。
2. **冲击拍并没有正拍按拍以及高速度的强制要求**,只要是一组循环的单发拍,无论正反拍都可以加上哨声提示当作冲击拍使用,只需注意使用场景、合理性与便捷性。
3. 哨声虽然是判定冲击拍的指标,但是**这只是一种新的提示方式**,只是为了让玩家更好地应对单发拍,请勿为了创造冲击拍而随意添加哨声堆砌提示,否则会显得相当冗杂。
虽然`5-X`引入了冲击拍,但有几点仍需注意:
1. **冲击拍依旧是一种单发拍**,其护士提示方式与正常拍长的单发拍基本无异,只不过在拍长较短时需要多加两个 Rea Dy 延长预先提示。
2. **冲击拍并没有正拍按拍以及高速度的强制要求**,只要是一组循环的单发拍,无论正反拍都可以加上哨声提示当作冲击拍使用,只需注意使用场景、合理性与便捷性。
3. 哨声虽然是判定冲击拍的指标,但是**这只是一种新的提示方式**,只是为了让玩家更好地应对单发拍,请勿为了创造冲击拍而随意添加哨声堆砌提示,否则会显得相当冗杂。
简言之,冲击拍并不是什么新东西,只是加了哨子提示的单发拍,仅此而已。尽管在`5-X`里其速度和正反按拍切换令人印象深刻,但只需像正常单发拍一样使用即可。
简言之,冲击拍并不是什么新东西,只是加了哨子提示的单发拍,仅此而已。尽管在`5-X`里其速度和正反按拍切换令人印象深刻,但只需像正常单发拍一样使用即可。
总的来说,冲击拍只是为了符合`5-X`剧情而设计的机制,其提示方式相较于正常单发拍更为复杂,如果有需要可以添加哨声提示以创造出冲击拍,但是只使用护士语音提示也是完全合理的,需根据个人需求进行选择。~~虽然我不推荐用就是了,提示整得那么麻烦还容易出错。~~
总的来说,冲击拍只是为了符合`5-X`剧情而设计的机制,其提示方式相较于正常单发拍更为复杂,如果有需要可以添加哨声提示以创造出冲击拍,但是只使用护士语音提示也是完全合理的,需根据个人需求进行选择。~~虽然我不推荐用就是了,提示整得那么麻烦还容易出错。~~
+36 -7
View File
@@ -1,18 +1,47 @@
# 轨道移动
## 轨道的显示与隐藏
## 默认轨道位置
轨道在关卡进行的过程中是可以随时调整其显示和隐藏状态的,这个功能可在紫色版块的**隐藏轨道**中找到。打开后有三个设置项:
![](../images/moverow-wyym.png)
当我们在同一房间里设置了多个轨道的时候,角色位置会自动生成在屏幕上。不同的轨道数量下默认的位置也会不同:
**轨道**:要显示/隐藏的是哪条轨道。注意一次只能选择一个,所以如果需要大批量地显示和隐藏只能一点点加入事件了。
* 一根轨道在屏幕的`(9.94%, 50.00%)`
* 两根轨道在屏幕的`(9.94%, 60.61%)`,`(9.94%, 39.39%)`
* 三根轨道在屏幕的`(9.94%, 71.21%)`,`(9.94%, 50.00%)`,`(9.94%, 28.79%)`
* 四根轨道在屏幕的`(9.94%, 81.82%)`,`(9.94%, 60.61%)`,`(9.94%, 39.39%)`,`(9.94%, 18.18%)`
**动作**:四个选项(显示/隐藏/仅显示角色/仅显示轨道),显示和隐藏比较好理解;仅显示角色的意思是没有中间的横线和右边的心脏;仅显示轨道的意思是没有左边的人物图。
![](../images/settheme-rowposition.png)
> 如果你想要不显示心脏,最简单的办法是使用移动轨道事件将心脏移到屏幕外。如果想要隐藏所有轨道的心脏,也可以通过调用自定义方法 invisibleHeart = true 来实现,参考[自定义方法](../pages/custommethod.md)。
<div class="theme-positions" id="theme-positions">
<div class="column ccharacter"></div>
<div class="column c1"></div>
<div class="column c2"></div>
<div class="column c3"></div>
<div class="column c4"></div>
<div class="column c5"></div>
<div class="column ccenter"></div>
<div class="column c6"></div>
<div class="column c7"></div>
<div class="column cheart"></div>
<div class="row r1"></div>
<div class="row r2"></div>
<div class="row r3"></div>
<div class="row r4"></div>
<div class="row r5"></div>
<div class="row r6"></div>
<div class="row r7"></div>
<div class="selectbox"></div>
<div class="infobox">
<p class="per"><span class="x"></span>, <span class="y"></span></p>
<p class="px"><span class="x"></span>, <span class="y"></span></p>
<p class="other"></p>
</div>
</div>
**过渡方式**:顺滑(有烟雾特效);即刻(瞬间显示/隐藏);全动画(仅限“动作”中选择“显示”,有完整的出场及轨道延伸动画)。
此处的坐标原点为屏幕左下角,轨道的参考位置是人物像素画的中心点。如果任其按默认位置排布,会显得有些缺乏设计感。
![](../images/settheme-4rows.png)
## 轨道的移动
+1 -1
View File
@@ -29,7 +29,7 @@ eb.AddOneshotBeat 5 1[tick=0.75,loop=3,interval=1];
## 单发拍子护士提示
单发拍子与普通拍子不同之处在于,普通拍子可以观察前六次脉冲,推测出第拍的正确按键时机;但单发拍子只有一次脉冲,玩家无法仅通过单发拍子的脉冲得知按键时机。因此,为了能让玩家正确击打单发拍子,**必须为单发拍子添加护士提示**。
单发拍子与普通拍子不同之处在于,普通拍子可以观察前六次脉冲,推测出第拍的正确按键时机;但单发拍子只有一次脉冲,玩家无法仅通过单发拍子的脉冲得知按键时机。因此,为了能让玩家正确击打单发拍子,**必须为单发拍子添加护士提示**。
![护士语音设置](../images/oneshotbeat-whatshouldnursesay.png)
-85
View File
@@ -1,85 +0,0 @@
# 房间
完全掌握了节奏医生的节奏设计之后,我们要把目光放在谱面的视觉呈现上了,这一节我们就从理解“房间”的概念开始。
编辑器总共支持四个房间,你可以想象成一栋房子有四层楼,每层楼最多住四个人,同时,每层楼的装修可以随意摆设互不干扰。这四层楼不一定刚刚好叠在一起,可以随时移动,互不交叠,只是高度不同。我们飞在空中从上往下看,就能看到目视所及范围内的大楼景象。同时,如果空中飘扬着其他物件,也会被我们的眼睛捕捉到,如果大楼的天花板和地板是透明的,那么地基里有什么东西也会被看到。
当我们使用**排列显示房间**事件时,房间就只能从上到下地排列在屏幕上,而如果使用**移动房间**事件,则可以随便移动。
## 排列显示房间
![排列房间设置(左)与实际效果(右)](../images/room-visible.png)
将左边的可见房间点亮为黄色,屏幕上就会显示这个房间,高度可以自定义,填写数据后房间高度将自动分配。详细的分配规则点击下方箭头展开。如果你懒得理解分配规则,只需永远保证 **不点亮多余的房间,并且将存在的房间高度总和设置到100%** 即可。
### 房间分配规则
- 如果全部不填,那么
所有房间高度为 `25%`
- 否则
- 计算已填房间高度的总和 `Sum`
- 如果 `Sum = 100%`,那么
按实际数据设置房间高度,未填写的视为 `0%`
- 如果 `Sum < 100%`,那么
- 如果有未填写房间高度的房间,设个数为 `n`,那么
未填写高度的房间高度 `height = (100% - Sum) / n`,即均分未分配的房间高度;
- 否则
每个房间的房间高度 `height = height * 100% / Sum`,即等比例拉伸房间高度至高度总和为 `100%`
- 如果 `Sum > 100%`,那么
每个房间的房间高度 `height = height * 100% / Sum`,即等比例缩小房间高度至高度总和为 `100%`
下面还留有一个时长和缓速的选项,这个和轨道移动的时所介绍的设置规律是完全相同的。如果填写0拍就是瞬间完成房间分配。
> 如果你想直观地看看各种移动方式的速度区别,可以[看看这里](https://easings.net/zh-cn)。
## 房间的前后关系
但实际上很多时候我们并不想就让房间这样死板地上下排布,甚至想让这些房间交叠在一起。要做到这一点,我们得先明确各个房间叠在一起的时候,它们的前后关系是怎样的。
![](../images/room-animation.gif)
首先看一个房间内,各种事件的由前往后关系是:
> [浮动文字]
[弹幕效果]
[前景图片]
[轨道4]
[轨道3]
[轨道2]
[轨道1]
[背景图片(图片)]
[背景图片(颜色)]
它们的前后顺序是不可改变的。
各个房间整体之间又有前后顺序,默认由前往后关系是:
> [显示状态牌]
[应用到:顶部]
[房间1]
[房间2]
[房间3]
[房间4]
四个房间的顺序是可以改变的,只需要在黄色版块内添加一个**重新排序房间**事件,把前后顺序拖动到想要的即可。
![](../images/room-layer.png)
## 激活其他房间
在刚打开编辑器的状态下,实际上只能看到房间1。并不是另外三个房间不存在,而是房间大小被设置成了`0%`。在黄色版块中找到**移动房间**事件,事件放置在工作区的哪一行,就是对应的哪个房间被移动。创建三个移动房间事件置于234行,将“大小”设置为`(100%,100%)`,这样所有的房间都以屏幕大小叠在一起了。
还记得[这里](../pages/settheme.md)提到了**设置主题**事件吗,我们再添加一个“花房·昼”。由于黄色版块不再是空白的,设置主题的事件的右上角出现了新的按钮,这是将事件设置到具体房间的功能。
![](../images/room-theme.png)
将带有花房·昼主题的事件选择房间4,预览效果会发现我们看到的依然是武士电子,因为现在不透明的房间1完全遮住了房间4。删掉武士电子主题,会发现背景全黑,依然看不到花房。这是因为默认状态下房间并不是透明背景,而是纯黑背景。
在紫色的版块找到**设置背景**事件,将背景颜色选择为纯透明,应用到123房间。再次预览,终于能看到房间4了。“应用到”选项是可以多选的。
![](../images/room-eventselectroom.png)
有时还会有第五个按钮“顶部”,如果你理解了本节最开头的比喻的话,应用到顶部的事件是直接在大楼与你之间的空中生效的。房间怎么移动都不会影响到顶部。
![](../images/room-eventsettings.png)
经此示例,相信你已经能理解房间的基本运作原理了。
+5
View File
@@ -1,5 +1,10 @@
# 房间视效
## 房间透明度
**淡入淡出房间**事件倒是没有太多可说的,调节的是房间整体的透明度。同样也可以选择时长和缓速模式。
最基本的用法就是把一个房间的透明度从`0`调到`100`,另一个从`100`调到`0`,完成淡入淡出的转场。
## 房间遮罩
### 图片遮罩
+15
View File
@@ -56,6 +56,21 @@
![](../images/rowfx-heighlight.gif)
## 轨道的显示与隐藏
轨道在关卡进行的过程中是可以随时调整其显示和隐藏状态的,这个功能可在紫色版块的**隐藏轨道**中找到。打开后有三个设置项:
![](../images/moverow-wyym.png)
**轨道**:要显示/隐藏的是哪条轨道。注意一次只能选择一个,所以如果需要大批量地显示和隐藏只能一点点加入事件了。
**动作**:四个选项(显示/隐藏/仅显示角色/仅显示轨道),显示和隐藏比较好理解;仅显示角色的意思是没有中间的横线和右边的心脏;仅显示轨道的意思是没有左边的人物图。
> 如果你想要不显示心脏,最简单的办法是使用移动轨道事件将心脏移到屏幕外。如果想要隐藏所有轨道的心脏,也可以通过调用自定义方法 invisibleHeart = true 来实现,参考[自定义方法](../pages/custommethod.md)。
**过渡方式**:顺滑(有烟雾特效);即刻(瞬间显示/隐藏);全动画(仅限“动作”中选择“显示”,有完整的出场及轨道延伸动画)。
## 适合配合轨道或节拍一起使用的特效
**闪烁:**屏幕白色闪烁,分为短,中,长三种。
+39 -40
View File
@@ -1,45 +1,5 @@
# 场景与主题
当我们在同一房间里设置了多个轨道的时候,角色位置会自动生成在屏幕上。不同的轨道数量下默认的位置也会不同:
* 一根轨道在屏幕的`(9.94%, 50.00%)`
* 两根轨道在屏幕的`(9.94%, 60.61%)`,`(9.94%, 39.39%)`
* 三根轨道在屏幕的`(9.94%, 71.21%)`,`(9.94%, 50.00%)`,`(9.94%, 28.79%)`
* 四根轨道在屏幕的`(9.94%, 81.82%)`,`(9.94%, 60.61%)`,`(9.94%, 39.39%)`,`(9.94%, 18.18%)`
![](../images/settheme-rowposition.png)
<div class="theme-positions" id="theme-positions">
<div class="column ccharacter"></div>
<div class="column c1"></div>
<div class="column c2"></div>
<div class="column c3"></div>
<div class="column c4"></div>
<div class="column c5"></div>
<div class="column ccenter"></div>
<div class="column c6"></div>
<div class="column c7"></div>
<div class="column cheart"></div>
<div class="row r1"></div>
<div class="row r2"></div>
<div class="row r3"></div>
<div class="row r4"></div>
<div class="row r5"></div>
<div class="row r6"></div>
<div class="row r7"></div>
<div class="selectbox"></div>
<div class="infobox">
<p class="per"><span class="x"></span>, <span class="y"></span></p>
<p class="px"><span class="x"></span>, <span class="y"></span></p>
<p class="other"></p>
</div>
</div>
此处的坐标原点为屏幕左下角,轨道的参考位置是人物像素画的中心点。如果任其按默认位置排布,会显得有些缺乏设计感。
![](../images/settheme-4rows.png)
所以在设计全程静止不动的轨道位置的时候,我们应该在一开始就让他们站在合适的位置上,并且最好能契合背景画面。下面我们拿官方自带的一些场景来举例子。使用这些场景需要在紫色版块里面添加**设置主题**,再选择“花房·昼”。然后在时间轴最开始的位置设好三个人物的轨道移动。
这样三个角色都**站在地上**并且有一定的交错。没错,这就是`3-1`沉睡花园。
@@ -57,3 +17,42 @@
甚至轨道可以大部分位置都不可见。比如制作只显示脉冲心跳部位的单发拍子,并做成纯色等。
> 调用自定义方法`invisibleHeart = true`可以让轨道上的心脏消失。如果不选择角色,那么就只剩下一根线了,在一些极简色块画风的关卡里效果拔群。
## 背景和前景图片
在房间的前后关系里我们提到了背景图片和前景图片这两个概念,也就是说,除了使用编辑器内自带的一些主题之外,其实自己也是可以自定义画面的背景和前景图案的。这个功能同样十分灵活,但是如果想要完全靠自己来原创或是临摹背景图片的话,可能需要对画像素画有一些掌握。
选择紫色的版块,放置一个**设置背景**的事件,发现里面有两种形式,一种是纯色,这个比较好理解,唯独要注意的是**纯色的默认值是黑色而不是透明**,另一种是插入图片。
> 插入的图片必须是.png格式,如果你是自己画背景图,建议使用画布大小为352\*198像素。
颜色选项,看似意义不明,但其实也有一定的用处。如果你导入的图片只有纯白和透明两种颜色,这个功能就可以为纯白的部分染色,染成你自己设置的颜色。但是因为一次只能设置一种颜色,使用起来也有些限制。
缩放功能其实类似于操作系统里壁纸的填充方式,具体效果点开小箭头查看。
<details><summary><strong>填充方式</strong></summary>
<img src="../images/assets-fillmode.png" alt="填充方式图例" />
<ul>
<li><strong>填满:</strong>将画面拉伸到横纵全部填满屏幕。要注意的是,如果你塞进去一张高清图,编辑器也会把它缩小到352\*198像素来适应画面,而且这个缩放机制是像素化的,画面会变得非常模糊。</li>
<li><strong>拉伸:</strong>将画面等比拉伸直到左右垂直线对齐屏幕。</li>
<li><strong>等比拉伸:</strong>将画面等比拉伸直到上下水平线对齐屏幕。</li>
<li><strong>居中:</strong>就是按原画面大小放在正中间。</li>
<li><strong>平铺:</strong>像铺瓷砖一样铺满屏幕。</li>
</ul>
</details>
> 如果你想做一个动图效果,需要把.gif拆解成一连串的.png图片,在选择图片时同时选择所有的图点击确定。接下来就会多出一个设置帧数的选项,来确定动图的播放速度。帧数 = \(背景图总画面数 / 一个循环占节拍数量\) \* 曲速/60。
设置背景一样,但使用这个功能的主要目的还是为了能产生一个图片覆盖轨道和手的效果,这样在画面效果的处理上可以更加灵活。
## 平铺背景
唯独平铺选项会多冒出来一个速度设置,它可以让背景图片始终保持设置的速度移动。
* 在滚屏模式下,效果类似于<code>4-2</code>的天空在慢慢移动,速度和缓速则是调整背景从静止到目标速度的变化过程。
* 在脉冲模式下,背景的移动速度变化就是周期性的,用<code>间隔</code>来调整周期。另外,如果你想设计这样的移动背景,在作画的时候也需要考虑画面左右两端和上下两端的连续,否则画面运动起来会感觉到割裂。
举个例子,公路,山坡,白云,蓝天的运动速度都不一样,毕竟在画面中的远近关系不一样,把他们分别画在四个背景图片里,放在四个房间,然后设置四个移动房间事件,把位置都改为`(50%, 50%)`,把大小都改为`(100%, 100%)`,设置四个背景图片,使用平铺的方式,设置不同的水平移动速度,就得到了在公路上慢慢前进的效果。
![灵 魂 公 路](../images/assets-road.gif)
另外,使用小图片像地砖一样平铺才是更为原本的用法。
+59 -29
View File
@@ -30,7 +30,7 @@
老话说得好,知其然也要知其所以然。json文件也是可以手动写一个的,只要你能明白文件的编排方式就行。如果需要使用,只需要把这一堆复制下来保存到.txt中,数据按自己的来改动,最后把文件后缀名改成.json即可。**注意这个文件的文件名必须和精灵图的完全一致**。模板在下面:
<details><summary><strong>点我展开</strong></summary>
```json
<pre data-lang="json"><code>
{
"size": [64,64],
"name":"吾夜犹明",
@@ -80,7 +80,6 @@
},
]
}
```
</code></pre>
</details>
@@ -100,34 +99,64 @@
显示这个缩略图的时候,画面正中心偏移多少像素。
- **`"clips"`**
里面是对每个表情分别进行设置。
- **`"name"`**
后续出现的name指的是人物的各个表情,
以下四个表情是默认存在的,因为和按拍效果有关,但你也可以在此基础上增加新的表情,自己命名一个其他表情,这会在紫色版块**播放表情**中有效。
- `neutral`表示普通表情,
- `happy`表示拍子按对时的表情,
- `barely`表示拍子按歪时的表情,
- `missed`表示漏过没按时的表情。
- **`"frames"`**
这是每种状态所涉及的帧数。在精灵图里从左往右,从0开始计算。例如:
- `[0,1]`:第1张和第2张组合成两帧的动图;
- `[3,5]`:第4张和第6张组合成两帧的动图;
- `[0,1,1,1]`第1张和第2张组合成四帧的动图,其中第2张播放三次。
- **`"loop"`**
动图的循环模式,如果是`no`那么播放完就会自动回到neutral状态,如果是`onbeat`就会一直循环。
- **`"loopStart"`**
如果设置为循环,首先按列表播放一次完整动画,接着再从列表内第loopstart+1项播放到结尾,并以此为循环。打比方做了一个5帧的骑车的动画`[助力,上车,坐下,蹬左脚,蹬右脚]`,loopstart:3,则实际效果为骑上车后循环骑车动作。
- **`"fps"`**
动图播放的帧数,填大于0的帧数,就是按填写的帧数播放;填0就自动随着音乐bpm改变
- **`"portaitOffset"`**
截取动图中的一部分作为显示对话时的立绘,两个数字表示截取立绘的起始位置。以每一帧画面的左下角作为原点,第一个数字为横坐标,第二个数字为纵坐标。
- **`"portaitSize"`**
截取动图中的一部分作为显示对话时的立绘,两个数字表示截取立绘图片的像素大小。第一个数字为立绘的宽度,第二个数字为立绘的高度。
- **`"portaitScale"`**
图片放大比例,这是显示对话时的立绘用的,因为立绘画面一般都只是人物的头部和上身,且立绘窗口也比较大,这里最好填写大于1的数字
- **`"name"`**
后续出现的name指的是人物的各个表情,
以下四个表情是默认存在的,因为和按拍效果有关,但你也可以在此基础上增加新的表情,自己命名一个其他表情,这会在紫色版块**播放表情**中有效。
- `neutral` 表示普通状态下的默认表情,
- `happy` 是**击中**时的表情,
- `barely` 是**失误**的表情,
- `missed` 是**错失**的表情。
> `neutral` 表情是**必需**的,无论是轨道角色还是精灵资源。
在早期,以上四个表情在轨道角色上时也是必需的,不过在后来的版本中移除了其他三个表情的限制。
此字段需要满足[这些要求](../pages/spritesheet.md#自定义角色的表情机制)。
- **`"frames"`**
这是每种状态所涉及的帧数。在精灵图里从左往右,从0开始计算。例如:
- `[0,1]`:第1张和第2张组合成两帧的动图;
- `[3,5]`:第4张和第6张组合成两帧的动图;
- `[0,1,1,1]`第1张和第2张组合成四帧的动图,其中第2张播放三次。
- **`"loop"`**
动图的循环模式
- `no`:播放一次回到 neutral 状态
- `yes`:按 `fps` 值循环。
- `onBeat`:按节拍循环。
> 当 `loop` 值不为 `onBeat``neutral` 表情没有帧时会导致精灵缩放效果在播放至第三拍时停止(无论是从哪个小节开始)。这是一个已知 bug。
- **`"loopStart"`**
在每轮循环帧播放完毕后回到 `frames` 中此值所指处的帧继续播放。
例:一个 5 帧的动画`[5,6,7,8,9]`loopstart3,实际效果为 `[5,6,7,8,9,8,9,8,9,8,9,...]`
- **`"fps"`**
播放帧率,填大于0的帧率,就是按填写的帧率播放;填0就自动随着音乐bpm改变。可以为小数。
- **`"portaitOffset"`**
截取动图中的一部分作为显示对话时的立绘,两个数字表示截取立绘的起始位置。以每一帧画面的左下角作为原点,第一个数字为横坐标,第二个数字为纵坐标。
- **`"portaitSize"`**
截取动图中的一部分作为显示对话时的立绘,两个数字表示截取立绘图片的像素大小。第一个数字为立绘的宽度,第二个数字为立绘的高度。
- **`"portaitScale"`**
图片放大比例,这是显示对话时的立绘用的,因为立绘画面一般都只是人物的头部和上身,且立绘窗口也比较大,这里最好填写大于1的数字。
## 回到编辑器添加人物
## 自定义角色的音效
一切准备就绪,现在进入编辑器添加一个人物,然后在人物设置里面点一下文件夹图标,选择刚才我们写好的.json文件。
游戏在运行到**显示对话**事件时,如果遇到对应的表情 [ID](../pages/spritesheet.md#自定义角色的表情机制) 时,会尝试:
- 寻找此自定义角色的同名 ID 音频文件
- 若无,寻找此自定义角色的默认 ID 音频文件
- 若无,设为默认对话音频文件。
游戏会尝试在对话框内每弹出一个字就播放一次。
## 自定义角色的表情机制
一个自定义角色可以拥有多个表情,每个表情都对应一个 ID,
为便于理解,这里假设我们的精灵名为 `吾夜犹明`
ID 的规则如下:
- `neutral` 表情对应无后缀的精灵名,即 `吾夜犹明`
- 其他表情对应 `[精灵名]_[表情名]`,例如 `happy` 对应 `吾夜犹明_happy`
此规则会影响**显示对话**事件的角色表情选取和音效文件选取。
这也是为什么表情名不能含有 `_` 和文件名不合法字符。
## 回到编辑器添加自定义角色
一切准备就绪,现在进入编辑器添加一个自定义角色,然后在人物设置里面点一下文件夹图标,选择刚才我们写好的.json文件。
![](../images/spritesheet-openfile.png)
@@ -148,7 +177,8 @@
## 选择立绘
在[这里](../pages/captions.md#显示对话)我们已经提到过,显示对话的对话框中手动写下立绘名字也是可以插入立绘的。如果我们在这里导入一个文件名为“吾夜犹明”的猫猫,然后在对话框的开头写下`吾夜犹明:`,那么播放的立绘就是默认的neutral那一项。**依然需要注意是英文的冒号**。如果想要选择其他表情作为立绘,需要额外写一个下划线,后面加上表情名,例如`吾夜犹明_happy:``吾夜犹明_barely:``吾夜犹明_missed:`
在[这里](../pages/captions.md#显示对话)我们已经提到过,显示对话的对话框中手动写下立绘名字也是可以插入立绘的。如果我们需要让对话显示立绘,需要在一行中输入 `[表情 ID]:[对话内容]`。表情格式见[表情 ID](../pages/spritesheet.md#自定义角色的表情机制)。
例如 `吾夜犹明_happy:今天真是美好的一天。`
如果我们在json文件中自己添加了名为`Error`自定义表情,例如:
+5 -5
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)
+21
View File
@@ -0,0 +1,21 @@
# 双人模式设计
> 此页面处于初稿阶段,欢迎补充!
## 切换至双人模式
[在编辑器预览栏下](../pages/editorinterface.md)打开双人模式选项,这时候预览画面中的所有玩家操控的轨道会相应地变换。
由玩家 1 操控的轨道的默认颜色变为蓝色(`#0050FF`),心脏处会增加一个蓝色的 P1 图标;
由玩家 2 操控的轨道的默认颜色变为红色(`#AC0929`),心脏处会增加一个红色的 P2 图标。
指示轨道控制者的图标(P1,P2,CPU)都不会受轨道染色事件影响。
在播放关卡过程中会影响以下游戏元素的行为。
- 切换玩家轨道事件是为改变轨道控制者而设计的。将下拉单选框的选项改为`双人模式``单人或双人模式`时,事件就会在双人模式下执行。
- 编辑条件时将类型改为玩家模式,再在下面的玩家模式中选择双人,即可使应用了这个条件的事件仅在双人模式下执行。
## 多轨道设计
官方在 v0.18.0 时重新设计了大量关卡的双人模式版本。
在设计双人模式关卡时,最核心的要点就是:需要让玩家分清他所控制的轨道。这同时也适用于在关卡中将某个轨道托付给 Otto 时的情况。
在为单发拍使用护士提示时需要用两种护士音效以区分玩家,通常为 P1 使用的语音为**护士**,P2 使用的语音为**伊恩**。你也可以根据实际场景灵活变通。
轨道的音效使用左右声道作区分(例如左声道只播放 P1 需要操控的轨道的音效而右声道相反)也是一个不错的选择。
当 P1、P2 交换轨道控制权时,需要使用护士语音 `Switch` 以提示即将交换轨道控制权。
+22 -12
View File
@@ -29,9 +29,9 @@
可写变量与只读变量类似,其参数可以随游戏进程变化,与只读变量不同的是,可写变量的值可人为更改。可写变量大致可分为三类:
> 1. **整型**(int):整型变量可以存储整数,变量名写作i0、i1、...、i9。
> 2. **浮点型**(float):浮点型变量可以存储浮点数(也就是小数。特别地,形如1.0的值为浮点数),变量名写作f0、f1、...、f9。
> 3. **布尔型**boolean, 也可简写为 bool):布尔型变量可以存储布尔值。布尔值包含且仅包含True(真)和False(伪)。布尔型变量名写作b0、b1、...、b9。
- **整型**(int):整型变量可以存储整数,变量名写作i0、i1、...、i9。
- **浮点型**(float):浮点型变量可以存储浮点数(也就是小数。特别地,形如1.0的值为浮点数),变量名写作f0、f1、...、f9。
- **布尔型**boolean/bool):布尔型变量可以存储布尔值。布尔值包含且仅包含True(真)和False(伪)。布尔型变量名写作b0、b1、...、b9。
需要注意的是上述三类变量,每类有且仅有所列举的10个,且无法修改变量名。一般地,整形变量的值属于区间[-2147483648, 2147483647],而浮点型变量取值范围相对广泛。
@@ -55,14 +55,24 @@
| < | 小于 |
| > | 大于 |
> 为了保留更高的兼容性,参与计算的数据都会转换为浮点数,也就意味着:
> - int 和 bool 实际上都会转换为 float
> - 转换为 int 的操作是四舍五入,并且偏好于最临近的偶数,例如:
> - `i0 = 2.75` => i0: 3
> - `i0 = 2.5` => i0: 2
> - `i0 = 3.5` => i0: 4
> - 转换为 bool 的操作是非 0 即为 True,例如:
> - `b0 = -3` => b0: True
> - `b0 = -0.0` => b0: False
变量可以在调用自定义方法;浮动文字、显示对话、显示状态牌;条件中使用。
> 1. **调用自定义方法**
> 可以在调用自定义方法的表达式框里对变量进行读写、赋值,可使用赋值运算符。
> 例如:i1++(使i1的值+1),i4=4(将i4的值赋为4)
> 2. **浮动文字、显示对话、显示状态牌**
> 变量可以嵌入到这些事件的显示内容中,使用时将表达式用大括号包裹起来,可使用算数运算符连接。
> 例如:“武士有{i0}个朋友,洛根比武士多{i1}个朋友,所以洛根有{i0+i1}个朋友。”这句话在i0==4且i1==6时输出为“武士有4个朋友,洛根比武士多6个朋友,所以洛根有10个朋友。”
> 3. **条件**
> 变量可用于自定义类型条件的表达式框中。通常与比较运算符结合使用。
> 例如:i0==10i0等于10)、i5<i0i5小于i0
- **调用自定义方法**
可以在调用自定义方法的表达式框里对变量进行读写、赋值,可使用赋值运算符。
例如:i1++(使i1的值+1),i4=4(将i4的值赋为4)
- **浮动文字、显示对话、显示状态牌**
变量可以嵌入到这些事件的显示内容中,使用时将表达式用大括号包裹起来,可使用算数运算符连接。
例如:“武士有{i0}个朋友,洛根比武士多{i1}个朋友,所以洛根有{i0+i1}个朋友。”这句话在i0==4且i1==6时输出为“武士有4个朋友,洛根比武士多6个朋友,所以洛根有10个朋友。”
- **条件**
变量可用于自定义类型条件的表达式框中。通常与比较运算符结合使用。
例如:i0==10i0等于10)、i5\<i0i5小于i0
+6 -67
View File
@@ -1,72 +1,11 @@
# 视觉干扰
# 基础 VFX 效果
> 在关卡中出现对比度较高画面快速交替,如红蓝、黑白快速闪烁,必须开启癫痫警告。使用“放克”特效,必须开启癫痫警告。使用“干扰”特效较多,建议开启癫痫警告。一秒出现3次以上快速闪烁,建议开启癫痫警告。
## 震动
> 即使开启了癫痫警告,也不要设计出会伤害视力的干扰效果。
## 闪光
## 综述
## 预设 VFX
视觉干扰效果算是节奏医生做谱的一大特色了。因为游戏玩法本身是可以做到纯音押的,所以在视觉上对玩家进行考验不仅可以增大难度,同时又可以有很新奇的观赏效果。在剧情模式中,视觉干扰主要集中在`1-X 东方失眠夜``1-XN 超·东方失眠夜`两关,其余还有关卡的小部分环节用到了视觉干扰,效果都极具震撼。
## 反色
需要注意的是,**视觉干扰不等于读谱不能**,更不等于塞爆。这些干扰是为重复或单一的节奏增添趣味与挑战性而添加的,虽然一定意义上增加了读谱难度,但是仍应当给玩家留有恰当的反应时间。如果节奏设计本身就有难度,还刻意阻挡玩家进行识谱,效果将会是适得其反的。
另外,视觉干扰设计的主要原则依然是采音。音乐具有这样的特色,且不好转变为轨道节奏的话,就把这部分采音对应到视觉的同步变化和卡顿,完美地解决节奏设计与画面设计两个问题。
## 卡带效果
如果你对剧情关卡`1-X`东方不眠夜还有印象的话,或许能回忆起节奏配合着音乐一卡一卡的效果,这就是靠**卡带效果**做到的,它可以让心跳在某一拍卡住。
![](../images/vfx-settings.jpg)
这样看图似乎也不够直观,我们举几个实例来看看是怎么做到的:
![拍子源 2 长度 0.25 循环 3 的卡带效果](../images/vfx-witch.gif)
能看到其中一个单发拍子拍下空格之后蓝条是**反复闪烁了3次**的,这就是循环的作用;而这三次闪烁之间的**时间间隔均为0.25拍**,这就是长度的作用;循环的是该小节**第2拍起头时的画面**,这就是拍子源的作用。
![拍子源 1 长度 0.25 循环 7 的卡带效果 + 拍子源 3 长度 0.5 循环 1 的卡带效果](../images/vfx-electronicgirlrotate.gif)
这里的音乐段是一个密集的鼓点,于是就按照鼓点的节奏同步地设计了卡带效果,并且反复。这个卡带的位置刚好放置了一个轨道的旋转,进而做到了轨道卡壳转不动的效果。
这似乎也给了我们一个很好的提示:卡带效果往往都是和音乐中的配器节奏同步的,画面跟随着音乐一起卡才能卡在一个最爽的位置。密集的采音往往本身也就不适合用来做节奏,将其转化为卡带效果反而能够出奇制胜。
## 设置速度
**设置速度**事件修改的是画面的播放速度,但注意,音乐速度并不会跟着改变,改变的只有设置主题里的动画速度,平铺背景的移动速度,所有带有“动画时长”设置的事件的变化速度等。
其中带有“动画时长”设置的事件(例如轨道移动,浮动文字)是要特别注意的,例如你填写了动画时长为`2拍`,同时又设置速度为`2倍`,那么该事件就在1拍的时间里完成了。
设置速度事件可以填写小数,并且最低可以修改为`0`。当速度为0时画面就会静止,但很邪门的是轨道上的人物和节奏脉冲是对此免疫的,要注意考虑好是否隐藏这些轨道以免造成违和感。
![频繁地设置速度为 0 与正常值交替带来卡机效果](../images/vfx-electronicgirlglich.gif)
## 预设特效的多重叠合
很多预设特效都自带一点画面干扰的倾向,而将这些效果叠加就能加剧干扰效果,如果再叠加上一节所述的镜厅效果则会更加一发不可收拾。堆预设特效可以玩玩,但也要适度。
除镜厅外,放克特效也是要谨慎使用的,过大的饱和度会让眼部急剧不适,往往可以放在整个场景的最底层空区域进行一个小面积适度的铺设。
下面举几个例子:
**竖直滚屏+VHS+电影噪点+高光**
带来信号干扰、丢失的感觉
**顶部镜厅+摄像头/房间动画**
旋转,跳跃,残影,你画面拉出了完美的拖尾
**屏幕翻转+颜色反转+滚屏**
画面你别乱跑
**短暂的噪点/干扰**
默数拍子开启
**放克+落雨+灰度+慢游戏速度**
听话,别把灰度去掉
**25%白色前景闪烁+暴风雪+雪花+VHS+关闭竖直滚屏+电影噪点+JPEG失真+落雨+快游戏速度**
最后的绝望(《冰与火之舞》 “XH-X 最后的希望”类似效果)
## 镜像
+72
View File
@@ -0,0 +1,72 @@
# 视觉干扰
> 在关卡中出现对比度较高画面快速交替,如红蓝、黑白快速闪烁,必须开启癫痫警告。使用“放克”特效,必须开启癫痫警告。使用“干扰”特效较多,建议开启癫痫警告。一秒出现3次以上快速闪烁,建议开启癫痫警告。
> 即使开启了癫痫警告,也不要设计出会伤害视力的干扰效果。
## 综述
视觉干扰效果算是节奏医生做谱的一大特色了。因为游戏玩法本身是可以做到纯音押的,所以在视觉上对玩家进行考验不仅可以增大难度,同时又可以有很新奇的观赏效果。在剧情模式中,视觉干扰主要集中在`1-X 东方失眠夜``1-XN 超·东方失眠夜`两关,其余还有关卡的小部分环节用到了视觉干扰,效果都极具震撼。
需要注意的是,**视觉干扰不等于读谱不能**,更不等于塞爆。这些干扰是为重复或单一的节奏增添趣味与挑战性而添加的,虽然一定意义上增加了读谱难度,但是仍应当给玩家留有恰当的反应时间。如果节奏设计本身就有难度,还刻意阻挡玩家进行识谱,效果将会是适得其反的。
另外,视觉干扰设计的主要原则依然是采音。音乐具有这样的特色,且不好转变为轨道节奏的话,就把这部分采音对应到视觉的同步变化和卡顿,完美地解决节奏设计与画面设计两个问题。
## 卡带效果
如果你对剧情关卡`1-X`东方不眠夜还有印象的话,或许能回忆起节奏配合着音乐一卡一卡的效果,这就是靠**卡带效果**做到的,它可以让心跳在某一拍卡住。
![](../images/vfx-settings.jpg)
这样看图似乎也不够直观,我们举几个实例来看看是怎么做到的:
![拍子源 2 长度 0.25 循环 3 的卡带效果](../images/vfx-witch.gif)
能看到其中一个单发拍子拍下空格之后蓝条是**反复闪烁了3次**的,这就是循环的作用;而这三次闪烁之间的**时间间隔均为0.25拍**,这就是长度的作用;循环的是该小节**第2拍起头时的画面**,这就是拍子源的作用。
![拍子源 1 长度 0.25 循环 7 的卡带效果 + 拍子源 3 长度 0.5 循环 1 的卡带效果](../images/vfx-electronicgirlrotate.gif)
这里的音乐段是一个密集的鼓点,于是就按照鼓点的节奏同步地设计了卡带效果,并且反复。这个卡带的位置刚好放置了一个轨道的旋转,进而做到了轨道卡壳转不动的效果。
这似乎也给了我们一个很好的提示:卡带效果往往都是和音乐中的配器节奏同步的,画面跟随着音乐一起卡才能卡在一个最爽的位置。密集的采音往往本身也就不适合用来做节奏,将其转化为卡带效果反而能够出奇制胜。
## 设置速度
**设置速度**事件修改的是画面的播放速度,但注意,音乐速度并不会跟着改变,改变的只有设置主题里的动画速度,平铺背景的移动速度,所有带有“动画时长”设置的事件的变化速度等。
其中带有“动画时长”设置的事件(例如轨道移动,浮动文字)是要特别注意的,例如你填写了动画时长为`2拍`,同时又设置速度为`2倍`,那么该事件就在1拍的时间里完成了。
设置速度事件可以填写小数,并且最低可以修改为`0`。当速度为0时画面就会静止,但很邪门的是轨道上的人物和节奏脉冲是对此免疫的,要注意考虑好是否隐藏这些轨道以免造成违和感。
![频繁地设置速度为 0 与正常值交替带来卡机效果](../images/vfx-electronicgirlglich.gif)
## 预设特效的多重叠合
很多预设特效都自带一点画面干扰的倾向,而将这些效果叠加就能加剧干扰效果,如果再叠加上一节所述的镜厅效果则会更加一发不可收拾。堆预设特效可以玩玩,但也要适度。
除镜厅外,放克特效也是要谨慎使用的,过大的饱和度会让眼部急剧不适,往往可以放在整个场景的最底层空区域进行一个小面积适度的铺设。
下面举几个例子:
**竖直滚屏+VHS+电影噪点+高光**
带来信号干扰、丢失的感觉
**顶部镜厅+摄像头/房间动画**
旋转,跳跃,残影,你画面拉出了完美的拖尾
**屏幕翻转+颜色反转+滚屏**
画面你别乱跑
**短暂的噪点/干扰**
默数拍子开启
**放克+落雨+灰度+慢游戏速度**
听话,别把灰度去掉
**25%白色前景闪烁+暴风雪+雪花+VHS+关闭竖直滚屏+电影噪点+JPEG失真+落雨+快游戏速度**
最后的绝望(《冰与火之舞》 “XH-X 最后的希望”类似效果)
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
+31
View File
@@ -0,0 +1,31 @@
const customMethods = [
{ type: 'mod',}
]
const editDistance = (a, b) => {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1),
);
}
}
}
return matrix[b.length][a.length];
}
+32
View File
@@ -0,0 +1,32 @@
class CustomMethods{
type: "mod"|"surgery"|"comment"|"custom_method";
}
function editDistance(a: String, b: String): number {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix: number[][] = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1),
);
}
}
}
return matrix[b.length][a.length];
}
export { editDistance };
+1 -1
View File
@@ -5,5 +5,5 @@
* (c) 2017-2023 JP Erasmus <jperasmus11@gmail.com>
* MIT license
*/
!function(){"use strict";function e(o){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(o)}!function(e,o){void 0===o&&(o={});var t=o.insertAt;if(e&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],c=document.createElement("style");c.type="text/css","top"===t&&n.firstChild?n.insertBefore(c,n.firstChild):n.appendChild(c),c.styleSheet?c.styleSheet.cssText=e:c.appendChild(document.createTextNode(e))}}(".docsify-copy-code-button,.docsify-copy-code-button>span{cursor:pointer;transition:all .25s ease}.docsify-copy-code-button{background:grey;background:var(--theme-color,grey);border:0;border-radius:0;color:#fff;font-size:1em;opacity:0;outline:0;overflow:visible;padding:.65em .8em;position:absolute;right:0;top:0;z-index:1}.docsify-copy-code-button>span{background:inherit;border-radius:3px;pointer-events:none}.docsify-copy-code-button>.error,.docsify-copy-code-button>.success{font-size:.825em;opacity:0;padding:.5em .65em;position:absolute;right:0;top:50%;transform:translateY(-50%);z-index:-100}.docsify-copy-code-button.error>.error,.docsify-copy-code-button.success>.success{opacity:1;right:100%;transform:translate(-25%,-50%)}.docsify-copy-code-button:focus,pre:hover .docsify-copy-code-button{opacity:1}.docsify-copy-code-button>[aria-live]{height:1px;left:-10000px;overflow:hidden;position:absolute;top:auto;width:1px}"),document.querySelector('link[href*="docsify-copy-code"]')&&console.warn("[Deprecation] Link to external docsify-copy-code stylesheet is no longer necessary."),window.DocsifyCopyCodePlugin={init:function(){return function(e,o){e.ready((function(){console.warn("[Deprecation] Manually initializing docsify-copy-code using window.DocsifyCopyCodePlugin.init() is no longer necessary.")}))}}},window.$docsify=window.$docsify||{},window.$docsify.plugins=[function(o,t){var n={buttonText:"Copy to clipboard",errorText:"Error",successText:"Copied"};o.doneEach((function(){var o=Array.from(document.querySelectorAll("pre[data-lang]"));t.config.copyCode&&Object.keys(n).forEach((function(o){var c=t.config.copyCode[o];"string"==typeof c?n[o]=c:"object"===e(c)&&Object.keys(c).some((function(e){var t=location.href.indexOf(e)>-1;return n[o]=t?c[e]:n[o],t}))}));var c=['<button class="docsify-copy-code-button">','<span class="label">'.concat(n.buttonText,"</span>"),'<span class="error" aria-hidden="hidden">'.concat(n.errorText,"</span>"),'<span class="success" aria-hidden="hidden">'.concat(n.successText,"</span>"),'<span aria-live="polite"></span>',"</button>"].join("");o.forEach((function(e){e.insertAdjacentHTML("beforeend",c)}))})),o.mounted((function(){var e=document.querySelector(".content");e&&e.addEventListener("click",(function(e){if(e.target.classList.contains("docsify-copy-code-button")){var o="BUTTON"===e.target.tagName?e.target:e.target.parentNode,t=document.createRange(),c=o.parentNode.querySelector("code"),i=o.querySelector("[aria-live]"),r=window.getSelection();t.selectNode(c),r&&(r.removeAllRanges(),r.addRange(t));try{document.execCommand("copy")&&(o.classList.add("success"),i.innerText=n.successText,setTimeout((function(){o.classList.remove("success"),i.innerText=""}),1e3))}catch(e){console.error("docsify-copy-code: ".concat(e)),o.classList.add("error"),i.innerText=n.errorText,setTimeout((function(){o.classList.remove("error"),i.innerText=""}),1e3)}(r=window.getSelection())&&("function"==typeof r.removeRange?r.removeRange(t):"function"==typeof r.removeAllRanges&&r.removeAllRanges())}}))}))}].concat(window.$docsify.plugins||[])}();
!function(){"use strict";function e(o){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(o)}!function(e,o){void 0===o&&(o={});var t=o.insertAt;if(e&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],c=document.createElement("style");c.type="text/css","top"===t&&n.firstChild?n.insertBefore(c,n.firstChild):n.appendChild(c),c.styleSheet?c.styleSheet.cssText=e:c.appendChild(document.createTextNode(e))}}(".docsify-copy-code-button,.docsify-copy-code-button>span{cursor:pointer;transition:all .25s ease}.docsify-copy-code-button{background:grey;background:var(--theme-color,grey);border:0;border-radius:0;color:#fff;font-size:1em;opacity:0;outline:0;overflow:visible;padding:.65em .8em;position:absolute;right:0;top:0;z-index:1}.docsify-copy-code-button>span{background:inherit;border-radius:3px;pointer-events:none}.docsify-copy-code-button>.error,.docsify-copy-code-button>.success{font-size:.825em;opacity:0;padding:.5em .65em;position:absolute;right:0;top:50%;transform:translateY(-50%);z-index:-100}.docsify-copy-code-button.error>.error,.docsify-copy-code-button.success>.success{opacity:1;right:100%;transform:translate(-25%,-50%)}.docsify-copy-code-button:focus,pre:hover .docsify-copy-code-button{opacity:1}.docsify-copy-code-button>[aria-live]{height:1px;left:-10000px;overflow:hidden;position:absolute;top:auto;width:1px}"),document.querySelector('link[href*="docsify-copy-code"]')&&console.warn("[Deprecation] Link to external docsify-copy-code stylesheet is no longer necessary."),window.DocsifyCopyCodePlugin={init:function(){return function(e,o){e.ready((function(){console.warn("[Deprecation] Manually initializing docsify-copy-code using window.DocsifyCopyCodePlugin.init() is no longer necessary.")}))}}},window.$docsify=window.$docsify||{},window.$docsify.plugins=[function(o,t){var n={buttonText:"复制",errorText:"错误",successText:"已复制"};o.doneEach((function(){var o=Array.from(document.querySelectorAll("pre[data-lang]"));t.config.copyCode&&Object.keys(n).forEach((function(o){var c=t.config.copyCode[o];"string"==typeof c?n[o]=c:"object"===e(c)&&Object.keys(c).some((function(e){var t=location.href.indexOf(e)>-1;return n[o]=t?c[e]:n[o],t}))}));var c=['<button class="docsify-copy-code-button">','<span class="label">'.concat(n.buttonText,"</span>"),'<span class="error" aria-hidden="hidden">'.concat(n.errorText,"</span>"),'<span class="success" aria-hidden="hidden">'.concat(n.successText,"</span>"),'<span aria-live="polite"></span>',"</button>"].join("");o.forEach((function(e){e.insertAdjacentHTML("beforeend",c)}))})),o.mounted((function(){var e=document.querySelector(".content");e&&e.addEventListener("click",(function(e){if(e.target.classList.contains("docsify-copy-code-button")){var o="BUTTON"===e.target.tagName?e.target:e.target.parentNode,t=document.createRange(),c=o.parentNode.querySelector("code"),i=o.querySelector("[aria-live]"),r=window.getSelection();t.selectNode(c),r&&(r.removeAllRanges(),r.addRange(t));try{document.execCommand("copy")&&(o.classList.add("success"),i.innerText=n.successText,setTimeout((function(){o.classList.remove("success"),i.innerText=""}),1e3))}catch(e){console.error("docsify-copy-code: ".concat(e)),o.classList.add("error"),i.innerText=n.errorText,setTimeout((function(){o.classList.remove("error"),i.innerText=""}),1e3)}(r=window.getSelection())&&("function"==typeof r.removeRange?r.removeRange(t):"function"==typeof r.removeAllRanges&&r.removeAllRanges())}}))}))}].concat(window.$docsify.plugins||[])}();
//# sourceMappingURL=docsify-copy-code.min.js.map
+11 -8
View File
@@ -10,8 +10,11 @@ var ottos = [
"/images/otto-rd2-xmas-compressed.png",
"/images/otto-traditional.png",
]
function getCurrentImage() {
ottoIndex = (ottoIndex) % ottos.length;
return ottos[ottoIndex];
}
var ottoIndex = 0;
function showOtto(callback) {
node.style.opacity = 1;
anim = node.animate([
@@ -37,13 +40,13 @@ function hideOtto(callback) {
}
}
window.onload = function () {
window.addEventListener("load", function () {
fetch("tips.json").then(response => response.json()).then(data => {
tipTexts = data;
});
addOtto(document);
showOtto()
}
})
window.$docsify.onOttoShow = function () {
@@ -57,9 +60,14 @@ window.$docsify.onOttoShow = function () {
} else {
hideOtto();
ottoIndex++;
window.localStorage.setItem('ottoIndex', ottoIndex);
}
}
function addOtto(document) {
let localStorageOttoIndex = window.localStorage.getItem('ottoIndex');
if (localStorageOttoIndex !== null) {
ottoIndex = parseInt(localStorageOttoIndex);
}
node = document.createElement("div");
node.className = "otto";
image = document.createElement("img");
@@ -117,8 +125,3 @@ function playTips() {
function randomTips() {
return tipTexts[Math.floor(Math.random() * tipTexts.length)];
}
function getCurrentImage() {
ottoIndex = (ottoIndex) % ottos.length;
return ottos[ottoIndex];
}
+8 -8
View File
@@ -5,22 +5,22 @@ let columny;
let positions = {
"c1": { "x": 64, "name": "第一拍" },
"c2": { "x": 88, "name": "第单发拍" },
"c2": { "x": 88, "name": "第拍" },
"c3": { "x": 112, "name": "第三拍" },
"c4": { "x": 136, "name": "第四拍" },
"c5": { "x": 160, "name": "第五拍" },
"c6": { "x": 184, "name": "第六拍" },
"c7": { "x": 249, "name": "第拍" },
"c7": { "x": 249, "name": "第拍" },
"ccharacter": { "x": 35, "name": "角色" },
"ccenter": { "x": 176, "name": "中心" },
"cheart": { "x": 317, "name": "心" },
"r1": { "y": 162, "name": "四轨时的第轨" },
"r2": { "y": 141, "name": "三轨时的第轨" },
"r3": { "y": 120, "name": "二轨时的第一轨/四轨时的第轨" },
"r1": { "y": 162, "name": "四轨时的第轨" },
"r2": { "y": 141, "name": "三轨时的第轨" },
"r3": { "y": 120, "name": "二轨时的第一轨/四轨时的第轨" },
"r4": { "y": 99, "name": "一轨时的第一轨/三轨时的第二轨" },
"r5": { "y": 78, "name": "二轨时的第二轨/四轨时的第轨" },
"r6": { "y": 57, "name": "三轨时的第轨" },
"r7": { "y": 36, "name": "四轨时的第轨" },
"r5": { "y": 78, "name": "二轨时的第二轨/四轨时的第轨" },
"r6": { "y": 57, "name": "三轨时的第轨" },
"r7": { "y": 36, "name": "四轨时的第轨" },
}
function themePosition() {
+65 -59
View File
@@ -14,37 +14,22 @@ var switchLightDarkModeOptions = {
svgColor: '#7d7b75',
}
window.$docsify.currentThemeModeIndex = 2;
const themeModes = ['light', 'dark', 'auto',]
// Docsify plugin functions
function plugin(hook, vm) {
var switchSpan = document.createElement('span')
if (!switchLightDarkModeOptions.useSwitchMode) {
return
}
switchSpan.id = 'switchLightDarkModeDivBeforeArticle'
switchSpan.style.position = 'fixed'
switchSpan.style.right = switchLightDarkModeOptions.right.toString() + 'px'
switchSpan.style.top = switchLightDarkModeOptions.top.toString() + 'px'
let themeModes = ['light', 'dark', 'auto',]
let currentThemeModeIndex = 2
hook.mounted(function () {
// let lightTheme = Docsify.dom.findAll('[href="/style/light.css"]')[0]
// let darkTheme = Docsify.dom.findAll('[href="/style/dark.css"]')[0]
var switchSpan = document.createElement('span')
switchSpan.id = 'switchLightDarkModeDivBeforeArticle'
switchSpan.style.position = 'fixed'
switchSpan.style.right = switchLightDarkModeOptions.right.toString() + 'px'
switchSpan.style.top = switchLightDarkModeOptions.top.toString() + 'px'
const lightModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M12 18a6 6 0 100-12 6 6 0 000 12zM22 12h1M12 2V1M12 23v-1M20 20l-1-1M20 4l-1 1M4 20l1-1M4 4l1 1M1 12h1" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const darkModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M3 11.507a9.493 9.493 0 0018 4.219c-8.507 0-12.726-4.22-12.726-12.726A9.494 9.494 0 003 11.507z" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const autoModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M3 15c2.483 0 4.345-3 4.345-3s1.862 3 4.345 3c2.482 0 4.965-3 4.965-3s2.483 3 4.345 3M3 20c2.483 0 4.345-3 4.345-3s1.862 3 4.345 3c2.482 0 4.965-3 4.965-3s2.483 3 4.345 3M19 10a7 7 0 10-14 0" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomInIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M8 11h3m3 0h-3m0 0V8m0 3v3M17 17l4 4M3 11a8 8 0 1016 0 8 8 0 00-16 0z" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomOutIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M17 17l4 4M3 11a8 8 0 1016 0 8 8 0 00-16 0zM8 11h6" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomDefaultIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M12 19a7 7 0 100-14 7 7 0 000 14zM12 19v2M5 12H3M12 5V3M19 12h2" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const ottoIconXml = `
const lightModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M12 18a6 6 0 100-12 6 6 0 000 12zM22 12h1M12 2V1M12 23v-1M20 20l-1-1M20 4l-1 1M4 20l1-1M4 4l1 1M1 12h1" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const darkModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M3 11.507a9.493 9.493 0 0018 4.219c-8.507 0-12.726-4.22-12.726-12.726A9.494 9.494 0 003 11.507z" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const autoModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M3 15c2.483 0 4.345-3 4.345-3s1.862 3 4.345 3c2.482 0 4.965-3 4.965-3s2.483 3 4.345 3M3 20c2.483 0 4.345-3 4.345-3s1.862 3 4.345 3c2.482 0 4.965-3 4.965-3s2.483 3 4.345 3M19 10a7 7 0 10-14 0" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomInIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M8 11h3m3 0h-3m0 0V8m0 3v3M17 17l4 4M3 11a8 8 0 1016 0 8 8 0 00-16 0z" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomOutIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M17 17l4 4M3 11a8 8 0 1016 0 8 8 0 00-16 0zM8 11h6" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomDefaultIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M12 19a7 7 0 100-14 7 7 0 000 14zM12 19v2M5 12H3M12 5V3M19 12h2" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const ottoIconXml = `
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="30px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24" color="` + switchLightDarkModeOptions.svgColor + `" stroke="` + switchLightDarkModeOptions.svgColor + `" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path class="st0" d="M17.5,12.9c0,1-.1,1.8-1,1.8s-1.6-.8-1.6-1.8.7-1.8,1.6-1.8,1,.8,1,1.8Z"/>
@@ -61,38 +46,58 @@ function plugin(hook, vm) {
<line class="st0" x1="16.8" y1="11.1" x2="16.8" y2="1.4"/>
</svg>
`
let setThemeMode = function (currentTheme) {
switch (currentTheme) {
case 'light':
// lightTheme.disabled = false
// darkTheme.disabled = true
document.body.classList.remove('dark')
document.body.classList.add('light')
switchSpan.innerHTML = lightModeIconXml
break
case 'dark':
// lightTheme.disabled = true
// darkTheme.disabled = false
document.body.classList.remove('light')
document.body.classList.add('dark')
switchSpan.innerHTML = darkModeIconXml
break
case 'auto':
var isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
// lightTheme.disabled = isDarkMode
// darkTheme.disabled = !isDarkMode
if (isDarkMode) {
document.body.classList.remove('light')
document.body.classList.add('dark')
}
else {
document.body.classList.remove('dark')
document.body.classList.add('light')
}
switchSpan.innerHTML = autoModeIconXml
break
}
let setThemeMode = function (currentTheme) {
switch (currentTheme) {
case 'light':
document.body.classList.remove('dark')
document.body.classList.add('light')
switchSpan.innerHTML = lightModeIconXml
break
case 'dark':
document.body.classList.remove('light')
document.body.classList.add('dark')
switchSpan.innerHTML = darkModeIconXml
break
case 'auto':
var isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
if (isDarkMode) {
document.body.classList.remove('light')
document.body.classList.add('dark')
}
else {
document.body.classList.remove('dark')
document.body.classList.add('light')
}
switchSpan.innerHTML = autoModeIconXml
break
}
}
window.$docsify.currentThemeModeIndex = 2;
// Docsify plugin functions
function plugin(hook, vm) {
if (!switchLightDarkModeOptions.useSwitchMode) {
return
}
let localStorageCurrentThemeModeIndex = window.localStorage.getItem('currentThemeModeIndex')
let currentThemeModeIndex = 2
if (localStorageCurrentThemeModeIndex !== null) {
currentThemeModeIndex = parseInt(window.localStorage.getItem('currentThemeModeIndex'))
}
window.$docsify.currentThemeModeIndex = currentThemeModeIndex
setThemeMode(themeModes[currentThemeModeIndex])
window.localStorage.setItem('currentThemeModeIndex', currentThemeModeIndex)
if (window.$docsify.onLightDarkModeChange) {
window.$docsify.onLightDarkModeChange();
}
hook.mounted(function () {
// let lightTheme = Docsify.dom.findAll('[href="/style/light.css"]')[0]
// let darkTheme = Docsify.dom.findAll('[href="/style/dark.css"]')[0]
setThemeMode(themeModes[currentThemeModeIndex])
let preferredThemeChangeEventListenerFunction = function () {
@@ -111,6 +116,7 @@ function plugin(hook, vm) {
}
window.$docsify.currentThemeModeIndex = currentThemeModeIndex
setThemeMode(themeModes[currentThemeModeIndex])
window.localStorage.setItem('currentThemeModeIndex', currentThemeModeIndex)
if (window.$docsify.onLightDarkModeChange) {
window.$docsify.onLightDarkModeChange();
}
+78 -14
View File
@@ -5,6 +5,20 @@
src: url(/font/DinkieBitmap-7px.woff2) format('woff2');
}
@font-face {
font-family: 'd7i';
font-style: normal;
font-weight: 400;
src: url(/font/DinkieBitmap-9pxItalic.woff2) format('woff2');
}
@font-face {
font-family: 'd7c';
font-style: normal;
font-weight: 400;
src: url(/font/DinkieBitmap-9pxCode.woff2) format('woff2');
}
@font-face {
font-family: 'd9';
font-style: normal;
@@ -172,6 +186,7 @@ div.image-tip img {
div.image-tip {
pointer-events: none;
& p {
pointer-events: auto;
height: 0;
@@ -182,7 +197,8 @@ div.image-tip {
justify-self: center;
transition: all 0.2s;
}
& img{
& img {
pointer-events: auto;
}
}
@@ -410,12 +426,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);
}
@@ -471,7 +487,7 @@ main.hidden {
}
.sidebar-toggle {
background: linear-gradient(to right, transparent 50px, #0ff8);
background: linear-gradient(to right, #0dd 50px, #0ff8);
border: 0;
outline: none;
padding: 10px;
@@ -538,10 +554,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 +949,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 +961,6 @@ section.cover blockquote>p>a {
margin-left: 0;
}
.markdown-section iframe {
margin: 1em 0
}
.markdown-section em {
color: #7f8c8d
}
@@ -950,7 +978,7 @@ section.cover blockquote>p>a {
.markdown-section output,
.markdown-section pre {
margin: 1.2em 0;
margin: 1em 0;
position: relative
}
@@ -967,7 +995,7 @@ section.cover blockquote>p>a {
}
.markdown-section code {
border-radius: 2px;
border-radius: 4px;
margin: 0 2px;
padding: 3px 5px;
white-space: pre-wrap
@@ -986,8 +1014,8 @@ section.cover blockquote>p>a {
.markdown-section pre>code {
color: #525252;
font-size: .8rem;
padding: 2.2em 5px;
line-height: inherit;
padding: 1em 5px;
line-height: 1.2em;
margin: 0 2px;
max-width: inherit;
overflow: inherit;
@@ -1130,6 +1158,7 @@ div.bv {
height: 0;
padding-bottom: 56.25%;
margin-bottom: 30px;
&>iframe {
position: absolute;
background-color: transparent;
@@ -1137,6 +1166,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,7 +1254,41 @@ 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 {
opacity: 0.5 !important;
}
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;
transition: background-color 0.1s, color 0.1s;
}
}
.docsify-copy-code-button{
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.3));
padding: 0;
margin: 0;
border-radius: 5px;
font: 300 12px 'd9c', 'd9';
}
+21 -18
View File
@@ -7,12 +7,16 @@
.markdown-section blockquote{
background-color: var(--section-blockquote-background-color);
color: var(--section-blockquote-color);
color: var(--main-color);
}
.markdown-section p.tip,
.markdown-section tr:nth-child(2n) {
background-color: var(--section-background-color);
background-color: var(--table-2nd-row-background-color);
}
.markdown-section tr:nth-child(2n-1) {
background-color: var(--table-1st-row-background-color);
}
.progress {
@@ -128,11 +132,11 @@ body {
.markdown-section h3,
.markdown-section h4,
.markdown-section strong {
color: var(--section-background-color);
color: var(--section-color);
}
.markdown-section a {
color: var(--theme-color, var(--main-theme-color));
color: var(--theme-color, var(--section-blockquote-color));
}
.markdown-section blockquote {
@@ -142,7 +146,7 @@ body {
.markdown-section code {
background-color: var(--section-main-color);
color: var(--section-background-color);
color: var(--section-blockquote-color);
font-family: d9c, d9;
}
@@ -151,9 +155,6 @@ body {
-webkit-font-smoothing: initial;
background-color: var(--section-main-color);
font-family: d9c, d9;
margin: 1.2em 0;
padding: 0 1.4rem;
position: relative;
}
.token.string {
@@ -172,22 +173,14 @@ body {
-moz-osx-font-smoothing: initial;
-webkit-font-smoothing: initial;
background-color: var(--section-main-color);
border-radius: 2px;
color: var(--section-background-color);
display: block;
color: var(--section-blockquote-color);
font-family: d9c, d9;
}
.markdown-section code:after,
.markdown-section code:before {
letter-spacing: .05rem
}
pre:after {
color: #ccc;
content: attr(data-lang);
top: 0
top: 0;
}
.markdown-section p.tip {
@@ -220,4 +213,14 @@ article.markdown-section:has(.intro)>.notice {
&[onclick]:hover{
background-color: var(--section-main-color);
}
}
div.medium-zoom-overlay{
background-color: var(--background-color) !important;
}
.markdown-section:has(iframe[single]) {
&>p{
background-color: var(--background-color);
}
}
+6 -1
View File
@@ -2,11 +2,12 @@ body.dark {
--main-color: #c8c8c8;
--main-theme-color: #008080;
--background-color: #1f1f1f;
--background-border-color: #303030;
--section-main-color: #0f0f0f;
--section-background-color: #003333;
--section-table-header-border-color: #303030;
--section-blockquote-background-color: #003a3a;
--section-blockquote-color: #d4d4d4;
--section-blockquote-color: #7cdcfe;
--section-tip-background-color: #161616;
--section-tip-color: #3f3f3fcc;
--section-p-warn-background-color: #ea6f5a1a;
@@ -14,7 +15,11 @@ body.dark {
--notice-background-color: #3f3f3fcc;
--notice-border-color: #303030;
--notice-hover-background-color: #525252cc;
--table-1st-row-background-color: #00000038;
--table-2nd-row-background-color: #000000cc;
-webkit-tap-highlight-color: rgb(0, 0, 0);
--special-theme-position-backgound-color: #177500;
}
body.dark .sidebar li>a:has(>img) {
+5
View File
@@ -2,6 +2,7 @@ body.light {
--main-color: #202020;
--main-theme-color: #008585;
--background-color: #dddddd;
--background-border-color: #b1b1b1;
--section-main-color: #00c9c9;
--section-background-color: #009999;
--section-table-header-border-color: #b1b1b1;
@@ -14,7 +15,11 @@ body.light {
--notice-background-color: #f0f0f0;
--notice-border-color: #b1b1b1;
--notice-hover-background-color: #e0e0e0;
--table-1st-row-background-color: #08c5ff26;
--table-2nd-row-background-color: #08c5ff5b;
-webkit-tap-highlight-color: rgb(255, 255, 255);
--special-theme-position-backgound-color: #05a000;
}
body.light .sidebar li>a:has(>img) {
+3 -3
View File
@@ -114,9 +114,9 @@ div.theme-positions>.infobox {
width: max-content;
height: max-content;
padding: 3px;
border: 2px solid #535353;
border: 2px solid var( --background-border-color);
border-radius: 2px;
background-color: #363636;
background-color: var(--background-color);
text-align: center;
z-index: 20;
font-size: 15px;
@@ -130,7 +130,7 @@ div.theme-positions>.infobox {
font-weight: bold;
&.c {
background-color: #177500;
background-color: var(--special-theme-position-backgound-color);
border-radius: 3px;
}
}