Phira-docs 说明
这里是 Phira 相关的文档说明, 可前往 GitHub 提出问题或者贡献文档, 其他语言请通过修改 url 来访问, 如 /en/
如果希望提供翻译, 请参考
在 PR 内提供相应的翻译后 .po
文件, 并在 .github/mdbook.yml
的 env
的 LANGUAGES
中添加对应的语言, 语言代码见 ISO 639
资源包
在 prpr 中,你可以自定义资源包。资源包包含了音符的样式、粒子效果、打击音效等等元素。你可以在测试群或频道中找到资源包,也可以选择自己制作资源包。以下将具体阐述资源包的文件结构。
结构
资源包是单个 zip 压缩文件,其中包含了配置文件 info.yml
和其他的资源文件。其中,资源文件有些是必须存在,有些则是可选的。
资源文件
资源文件必须包括:
click.png
和click_mh.png
:Click 音符的皮肤,mh
代表双押;drag.png
和drag_mh.png
:Drag 音符的皮肤,mh
代表双押;flick.png
和flick_mh.png
:Flick 音符的皮肤,mh
代表双押;hold.png
和hold_mh.png
:Hold 音符的皮肤,mh
代表双押;hit_fx.png
:打击特效图片。
资源文件可以包括(即若不包括,将使用默认):
click.ogg
、drag.ogg
和flick.ogg
:对应音符的打击音效,注意采样率必须为 44100Hz,否则在渲染时(prpr-render)会导致崩溃;ending.mp3
:结算界面背景音乐。
配置文件
配置文件采用 yml,其中必填项如下(以默认资源包为例):
name: Default
author: "Mivik & MisaLiu"
hitFx: [5, 6]
holdAtlas: [50, 50]
holdAtlasMH: [50, 110]
name
:资源包的名字;author
:资源包的作者;description
:资源包介绍;hitFx
:打击特效宽、高的帧数。打击特效是将多帧动画存储在一张图中的,因此需要指定这张图中横竖各有几帧,例如,在 此图 中横、竖的帧数分别为 5 与 6(最后一行不太看得见,但是是存在的)(图片为了便于辨识使用了黑色背景,但在制作资源包时应当使用透明背景);holdAtlas
:Hold 贴图的尾、头高度。Hold 的皮肤是 一张图片,从上到下分别为 Hold 的尾部、中间和头部。而holdAtlas
的两个数字则分别指定了图片中尾部和头部的高度。例如,在 此图 中,尾部和头部高度均为 50 像素。holdAtlasMH
:意义与上一条类似,指定多押 Hold 的相关信息。
此外还有选填项:
hitFxDuration
(小数,默认0.5
):打击特效的持续时间,以秒为单位;hitFxScale
(小数,默认1.0
):打击特效缩放比例;hitFxRotate
(布尔值,默认false
):打击特效是否随 Note 旋转;hitFxTinted
(布尔值,默认true
):打击特效是否依照判定线颜色着色;hideParticles
(布尔值,默认false
):打击时是否隐藏方形粒子效果;holdKeepHead
(布尔值,默认false
):Hold 触线后是否还显示头部;holdRepeat
(布尔值,默认false
):Hold 的中间部分是否采用重复式拉伸。这里的三张图 从左到右依次是 Hold 资源图、不启用holdRepeat
时的长条和启用holdRepeat
时的长条;holdCompact
(布尔值,默认false
):是否把 Hold 的头部和尾部与 Hold 中间重叠。还是用上面的 示例,如果不开启holdCompact
,效果就会是左边第一张图,Hold 的头尾是和中间隔开的;而右边两张图都是开启了holdCompact
的效果;colorPerfect
(十六进制颜色代码,默认0xe1ffec9f
):AP(全 Perfect)情况下的判定线颜色;colorGood
(十六进制颜色代码,默认0xebb4e1ff
):FC(全连)情况下的判定线颜色。
Phira 谱面标准
谱面基本结构
Phira 谱面包是一个压缩包, 解压后的压缩包内应当直接包含以下文件而非文件夹:
info.yml
: 采用 YAML 格式的谱面信息文件- ...
info.yml
中所指定的其他文件
各文件支持情况
谱面文件
见 谱面文件格式
音乐文件
见 音乐文件格式
TBD
常见问题
RPE 的谱面 JSON 文件存储了元数据(创作者, 难度, 名称等), 这一行为是不被推荐的, 这会导致这部分信息可能被重复记录, 从而导致不一致性. Phira 的行为以 info.yml
为准
谱面信息格式
谱面信息是一种 元数据 , 用于描述谱面数据之外的的基本信息, 例如作者, 插图, 音乐信息等.
谱面信息存在两种变体, ChartInfo
和 BriefChartInfo
, 谱面信息 (ChartInfo
) 的存储采用 YAML 格式, 谱面的 info.yml
文件即为 ChartInfo
的实例
本地导入时会自动填充 ChartInfo
的所有字段, 但 YAML 格式本身并不难懂, 若希望填写好后一并打包导入来节省时间, 则需要注意以下字段的填写
ChartInfo
请参考源代码中的定义, 可能存在更新
谱面 ID id
不必需, i32
整数, 默认为空
用于标识谱面的唯一 ID, 与服务器中的 ID 保持一致, Phira 客户端会根据此 ID 来判断是否需要更新谱面
本地谱面该项为空
上传者 ID uploader
不必需, i32
整数, 默认为空
用于标识上传者的唯一 ID, 与服务器中的 ID 保持一致, 用于显示在线谱面的上传者信息
本地谱面该项为空
谱面名称 name
必需, String
字符串, 默认为 "UK"
谱面的名称
难度 difficulty
必需, f32
浮点数, 默认为 10.0
谱面的难度, 别乱来哈
难度等级 level
必需, String
字符串, 默认为 "UK Lv.10"
显示在游玩时屏幕右下角的难度等级
谱面作者 charter
必需, String
字符串, 默认为 "UK"
这谱谁写的.jpg
音乐作者 composer
必需, String
字符串, 默认为 "UK"
音乐的作者
插图作者 illustrator
必需, String
字符串, 默认为 "UK"
谱面的插图作者
谱面文件名 chart
必需, String
字符串, 默认为 "chart.json"
谱面文件的文件名, RPE 生成的谱面通常为 chart.json
, PE 生成的谱面通常为 xxx.pec
谱面格式 format
不必需, ChartFormat
枚举, 默认为空
谱面的格式, 不应当手动填写, 由 Phira 客户端自动识别并写入, 出于完整性需求在这里列出
音乐文件名 music
必需, String
字符串, 默认为 "song.mp3"
音乐文件的文件名
插图文件名 illustration
必需, String
字符串, 默认为 "background.png"
插图文件的文件名
解锁视频 unlockVideo
不必需, String
字符串, 默认为空
解锁该谱面的视频文件名
预览开始时间 previewStart
必需, f32
浮点数, 默认为 0.0
音乐预览开始的时间(秒)
预览结束时间 previewEnd
不必需, f32
浮点数, 默认为空
音乐预览结束的时间(秒), 留空则视为预览开始时间后 15.0 秒, 若超出结尾则会被截断
源代码: 截断
纵横比 aspectRatio
必需, f32
浮点数, 默认为 16.0 / 9.0
注意: 谱面显示的纵横比, 设备的纵横比大于此值(例如部分加长手机, 或者一般手机上此值填写为 4:3)时会保证谱面处于该值的纵横比, 而小于此值时会拉伸谱面以保证谱面填满屏幕(来源: TBD)
背景暗化程度 backgroundDim
必需, f32
浮点数, 默认为 0.6
谱面背景的暗化程度, 单位待补充
判定线长度 lineLength
必需, f32
浮点数, 默认为 6.0
谱面中线条的长度, 单位待补充(涉及到渲染细节, 文档待补充)
谱面延迟 offset
必需, f32
浮点数, 默认为 0.0
谱面相对于音乐之间的时间延迟(秒), 即该值为正时相比于该值为零时:
- 若两种情况中音乐同时开始, 则
offset
为正时谱面开始更晚 - 若两种情况中谱面同时开始, 则
offset
为正时音乐开始更早
提示 tip
不必需, String
字符串, 默认为空
Tip: 不写的话会给你塞一条别的
标签 tags
必需, String
字符串数组, 默认为空
谱面的标签, 用于分类和搜索, 标签相关文档待补充
简介 intro
必需, String
字符串, 默认为空
谱面的简介
长条选项 holdPartialCover
必需, bool
布尔值, 默认为 false
Hold 音符的一个渲染选项, 具体行为待补充
创建时间 created
不必需, DateTime<Utc>
可选的 UTC 时间, 默认为空
谱面创建的时间, 不应当手动填写, 由 Phira 客户端自动写入
更新时间 updated
不必需, DateTime<Utc>
可选的 UTC 时间, 默认为空
本地谱面最近一次更新的时间, 不应当手动填写, 由 Phira 客户端自动写入
谱面更新时间 chartUpdated
不必需, DateTime<Utc>
可选的 UTC 时间, 默认为空
云端谱面最近一次更新的时间, 不应当手动填写, 由 Phira 客户端自动写入. 用于判断是否需要更新谱面
BriefChartInfo
BriefChartInfo
是 ChartInfo
的一个简化版本, 用于在不需要详细信息的场景下使用. 以下是 BriefChartInfo
的主要字段:
TBD
谱面文件格式
目前支持的谱面文件格式包括:
- RPE 格式, 见 RPE 文档
- PEC 格式 (文档待完善)
- PBC 格式 (文档待完善)
格式的推断通过 info.yml
中的 format
字段进行, 若为空则通过文件内容进行推断.
注意: 忽略谱面文件的后缀名
RPE 文档
本文档在2024.7.25开始编辑,在2024.8.25正式完成。
详情见左侧目录。
2024.8.30更新:
RPE 1.5.4给顶栏图标都加了Tip。中文都在喵喵,英文都有波浪号,可爱捏。
RPE谱面根目录结构
警告:以下所有内容从编写开始时间(2024.7.25)最新RPE版本1.4.1开始编写,更早的加入版本等信息全部待补充。
谱面根目录结构
BPMList
BPMList
是一个 JsonArray
,包含若干个 JsonObject
。
每个JsonObject包含以下字段:
字段名 | 类型 | 说明 | 加入版本 |
---|---|---|---|
bpm | float | BPM值 | - |
startTime | beat | BPM开始时间 | - |
META
META
是一个 JsonObject
,包含以下字段:
字段名 | 类型 | 说明 | 加入版本 |
---|---|---|---|
RPEVersion | int | RPE版本,100~160 | - |
background | string | 背景图片相对于谱面根目录路径 | - |
charter | string | 谱师名义 | - |
composer | string | 曲师 | - |
id | string | 谱面ID,在RPE中用于识别谱面 | - |
illustration | string | 曲绘画师 | 141 |
level | string | 谱面等级 | - |
name | string | 谱面名称 | - |
offset | int | 音乐偏移,单位为毫秒 | - |
song | string | 音乐文件相对于谱面根目录路径 | - |
offset
为负数时,音乐应该在谱面开始前-offset
毫秒时播放;为正数时,音乐应该在谱面开始后offset
毫秒时播放。id
在RPE自动生成时为long
,实际上这个值可以随便篡改为任何字符,所以在实际谱面中存储方式为string
。- RPE 1.5.0 ~ RPE 1.6.0 之间的版本(不含RPE 1.6.0,含Alpha版本),META中的
RPEVersion
字段保持为150
,没有被更改。
chartTime
模拟器不需要本属性。
chartTime
是一个double
变量,时间单位是秒,表示谱面编辑时长,在141
加入。在RPE中,如果谱师在30秒内没有编辑谱面,则该值将不再变动,下次开始编辑后继续计时。(特性被移除)- 如果RPE失去焦点,RPE仍会继续计时,若RPE重新获得焦点,计时将回溯至失去焦点时的时间。
judgeLineGroup
模拟器不需要本属性。
judgeLineGroup
是一个JsonArray
,包含若干个string
;- 每一个
string
为一个判定线组。 - 实际行为待补充。
judgeLineList
judgeLineList
是一个JsonArray
,包含若干个JsonObject
,每个JsonObject
代表一个判定线。
multiLineString
模拟器不需要本属性。
multiLineString
是一个字符串,在RPE中多线编辑时使用,以空格分割,每个数字代表一个判定线。multiLineString
中也可能含有:
,1:20
将选中1
到20
号的所有判定线。
multiScale
模拟器不需要本属性。
multiScale
是一个float
,在RPE中用于缩放多线编辑页面的大小。
judgeLine
每一个judgeLine(判定线)都含有以下字段:
字段名 | 类型 | 描述 | 默认值 | 加入版本 |
---|---|---|---|---|
Group | int | 判定线所属组 | 0 | - |
Name | string | 判定线名称 | Untitled | - |
Texture | string | 判定线贴图,若非默认值,则为相对于谱面根目录的路径,更多详见Texture | line.png | - |
anchor | JsonArray | 判定线锚点,详见 extend | [ 0.5, 0.5 ] | 142 |
eventLayers | JsonArray? | 事件层级,默认包含至少一个层级(JsonObject),空层级详见下方,最大有五个,层级下事件见 event | - | - |
extended | JsonArray | 特殊事件,详见 extend Event | - | - |
father | int | 父线,-1 表示无父线(行为待补充) | - | - |
isCover | int | 遮罩(行为待补充) | 1 | - |
notes | JsonArray | 线上所有的Note,详见 note | - | - |
numOfNotes | int | Note总数量(包含 FakeNote ,不包含 Hold ) | 0 | - |
zOrder | int | 线z轴(即图层),范围为±100(范围需要验证) | 0 | - |
attachUI | string? | UI绑定,详见 extend;无绑定情况下,不存在本属性 | - | - |
isGif | bool | 纹理是否为GIF,若为 true ,Texture为一个GIF文件 | false | 150 |
posControl | JsonArray | 此字段无法在RPE中编辑 | - | - |
sizeControl | JsonArray | 此字段无法在RPE中编辑 | - | - |
skewControl | JsonArray | 此字段无法在RPE中编辑 | - | - |
yControl | JsonArray | 此字段无法在RPE中编辑 | - | - |
alphaControl | JsonArray | 此字段无法在RPE中编辑 | - | - |
bpmfactor | float | 此字段无法在RPE中编辑 | 1.0 | - |
- 若层级为空,在某个版本之前,字段为
null
,在某个版本及以后,空层级无字段。(当前已知至少在143
版本时无字段)- 若所有层级都为空,
eventLayers
字段不会出现。
- 若所有层级都为空,
事件插值
Python 示例
- 定义
rpe_easing.py
(略) - 定义
Chart_Objects_Rpe.py
(部分略)
from __future__ import annotations
import typing
from dataclasses import dataclass
from functools import lru_cache, cache
import rpe_easing
def easing_interpolation(
t: float, st: float,
et: float, sv: float,
ev: float, f: typing.Callable[[float], float]
):
if t == st: return sv
return f((t - st) / (et - st)) * (ev - sv) + sv
def conrpepos(x: float, y: float):
return (x + 675) / 1350, 1.0 - (y + 450) / 900
def _init_events(es: list[LineEvent]):
aes = []
for i, e in enumerate(es):
if i != len(es) - 1:
ne = es[i + 1]
if e.endTime.value < ne.startTime.value:
aes.append(LineEvent(e.endTime, ne.startTime, e.end, e.end, 1))
es.extend(aes)
es.sort(key = lambda x: x.startTime.value)
if es: es.append(LineEvent(es[-1].endTime, Beat(31250000, 0, 1), es[-1].end, es[-1].end, 1))
@dataclass
class Beat:
var1: int
var2: int
var3: int
def __post_init__(self):
self.value = self.var1 + (self.var2 / self.var3)
self._hash = hash(self.value)
def __hash__(self) -> int:
return self._hash
@dataclass
class Note:
...
@dataclass
class LineEvent:
...
easingFunc: typing.Callable[[float], float] = rpe_easing.ease_funcs[0]
def __post_init__(self):
if not isinstance(self.easingType, int): self.easingType = 1
self.easingType = 1 if self.easingType < 1 else (len(rpe_easing.ease_funcs) if self.easingType > len(rpe_easing.ease_funcs) else self.easingType)
self.easingFunc = rpe_easing.ease_funcs[self.easingType - 1]
@dataclass
class EventLayer:
...
def __post_init__(self):
self.speedEvents.sort(key = lambda x: x.startTime.value)
self.moveXEvents.sort(key = lambda x: x.startTime.value)
self.moveYEvents.sort(key = lambda x: x.startTime.value)
self.rotateEvents.sort(key = lambda x: x.startTime.value)
self.alphaEvents.sort(key = lambda x: x.startTime.value)
_init_events(self.speedEvents)
_init_events(self.moveXEvents)
_init_events(self.moveYEvents)
_init_events(self.rotateEvents)
_init_events(self.alphaEvents)
@dataclass
class Extended:
...
def __post_init__(self):
self.scaleXEvents.sort(key = lambda x: x.startTime.value)
self.scaleYEvents.sort(key = lambda x: x.startTime.value)
self.colorEvents.sort(key = lambda x: x.startTime.value)
self.textEvents.sort(key = lambda x: x.startTime.value)
_init_events(self.scaleXEvents)
_init_events(self.scaleYEvents)
_init_events(self.colorEvents)
_init_events(self.textEvents)
@dataclass
class MetaData:
...
@dataclass
class BPMEvent:
...
@dataclass
class JudgeLine:
...
def GetEventValue(self, t: float, es: list[LineEvent], default):
for e in es:
if e.startTime.value <= t <= e.endTime.value:
if isinstance(e.start, float|int):
return easing_interpolation(t, e.startTime.value, e.endTime.value, e.start, e.end, e.easingFunc)
elif isinstance(e.start, str):
return e.start
elif isinstance(e.start, list):
r = easing_interpolation(t, e.startTime.value, e.endTime.value, e.start[0], e.end[0], e.easingFunc)
g = easing_interpolation(t, e.startTime.value, e.endTime.value, e.start[1], e.end[1], e.easingFunc)
b = easing_interpolation(t, e.startTime.value, e.endTime.value, e.start[2], e.end[2], e.easingFunc)
return (r, g, b)
return default
@lru_cache
def GetPos(self, t: float, master: Rpe_Chart) -> list[float, float]:
linePos = [0.0, 0.0]
for layer in self.eventLayers:
linePos[0] += self.GetEventValue(t, layer.moveXEvents, 0.0)
linePos[1] += self.GetEventValue(t, layer.moveYEvents, 0.0)
if self.father != -1:
try:
fatherPos = master.JudgeLineList[self.father].GetPos(t, master)
linePos = list(map(lambda x, y: x + y, linePos, fatherPos))
except IndexError:
pass
return linePos
def GetState(self, t: float, defaultColor: tuple[int, int, int], master: Rpe_Chart) -> tuple[tuple[float, float], float, float, tuple[int, int, int], float, float, str|None]:
"linePos, lineAlpha, lineRotate, lineColor, lineScaleX, lineScaleY, lineText"
linePos = self.GetPos(t, master)
lineAlpha = 0.0
lineRotate = 0.0
lineColor = defaultColor if not self.extended.textEvents else (255, 255, 255)
lineScaleX = 1.0
lineScaleY = 1.0
lineText = None
for layer in self.eventLayers:
lineAlpha += self.GetEventValue(t, layer.alphaEvents, 0.0 if (t >= 0.0 or self.attachUI is not None) else -255.0)
lineRotate += self.GetEventValue(t, layer.rotateEvents, 0.0)
if self.extended:
lineScaleX = self.GetEventValue(t, self.extended.scaleXEvents, lineScaleX)
lineScaleY = self.GetEventValue(t, self.extended.scaleYEvents, lineScaleY)
lineColor = self.GetEventValue(t, self.extended.colorEvents, lineColor)
lineText = self.GetEventValue(t, self.extended.textEvents, lineText)
return conrpepos(*linePos), lineAlpha / 255, lineRotate, lineColor, lineScaleX, lineScaleY, lineText
def __hash__(self) -> int:
return id(self)
def __eq__(self, oth) -> bool:
if isinstance(oth, JudgeLine):
return self is oth
return False
@dataclass
class Rpe_Chart:
...
def __post_init__(self):
self.BPMList.sort(key=lambda x: x.startTime.value)
@cache
def sec2beat(self, t: float, bpmfactor: float):
beat = 0.0
for i, e in enumerate(self.BPMList):
bpmv = e.bpm * bpmfactor
if i != len(self.BPMList) - 1:
et_beat = self.BPMList[i + 1].startTime.value - e.startTime.value
et_sec = et_beat * (60 / bpmv)
if t >= et_sec:
beat += et_beat
t -= et_sec
else:
beat += t / (60 / bpmv)
break
else:
beat += t / (60 / bpmv)
return beat
@cache
def beat2sec(self, t: float, bpmfactor: float):
sec = 0.0
for i, e in enumerate(self.BPMList):
bpmv = e.bpm * bpmfactor
if i != len(self.BPMList) - 1:
et_beat = self.BPMList[i + 1].startTime.value - e.startTime.value
if t >= et_beat:
sec += et_beat * (60 / bpmv)
t -= et_beat
else:
sec += t * (60 / bpmv)
break
else:
sec += t * (60 / bpmv)
return sec
def __hash__(self) -> int:
return id(self)
def __eq__(self, oth) -> bool:
if isinstance(oth, JudgeLine):
return self is oth
return False
- 加载谱面:
def load(chart: dict):
meta = chart.get("META", {})
rpe_chart_obj = Chart_Objects_Rpe.Rpe_Chart(
META = Chart_Objects_Rpe.MetaData(
RPEVersion = meta.get("RPEVersion", -1),
offset = meta.get("offset", 0),
name = meta.get("name", "Unknow"),
id = meta.get("id", "-1"),
song = meta.get("song", "Unknow"),
background = meta.get("background", "Unknow"),
composer = meta.get("composer", "Unknow"),
charter = meta.get("charter", "Unknow"),
level = meta.get("level", "Unknow"),
),
BPMList = [
Chart_Objects_Rpe.BPMEvent(
startTime = Chart_Objects_Rpe.Beat(
*BPMEvent_item.get("startTime", [0, 0, 1])
),
bpm = BPMEvent_item.get("bpm", 140)
)
for BPMEvent_item in chart.get("BPMList", [])
],
JudgeLineList = [
Chart_Objects_Rpe.JudgeLine(
isCover = judgeLine_item.get("isCover", 1),
Texture = judgeLine_item.get("Texture", "line.png"),
attachUI = judgeLine_item.get("attachUI", None),
bpmfactor = judgeLine_item.get("bpmfactor", 1.0),
father = judgeLine_item.get("father", -1),
zOrder = judgeLine_item.get("zOrder", 0),
eventLayers = [
Chart_Objects_Rpe.EventLayer(
speedEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", 0.0),
end = LineEvent_item.get("end", 0.0),
easingType = 1
)
for LineEvent_item in EventLayer_item.get("speedEvents", [])
] if EventLayer_item.get("speedEvents", []) is not None else [],
moveXEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", 0.0),
end = LineEvent_item.get("end", 0.0),
easingType = LineEvent_item.get("easingType", 1)
) for LineEvent_item in EventLayer_item.get("moveXEvents", [])
] if EventLayer_item.get("moveXEvents", []) is not None else [],
moveYEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", 0.0),
end = LineEvent_item.get("end", 0.0),
easingType = LineEvent_item.get("easingType", 1)
) for LineEvent_item in EventLayer_item.get("moveYEvents", [])
] if EventLayer_item.get("moveYEvents", []) is not None else [],
rotateEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", 0.0),
end = LineEvent_item.get("end", 0.0),
easingType = LineEvent_item.get("easingType", 1)
) for LineEvent_item in EventLayer_item.get("rotateEvents", [])
] if EventLayer_item.get("rotateEvents", []) is not None else [],
alphaEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", 0.0),
end = LineEvent_item.get("end", 0.0),
easingType = LineEvent_item.get("easingType", 1)
) for LineEvent_item in EventLayer_item.get("alphaEvents", [])
] if EventLayer_item.get("alphaEvents", []) is not None else []
) if EventLayer_item is not None else Chart_Objects_Rpe.EventLayer(speedEvents = [], moveXEvents = [], moveYEvents = [], rotateEvents = [], alphaEvents = [])
for EventLayer_item in judgeLine_item.get("eventLayers", [])
],
extended = Chart_Objects_Rpe.Extended(
scaleXEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", 1.0),
end = LineEvent_item.get("end", 1.0),
easingType = LineEvent_item.get("easingType", 1)
) for LineEvent_item in judgeLine_item.get("extended", {}).get("scaleXEvents", [])
] if judgeLine_item.get("extended", {}).get("scaleXEvents", []) is not None else [],
scaleYEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", 1.0),
end = LineEvent_item.get("end", 1.0),
easingType = LineEvent_item.get("easingType", 1)
) for LineEvent_item in judgeLine_item.get("extended", {}).get("scaleYEvents", [])
] if judgeLine_item.get("extended", {}).get("scaleYEvents", []) is not None else [],
colorEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", [255, 255, 255]),
end = LineEvent_item.get("end", [255, 255, 255]),
easingType = LineEvent_item.get("easingType", 1)
) for LineEvent_item in judgeLine_item.get("extended", {}).get("colorEvents", [])
] if judgeLine_item.get("extended", {}).get("colorEvents", []) is not None else [],
textEvents = [
Chart_Objects_Rpe.LineEvent(
startTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*LineEvent_item.get("endTime", [0, 0, 1])
),
start = LineEvent_item.get("start", ""),
end = LineEvent_item.get("end", ""),
easingType = LineEvent_item.get("easingType", 1)
) for LineEvent_item in judgeLine_item.get("extended", {}).get("textEvents", [])
] if judgeLine_item.get("extended", {}).get("textEvents", []) is not None else [],
) if judgeLine_item.get("extended", {}) is not None else None,
notes = [
Chart_Objects_Rpe.Note(
type = Note_item.get("type", 1),
startTime = Chart_Objects_Rpe.Beat(
*Note_item.get("startTime", [0, 0, 1])
),
endTime = Chart_Objects_Rpe.Beat(
*Note_item.get("endTime", [0, 0, 1])
),
positionX = Note_item.get("positionX", 0),
above = Note_item.get("above", 1),
isFake = Note_item.get("isFake", False),
speed = Note_item.get("speed", 1.0),
yOffset = Note_item.get("yOffset", 0.0),
visibleTime = Note_item.get("visibleTime", 999999.0),
width = Note_item.get("size", 1.0),
alpha = Note_item.get("alpha", 255),
)
for Note_item in judgeLine_item.get("notes", [])
]
)
for judgeLine_item in chart.get("judgeLineList", [])
]
)
return rpe_chart_obj
result = load({}) # 这里传入你的谱面
- 最后调用
result.JudgeLineList[i].GetState
, 并传入当前拍数为t
和判定线默认颜色为defaultColor
, 和谱面对象master
即可获取当前拍数下的判定线全部状态
beat
beat
是RPE所有事件的时间单位,它是一个 JsonArray
,在RPE中显示为 [0]:[1]/[2]
。
单BPM计算方式为:
double beat = RPEBeat[1] / RPEBeat[2] + RPEBeat[0];
double seconds = BPM * beat / 60;
多BPM计算方式待补充。
Python 示例
- 若
self.BPMList
为一个list[BPMEvent]
BPMEvent
定义:
@dataclass
class BPMEvent:
startTime: Beat
bpm: float
sec2beat
中t
为秒数,bpmfactor
为判定线中的bpmfactor
字段`beat2sec
中t
为拍数,bpmfactor
为判定线中的bpmfactor
字段`- 且
beat2sec(sec2beat(x)) == x
与sec2beat(beat2sec(x)) == x
的结果均为True
- 则有:
def sec2beat(self, t: float, bpmfactor: float):
beat = 0.0
for i, e in enumerate(self.BPMList):
bpmv = e.bpm * bpmfactor
if i != len(self.BPMList) - 1:
et_beat = self.BPMList[i + 1].startTime.value - e.startTime.value
et_sec = et_beat * (60 / bpmv)
if t >= et_sec:
beat += et_beat
t -= et_sec
else:
beat += t / (60 / bpmv)
break
else:
beat += t / (60 / bpmv)
return beat
def beat2sec(self, t: float, bpmfactor: float):
sec = 0.0
for i, e in enumerate(self.BPMList):
bpmv = e.bpm * bpmfactor
if i != len(self.BPMList) - 1:
et_beat = self.BPMList[i + 1].startTime.value - e.startTime.value
if t >= et_beat:
sec += et_beat * (60 / bpmv)
t -= et_beat
else:
sec += t * (60 / bpmv)
break
else:
sec += t * (60 / bpmv)
return sec
Note
note,即音符,是谱面的主要构成之一,每个note都应该含有以下参数:
字段 | 类型 | 描述 | 默认值 | 加入版本 |
---|---|---|---|---|
above | int | 1 为从线的正面下落, 2 反之;大于 2 或小于 1 时为从线的正面下落 | 1 | - |
alpha | int | note不透明度,0为完全透明,255为完全不透明 | 255 | - |
endTime | beat | note结束时间,若 type 为 2 ,此值为Hold的结束时间,否则与startTime一致 | - | - |
startTime | beat | note开始时间,若 type 为 2 ,此值为Hold的开始时间,否则与endTime一致 | - | - |
isFake | int | note真值,0 为真,1 为假,负数或大于1 的数为真;假note没有判定,没有打击特效与音效,不计分,不计物量,若为 hold 始终显示为未打击样式 | 0 | - |
positionX | float | note相对于判定线中心点的X坐标 | - | - |
size | float | note大小倍率 | 1.0 | - |
speed | float | 流速倍率,默认 | 1.0 | - |
type | int | note类型,1 为 Tap 、2 为 Hold 、3 为 Flick 、4 为 Darg | - | - |
visibleTime | float | note可见时间,单位为秒 | 999999.0000 | - |
yOffset | float | note的Y轴偏移,正数向上偏移,负数向下偏移,同时偏移打击特效的位置 | 0 | - |
hitsound | string? | note自定义打击音音频文件路径,相对于谱面文件根目录。没有自定义音效时,字段不存在,Hold 不会有本字段 | - | 142 |
size
字段实际上在RPE中显示为宽度,即只能控制音符的宽度而不是音符的整个大小。
Event
本页将介绍判定线事件层级下的普通事件。
- RPE中,一共有五种普通事件,它们分别是:
moveXEvents
(X轴移动事件)、moveYEvents
(Y轴移动事件)、rotateEvents
(旋转事件)、alphaEvents
(不透明度事件)、speedEvents
(音符流速事件)。 - 在层级下,这些字段都对应一个
JsonArray
,每一个元素代表一个事件。 - 在稍早版本,XY轴移动事件分离(XY事件不同时出现与消失)会被制谱器报错,因为这是对早期PhiEditor Chart的兼容,现在它不被报错,其本身也只是可选项。
- 当前判定线无某一个事件时,无对应字段而非空数组。
除了流速事件外的每个普通事件都应该含有以下字段。
字段名 | 类型 | 描述 | 默认值 | 加入版本 |
---|---|---|---|---|
bezier | int | 缓动是否为贝塞尔曲线,0 为不是,1 为是 | 0 | - |
bezierPoints | JsonArray | 贝塞尔曲线控制点,当bezier 为1 时生效,详见百度百科 | [ 0.0, 0.0, 0.0, 0.0 ] | - |
easingLeft | float | 缓动的左边界位置,最小为 0.0 ,最大为 1.0 | 0.0 | - |
easingRight | float | 缓动的右边界位置,最小为 0.0 ,最大为 1.0 | 1.0 | - |
easingType | int | 缓动类型,详见extend | 1 | - |
linkgroup | int | - | - | - |
start | float | 事件开始时数值 | - | - |
startTime | beat | 事件开始的时间 | - | - |
end | float | 事件结束时数值 | - | - |
endTime | beat | 事件结束的时间 | - | - |
- 坐标系锚点位于屏幕中心,X轴范围为
-675 ~ 675
,Y轴范围为-450 ~ 450
。 - 不透明度事件的正常范围为
0 ~ 255
,0
为完全透明,255
为完全不透明。- 若不透明度事件数值为负数,则会在隐藏判定线的同时隐藏这条判定线上的所有音符。(根据作者所述,此功能是废弃的非法功能但它仍然有效)
- 音符流速事件只有上述的
startTime
、endTime
、start
、end
、linkgroup
字段。- 音符流速事件不支持缓动,即只有线性变化。
- 流速为负数时,音符会向上飞,若音符为
Hold
,在Hold
尾出现时,整个音符都会出现(即使Hold
还没完全回到判定线正面)。
Python 示例 (不支持bezier):
- 定义
rpe_easing.py
(略) - 定义
Beat
:
@dataclass
class Beat:
var1: int
var2: int
var3: int
def __post_init__(self):
self.value = self.var1 + (self.var2 / self.var3)
self._hash = hash(self.value)
def __hash__(self) -> int:
return self._hash
- 定义
LineEvent
@dataclass
class LineEvent:
startTime: Beat
endTime: Beat
start: float|str|list[int]
end: float|str|list[int]
easingType: int
easingFunc: typing.Callable[[float], float] = rpe_easing.ease_funcs[0]
def __post_init__(self):
if not isinstance(self.easingType, int): self.easingType = 1
self.easingType = 1 if self.easingType < 1 else (len(rpe_easing.ease_funcs) if self.easingType > len(rpe_easing.ease_funcs) else self.easingType)
self.easingFunc = rpe_easing.ease_funcs[self.easingType - 1]
- 定义
easing_interpolation
def easing_interpolation(
t: float, st: float,
et: float, sv: float,
ev: float, f: typing.Callable[[float], float]
):
if t == st: return sv
return f((t - st) / (et - st)) * (ev - sv) + sv
default
为事件默认值- 则有:
def GetEventValue(t: float, es: list[LineEvent], default):
for e in es:
if e.startTime.value <= t <= e.endTime.value:
if isinstance(e.start, float|int):
return easing_interpolation(t, e.startTime.value, e.endTime.value, e.start, e.end, e.easingFunc)
elif isinstance(e.start, str):
return e.start
elif isinstance(e.start, list):
r = easing_interpolation(t, e.startTime.value, e.endTime.value, e.start[0], e.end[0], e.easingFunc)
g = easing_interpolation(t, e.startTime.value, e.endTime.value, e.start[1], e.end[1], e.easingFunc)
b = easing_interpolation(t, e.startTime.value, e.endTime.value, e.start[2], e.end[2], e.easingFunc)
return (r, g, b)
return default
特殊事件
本页讲解RPE的特殊事件,俗称故事板,位于事件编辑的第五个层级。
每一个事件字段都对应一个 JsonArray
,每一个元素对应一个事件。
除了 inclineEvents
(倾斜事件),其他事件在没有使用时都没有对应字段
colorEvents
颜色事件,可以控制判定线或纹理的颜色,它包含以下字段:
字段名 | 类型 | 描述 | 默认值 | 加入版本 |
---|---|---|---|---|
bezier | int | 缓动是否为贝塞尔曲线,0 为不是,1 为是 | 0 | - |
bezierPoints | JsonArray | 贝塞尔曲线控制点,当bezier 为1 时生效,详见百度百科 | [ 0.0, 0.0, 0.0, 0.0 ] | - |
easingLeft | float | 缓动的左边界位置,最小为 0.0 ,最大为 1.0 | 0.0 | - |
easingRight | float | 缓动的右边界位置,最小为 0.0 ,最大为 1.0 | 1.0 | - |
easingType | int | 缓动类型,详见extend | 1 | - |
linkgroup | int | - | - | - |
start | JsonArray | 事件开始时颜色,三个值分别对应 RGB ;最大为 255 ,最小为 0 | - | - |
startTime | beat | 事件开始的时间 | - | - |
end | JsonArray | 事件结束时颜色,三个值分别对应 RGB ;最大为 255 ,最小为 0 | - | - |
endTime | beat | 事件结束的时间 | - | - |
scaleXEvents
X轴缩放事件,可以控制判定线、纹理或文字的宽度缩放,它包含以下字段:
字段名 | 类型 | 描述 | 默认值 | 加入版本 |
---|---|---|---|---|
bezier | int | 缓动是否为贝塞尔曲线,0 为不是,1 为是 | 0 | - |
bezierPoints | JsonArray | 贝塞尔曲线控制点,当bezier 为1 时生效,详见百度百科 | [ 0.0, 0.0, 0.0, 0.0 ] | - |
easingLeft | float | 缓动的左边界位置,最小为 0.0 ,最大为 1.0 | 0.0 | - |
easingRight | float | 缓动的右边界位置,最小为 0.0 ,最大为 1.0 | 1.0 | - |
easingType | int | 缓动类型,详见extend | 1 | - |
linkgroup | int | - | - | - |
start | float | 事件开始时缩放 | 1 | - |
startTime | beat | 事件开始的时间 | - | - |
end | float | 事件结束时缩放 | 1 | - |
endTime | beat | 事件结束的时间 | - | - |
scaleYEvents
Y轴缩放事件,可以控制判定线、纹理或文字的高度缩放,它包含以下字段:
字段名 | 类型 | 描述 | 默认值 | 加入版本 |
---|---|---|---|---|
bezier | int | 缓动是否为贝塞尔曲线,0 为不是,1 为是 | 0 | - |
bezierPoints | JsonArray | 贝塞尔曲线控制点,当bezier 为1 时生效,详见百度百科 | [ 0.0, 0.0, 0.0, 0.0 ] | - |
easingLeft | float | 缓动的左边界位置,最小为 0.0 ,最大为 1.0 | 0.0 | - |
easingRight | float | 缓动的右边界位置,最小为 0.0 ,最大为 1.0 | 1.0 | - |
easingType | int | 缓动类型,详见extend | 1 | - |
linkgroup | int | - | - | - |
start | float | 事件开始时缩放 | 1 | - |
startTime | beat | 事件开始的时间 | - | - |
end | float | 事件结束时缩放 | 1 | - |
endTime | beat | 事件结束的时间 | - | - |
textEvents
文字事件,可以控制文字的显示,它包含以下字段:
字段名 | 类型 | 描述 | 默认值 | 加入版本 |
---|---|---|---|---|
bezier | int | 缓动是否为贝塞尔曲线,0 为不是,1 为是 | 0 | - |
bezierPoints | JsonArray | 贝塞尔曲线控制点,当bezier 为1 时生效,详见百度百科 | [ 0.0, 0.0, 0.0, 0.0 ] | - |
easingLeft | float | 缓动的左边界位置,最小为 0.0 ,最大为 1.0 | 0.0 | - |
easingRight | float | 缓动的右边界位置,最小为 0.0 ,最大为 1.0 | 1.0 | - |
easingType | int | 缓动类型,详见extend | 1 | - |
linkgroup | int | - | - | - |
start | string | 事件开始时字符 | - | - |
startTime | beat | 事件开始的时间 | - | - |
end | string | 事件结束时字符 | - | - |
endTime | beat | 事件结束的时间 | - | - |
font | string | 文字字体(本字段需看下方解释) | 请看下方解释 | - |
- 从
152
版本开始,font
字段在为cmdysj
默认字体时不会有本字段,只有有自定义字体时才会存在本字段。 - 在
152
版本之前,font
字段默认存在且默认为cmdysj
。 - 此事件设置缓动可能不会有效,也可能会出现未定义的错误导致模拟器崩溃,详见issue。
- 文字事件的文字中含有
%P%
时,可以让文本中的数字在事件播放过程中根据缓动动态变化。 - 有文字事件的判定线会始终隐藏,只显示文字(即使播放的地方没有文字事件),也会清除自定义纹理。
- 有文字事件但是没有颜色事件时,文字的颜色会始终为白色。
- 从
153
版本开始,文字事件中的文字可以包含\n
换行符且有效(其他的不可用)。
paintEvents
画笔事件,此事件在 143
版本被 shader
编辑功能取代。
gifEvents
GIF播放进度事件,此事件在 150
版本与GIF判定线纹理一同加入,用于控制GIF的播放进度。
字段名 | 类型 | 描述 | 默认值 | 加入版本 |
---|---|---|---|---|
easingType | int | 缓动类型,详见extend | 1 | - |
linkgroup | int | - | - | - |
start | float | 事件开始时GIF的播放进度 | - | - |
startTime | beat | 事件开始的时间 | - | - |
end | float | 事件结束时GIF的播放进度 | - | - |
endTime | beat | 事件结束的时间 | - | - |
- 若判定线纹理是GIF但是没有
gifEvents
时,GIF会自动循环播放。 - GIF的第一帧为
0.0
,最后一帧为1.0
,若播放进度超出此范围,GIF将会自动循环播放。 - 若当前播放进度没有
gifEvents
时,GIF会自动循环播放。 - 文字事件同样会将此纹理清除。
- 超过
15MB
大小的GIF会使RPE在加载谱面时弹出解码失败。 - 纹理为GIF以后,流速事件会被替换为此事件的编辑,所以理论上此事件不可能与流速事件同时出现。
- 位于其他层级的
gifEvents
会被忽略,且RPE纠错会标红。
inclineEvents
倾斜事件,疑似已被弃用,但是默认会在 extended
字段下留一个垫底事件。
此事件无法在RPE中编辑。
- 倾斜事件开始结束数值为倾斜角度,对
Hold
无效。 - 此事件年久失修,无法验证其行为,请谅解。
扩展参数
attachUI
attachUI
是RPE独有特性,它允许你使用判定线绑定UI元素,使你可以控制UI的位置,透明度,大小等。
属性对应UI元素列表:
值 | 对应UI元素 | RPE设置中对应数字 | 注 |
---|---|---|---|
pause | 暂停按钮 | 1 | - |
combonumber | 连击数 | 2 | 绑定此UI会使此UI透明度受到Alpha事件影响,默认连击大于等于 3 时才会显示 |
combo | 连击数下的 combo 文字 | 3 | 同上 |
score | 分数 | 4 | - |
bar | 进度条 | 5 | RPE 1.4.0 及以前,此属性绑定的为曲名左侧的白色竖条 |
name | 谱面名称 | 6 | - |
level | 谱面等级 | 7 | - |
- 在UI被绑定后,判定线将会自动隐藏,UI可以通过类似于子线的方式进行操作,不同的是可以操作UI角度和透明度;判定线实际位置仍然不变。
anchor
anchor
是RPE独有特性,它允许你设置判定线的锚点,它的设计是为文字事件服务的。
- 在RPE中,此设置在顶栏工具栏第二页中,两个数值用空格分割。
- 它是一个
JsonArray
,两个值对应x
和y
坐标。 x
默认为0.5
,即中心,1
时判定线向左移,0
时判定线向右移。y
默认为0.5
,即中心,1
时判定线向下移,0
时判定线向上移。
Texture
RPE允许你设置判定线的 Texture
字段来修改判定线的纹理,当判定线的纹理被修改后,判定线颜色不再受到AP/FC判定线颜色指示影响。
- 若不使用scaleXEvents和scaleYEvents,修改判定线纹理大小,则默认缩放为
1
。 - 若纹理为一个GIF动图,则会受到gifEvents的影响。(
150
版本开始支持)
easingType
easingType
是RPE用于对应缓动的数字标识,对照表如下:
值 | 对应缓动 | 注 |
---|---|---|
1 | Linear | - |
2 | Out Sine | - |
3 | In Sine | - |
4 | Out Quad | - |
5 | In Quad | - |
6 | In Out Sine | - |
7 | In Out Quad | - |
8 | Out Cubic | - |
9 | In Cubic | - |
10 | Out Quart | - |
11 | In Quart | - |
12 | In Out Cubic | - |
13 | In Out Quart | - |
14 | Out Quint | - |
15 | In Quint | - |
16 | Out Expo | - |
17 | In Expo | - |
18 | Out Circ | - |
19 | In Circ | - |
20 | Out Back | - |
21 | In Back | - |
22 | In Out Circ | - |
23 | In Out Back | - |
24 | Out Elastic | - |
25 | In Elastic | - |
26 | Out Bounce | - |
27 | In Bounce | - |
28 | In Out Bounce | - |
29 | In Out Elastic | 无法在RPE使用 |
你可以在这个网站查看它们的函数等信息。
Python 缓动示例
import math
import typing
ease_funcs:list[typing.Callable[[float], float]] = [
lambda t: t, # linear - 1
lambda t: math.sin((t * math.pi) / 2), # out sine - 2
lambda t: 1 - math.cos((t * math.pi) / 2), # in sine - 3
lambda t: 1 - (1 - t) * (1 - t), # out quad - 4
lambda t: t ** 2, # in quad - 5
lambda t: -(math.cos(math.pi * t) - 1) / 2, # io sine - 6
lambda t: 2 * (t ** 2) if t < 0.5 else 1 - (-2 * t + 2) ** 2 / 2, # io quad - 7
lambda t: 1 - (1 - t) ** 3, # out cubic - 8
lambda t: t ** 3, # in cubic - 9
lambda t: 1 - (1 - t) ** 4, # out quart - 10
lambda t: t ** 4, # in quart - 11
lambda t: 4 * (t ** 3) if t < 0.5 else 1 - (-2 * t + 2) ** 3 / 2, # io cubic - 12
lambda t: 8 * (t ** 4) if t < 0.5 else 1 - (-2 * t + 2) ** 4 / 2, # io quart - 13
lambda t: 1 - (1 - t) ** 5, # out quint - 14
lambda t: t ** 5, # in quint - 15
lambda t: 1 if t == 1 else 1 - 2 ** (-10 * t), # out expo - 16
lambda t: 0 if t == 0 else 2 ** (10 * t - 10), # in expo - 17
lambda t: (1 - (t - 1) ** 2) ** 0.5, # out circ - 18
lambda t: 1 - (1 - t ** 2) ** 0.5, # in circ - 19
lambda t: 1 + 2.70158 * ((t - 1) ** 3) + 1.70158 * ((t - 1) ** 2), # out back - 20
lambda t: 2.70158 * (t ** 3) - 1.70158 * (t ** 2), # in back - 21
lambda t: (1 - (1 - (2 * t) ** 2) ** 0.5) / 2 if t < 0.5 else (((1 - (-2 * t + 2) ** 2) ** 0.5) + 1) / 2, # io circ - 22
lambda t: ((2 * t) ** 2 * ((2.5949095 + 1) * 2 * t - 2.5949095)) / 2 if t < 0.5 else ((2 * t - 2) ** 2 * ((2.5949095 + 1) * (t * 2 - 2) + 2.5949095) + 2) / 2, # io back - 23
lambda t: 0 if t == 0 else (1 if t == 1 else 2 ** (-10 * t) * math.sin((t * 10 - 0.75) * (2 * math.pi / 3)) + 1), # out elastic - 24
lambda t: 0 if t == 0 else (1 if t == 1 else - 2 ** (10 * t - 10) * math.sin((t * 10 - 10.75) * (2 * math.pi / 3))), # in elastic - 25
lambda t: 7.5625 * (t ** 2) if (t < 1 / 2.75) else (7.5625 * (t - (1.5 / 2.75)) * (t - (1.5 / 2.75)) + 0.75 if (t < 2 / 2.75) else (7.5625 * (t - (2.25 / 2.75)) * (t - (2.25 / 2.75)) + 0.9375 if (t < 2.5 / 2.75) else (7.5625 * (t - (2.625 / 2.75)) * (t - (2.625 / 2.75)) + 0.984375))), # out bounce - 26
lambda t: 1 - (7.5625 * ((1 - t) ** 2) if ((1 - t) < 1 / 2.75) else (7.5625 * ((1 - t) - (1.5 / 2.75)) * ((1 - t) - (1.5 / 2.75)) + 0.75 if ((1 - t) < 2 / 2.75) else (7.5625 * ((1 - t) - (2.25 / 2.75)) * ((1 - t) - (2.25 / 2.75)) + 0.9375 if ((1 - t) < 2.5 / 2.75) else (7.5625 * ((1 - t) - (2.625 / 2.75)) * ((1 - t) - (2.625 / 2.75)) + 0.984375)))), # in bounce - 27
lambda t: (1 - (7.5625 * ((1 - 2 * t) ** 2) if ((1 - 2 * t) < 1 / 2.75) else (7.5625 * ((1 - 2 * t) - (1.5 / 2.75)) * ((1 - 2 * t) - (1.5 / 2.75)) + 0.75 if ((1 - 2 * t) < 2 / 2.75) else (7.5625 * ((1 - 2 * t) - (2.25 / 2.75)) * ((1 - 2 * t) - (2.25 / 2.75)) + 0.9375 if ((1 - 2 * t) < 2.5 / 2.75) else (7.5625 * ((1 - 2 * t) - (2.625 / 2.75)) * ((1 - 2 * t) - (2.625 / 2.75)) + 0.984375))))) / 2 if t < 0.5 else (1 +(7.5625 * ((2 * t - 1) ** 2) if ((2 * t - 1) < 1 / 2.75) else (7.5625 * ((2 * t - 1) - (1.5 / 2.75)) * ((2 * t - 1) - (1.5 / 2.75)) + 0.75 if ((2 * t - 1) < 2 / 2.75) else (7.5625 * ((2 * t - 1) - (2.25 / 2.75)) * ((2 * t - 1) - (2.25 / 2.75)) + 0.9375 if ((2 * t - 1) < 2.5 / 2.75) else (7.5625 * ((2 * t - 1) - (2.625 / 2.75)) * ((2 * t - 1) - (2.625 / 2.75)) + 0.984375))))) / 2, # io bounce - 28
lambda t: 0 if t == 0 else (1 if t == 0 else (-2 ** (20 * t - 10) * math.sin((20 * t - 11.125) * ((2 * math.pi) / 4.5))) / 2 if t < 0.5 else (2 ** (-20 * t + 10) * math.sin((20 * t - 11.125) * ((2 * math.pi) / 4.5))) / 2 + 1) # io elastic - 29
]
Phigros Official 谱面文档
Phigros Official谱面根目录结构
- 目前
Phigros Official
谱面的数据计算与渲染已经十分成熟
谱面根目录结构
formatVersion
formatVersion
是一个int
- 该项影响谱面判定线移动事件的解析方式
- 该值可能 为
1
,3
或其他
offset
offset
是一个float
- 该项为谱面的延迟, 单位为秒
- 为正数时, 谱面比音乐快, 为负数时, 谱面比音乐慢
judgeLineList
judgeLineList
是一个JsonArray
,包含若干个JsonObject
,每个JsonObject
代表一个判定线。
Note
note,即音符,是谱面的主要构成之一,每个note都应该含有以下参数:
字段名 | 类型 | 描述 | 单位 |
---|---|---|---|
type | int | note的类型 | - |
time | int | note的时间 | 1.875 / bpm |
holdTime | float | hold的持续时间 (仅hold, 其他为0.0) | 1.875 / bpm |
positionX | float | note的横向坐标 | 宽度单位 |
speed | float | 对于非hold: note的速度倍率 对于hold: hold的打击时的速度 tip: hold在打击之前的速度倍率恒为1 | - |
floorPosition | float | note初始化时距离判定线的高度 (仅方便计算) | 高度单位 |
Event
本页将介绍判定线事件下的所有事件。
speedEvent
字段名 | 类型 | 描述 | 单位 |
---|---|---|---|
startTime | float | 事件的开始时间 | 1.875 / bpm |
endTime | float | 事件的结束时间 | 1.875 / bpm |
value | float | 事件的值 | 高度单位 |
floorPosition | float | 速度事件开始时判定线共计以流过的速度 (仅方便计算, 高版本不存在) | 高度单位 |
judgeLineMoveEvent
字段名 | 类型 | 描述 | 单位 |
---|---|---|---|
startTime | float | 事件的开始时间 | 1.875 / bpm |
endTime | float | 事件的结束时间 | 1.875 / bpm |
formatVersion
为1
字段名 | 类型 | 描述 | 单位 |
---|---|---|---|
start | int | 事件的开始坐标 | - |
end | int | 事件的结束坐标 | - |
-
坐标计算 (Python):
x = (v - v % 1000) // 1000
y = v % 1000
- 单位:
x
1 / 880
谱面渲染范围宽度y
1 / 520
谱面渲染范围高度
-
转化为formatVersion为3的坐标 (python):
- 原事件以
e
表示, 新事件以ne
表示 ne.start = (e.start - e.start % 1000) // 1000
ne.end = (e.end - e.end % 1000) // 1000
ne.start2 = e.start % 1000
ne.end2 = e.end % 1000
- 原事件以
-
formatVersion
为3
字段名 | 类型 | 描述 | 单位 |
---|---|---|---|
start | float | 事件的开始x坐标 | 谱面渲染范围宽度 |
end | float | 事件的结束x坐标 | 谱面渲染范围宽度 |
start2 | float | 事件的开始y坐标 | 谱面渲染范围高度 |
end2 | float | 事件的结束y坐标 | 谱面渲染范围高度 |
judgeLineRotateEvent
字段名 | 类型 | 描述 | 单位 |
---|---|---|---|
startTime | int | 事件的开始时间 | 1.875 / bpm |
endTime | int | 事件的结束时间 | 1.875 / bpm |
start | float | 事件的开始值 | 角度 |
end | float | 事件的结束值 | 角度 |
judgeLineDisappearEvent
字段名 | 类型 | 描述 | 单位 |
---|---|---|---|
startTime | float | 事件的开始时间 | 1.875 / bpm |
endTime | float | 事件的结束时间 | 1.875 / bpm |
start | float | 事件的开始值 | - |
end | float | 事件的结束值 | - |
judgeLine
每一个judgeLine(判定线)都含有以下字段:
字段名 | 类型 | 描述 |
---|---|---|
bpm | float | 该判定线的bpm值 |
notesAbove | JsonArray | 正面下落的音符 |
notesBelow | JsonArray | 反面下落的音符 |
speedEvents | JsonArray | 速度事件 |
judgeLineMoveEvents | JsonArray | 移动事件 |
judgeLineRotateEvents | JsonArray | 旋转事件 |
judgeLineDisappearEvents | JsonArray | 透明度事件 |
- 注意: 所有事件及Note的有关时间的项, 单位都为
1.875 / bpm
s - 这里我们定义:
- "宽度单位" 为
0.05625 * 谱面渲染范围宽度
- "高度单位" 为
0.6 * 谱面渲染范围高度
- "宽度单位" 为
- 事件
- Note
音乐文件格式
目前支持的音乐文件格式包括:
.mp3
.ogg
.wav
注意:
mp3
格式可能会存在不可预测的延迟, 请尽量使用ogg
文件来避免wav
为未经压缩的原始音频信号, 占用空间较多, 可能会导致谱面上传失败
扩展特性文档
prpr 在支持 Phigros 官方谱面格式、pec
格式和 rpe
格式之外,加入了一些谱面特性。这些谱面特性目前只在 prpr 及基于其构建的客户端上可用。它们包括:
关于特性的配置需要全部存储在压缩包根目录的 extra.json
中。为了使用谱面特性,你需要手动编辑 JSON 文件。如果你不知道 JSON 是什么,请参阅 JSON 教程。
BPM 配置
在进行相关配置前,你需要在 extra.json
中预先设定好曲目的 BPM。例如:
{
"bpm": [
{ "time": [0, 0, 1], "bpm" : 200.0 },
{ "time": [10, 1, 2], "bpm" : 250.0 }
],
...
}
这里表示歌曲最初 BPM 为 200
,而后在十又二分之一拍是转换为 250
。
动画变量
在配置文件中会常常用到动画变量。例如,在 RPE 中,的 X 坐标变化事件实际上就是 X 坐标作为一个动画变量。
prpr 的动画变量有两种格式。第一种即单个值,表示该变量不会更改,一直维持同一个值。这种格式是为了方便填写而适用的。第二种格式与 RPE 中的格式相同,是由多个 Event
组成的。单个 Event
格式如下所示:
{
"startTime": [0, 0, 1],
"endTime": [0, 0, 1],
"easingType": 2,
"easingLeft": 0.0,
"easingRight": 1.0,
"start": ...,
"end": ...
}
其中 startTime
、endTime
是事件的开始和结束时间;easingType
是缓动类型;easingLeft
、easingRight
是可选的,代表裁剪缓动的开始与结束位置。
start
和 end
代表该时间开始和结束处该变量的值。这里值的类型是取决于变量本身的类型的,它可以是下面类型中的任意一种:
float
:单个小数;vec2
:两个小数表示的二维向量,用数组表示;color
:四个整数(0-255)表示的颜色,格式为[R, G, B, A]
。
例如,下面的动画变量将在特定时段内从 0
到 1
线性变化:
[
{
"startTime": [2, 0, 1],
"endTime": [4, 0, 1],
"easingType": 2,
"start": 0.0,
"end": 0.1
}
]
特效
特效是在一段时间内对谱面整体施加着色器效果的特性,可以用以达成一些普通谱面无法实现或难以实现的视觉效果。prpr 预置了一批着色器,你也可以自己编写着色器,详见 自行编写着色器。
格式
你需要在 extra.json
中加入 effects
字段,其内容为 Effect
的数组。
Effect
单个 Effect
的格式如下所示:
{
"start": [0, 0, 1],
"end": [0, 0, 1],
"shader": "着色器名字",
"global": false,
"vars": {
...
}
}
start
和 end
指定了特效的开始和结束时间,格式与 RPE 默认的时间表示格式相同([小节数, 分子, 分母]
)。
shader
,即着色器名字,既可以是内置着色器,也可以是自定义的着色器。
global
决定了该特效会不会影响到 UI 元素(连击数、暂停按钮等)。可以不填,默认为 false
。
vars
是可选项,它是一个变量名字到 动画变量
的映射,用于指定着色器的参数(或者说,着色器的 uniform
变量)。即,假设我需要指定两个变量 power
和 radius
的值,我可以这样写:
{
...,
"vars": {
"power": ...,
"radius": ...
}
}
示例
下面的例子将在 [2, 0, 1]
到 [4, 0, 1]
中使用 chromatic
内置着色器,并将 power
这一参数在这一时段内从 0
到 0.1
线性变化,且 sampleCount
被固定为 5
:
{
...,
"effects": [
{
"start": [2, 0, 1],
"end": [4, 0, 1],
"shader": "chromatic",
"vars": {
"power": [
{
"startTime": [2, 0, 1],
"endTime": [4, 0, 1],
"easingType": 2,
"start": 0.0,
"end": 0.1
}
],
"sampleCount": 5
}
}
]
}
内置着色器
你可以在 这里 查看所有的内置着色器,以及它们的参数和相关说明。
内置着色器
该部分列举了 prpr 的内置着色器。
chromatic
色差效果。
参数
sampleCount
(整数,默认3
,范围1-64
):采样次数,越高显示会更连贯,开销也更大;power
(小数,默认0.01
):强度,或者说偏移距离。
circleBlur
圆点模糊。会将像素点模糊放大为亮点,建议搭配参数动画使用。
参数
size
(小数,默认10.0
):原点的大小,以像素为单位。
fisheye
鱼眼效果。当 power
为负数时可以做出类似隧洞穿梭的效果。
power
为负数:
power
为正数:
参数
power
(小数,默认-0.1
):鱼眼缩放比例。
glitch
混乱 / 错误效果。该效果会随时间产生不确定的闪烁。
参数
power
(小数,默认0.3
):闪烁强度;rate
(小数,默认0.6
,范围0-1
):闪烁频率。0
为总不闪烁,1
为总是闪烁;speed
(小数,默认5.0
):整个闪烁动画的速度;blockCount
(小数,默认30.5
):见上图,错位条带的条数(大概);colorRate
(小数,默认0.01
,范围0-1
):色差的距离。
grayscale
灰度化效果。
参数
factor
(小数,默认1.0
):灰度化程度。0
为不进行灰度化处理,1
为完全进行灰度化处理。
noise
噪音效果。向画面附加一层随机的模糊效果。
参数
seed
(小数,默认81.0
):用来生成随机图案的种子。通过连续地变化种子可以使图案发生连续的变化。power
(小数,默认0.03
,范围0-1
):见上图,模糊的程度(像素偏移范围)。
pixel
像素化效果。
参数
size
(小数,默认10.0
):像素化后的单个像素大小。
radialBlur
放射模糊。
参数
centerX
(小数,默认0.5
,范围0-1
):放射中心的 X 坐标;centerY
(小数,默认0.5
,范围0-1
):放射中心的 Y 坐标;power
(小数,默认0.01
,范围0-1
):放射强度;sampleCount
(整数,默认3
,范围1-64
):采样次数,越高显示会更连贯,开销也更大。
shockwave
冲击波效果。建议配合 progress
的参数动画使用。
参数
progress
(小数,默认0.2
,范围0-1
):冲击波的进度。冲击波是一个动画,0
进度为开始,直到1
进度结束;centerX
(小数,默认0.5
,范围0-1
):冲击波中心的 X 坐标;centerY
(小数,默认0.5
,范围0-1
):冲击波中心的 Y 坐标;width
(小数,默认0.1
):冲击波的宽度;distortion
(小数,默认0.8
):冲击波扭曲程度;expand
(小数,默认10.0
):冲击波延伸广度。
vignette
虚光照效果。将屏幕的边缘调暗或调成其他颜色,可以用来模拟 Arcaea 的部分异象效果。
参数
color
(颜色,默认黑色):边缘的颜色;extend
(小数,默认0.25
,范围0-1
):边缘的延伸程度,值越大黑色部分越多;radius
(小数,默认15.0
):中央光照大小,值越小受到影响的范围越大。
自行编写着色器
用于特效的着色器是片元着色器。你可以自行编写着色器并将其附加于谱面压缩包中。在 shader
字段,你需要填写 /shader路径
。注意这里的 /
是必不可少的,它将内置着色器和自定义着色器区分开来。
为保证兼容性,着色器版本要求为 GLSL 1.00。
内置变量
prpr 为着色器内置了如下变量:
varying vec2 uv; // 材质 UV
uniform vec2 screenSize; // 屏幕大小(注意是整个屏幕,并不只是谱面部分)
uniform sampler2D screenTexture; // 屏幕材质(同样也是整个屏幕的材质)
uniform float time; // 谱面时间,以秒为单位
还有一些变量是虽然存在,但在片元着色器中使用无意义的。在定义自己的变量时,你应该避免与它们重名:
uniform mat4 Model;
uniform mat4 Projection;
uniform vec2 UVScale;
着色器变量
为了在谱面中可以指定参数,你需要这样定义你的着色器 uniform
变量:
uniform type name; // %def%
其中 type
为类型,目前支持 float
、vec2
和 vec4
;name
为变量名;def
为默认值。三个都是不可缺少的。
示例
下面的示例着色器将会根据 factor
的值给屏幕叠加上强弱不等的红色:
#version 100
precision mediump float;
varying lowp vec2 uv;
uniform sampler2D screenTexture;
uniform float factor; // %0.5% 0..1
void main() {
gl_FragColor = mix(texture2D(screenTexture, uv), vec4(1.0, 0.0, 0.0, 1.0), factor);
}
视频背景
在 prpr 中,你可以在背景播放视频,但音频并不会被播放。但由于大部分视频文件都较大,且 prpr 的谱面上传限制是 10MB,带有大型 BGA 的谱可能无法上传。如果在上传中需要用到视频背景,建议是用来实现一些小的动画、并对视频进行压缩(包括但不仅限于删除音频轨道)。
格式
你需要在 extra.json
中加入 videos
字段,其内容为 Video
的数组。
Video
单个 Video
的格式如下所示:
{
"path": "bga.mp4",
"time": [0, 0, 1],
"scale": "cropCenter",
"alpha": 1.0,
"dim": 0.3
}
其中 path
为必填,指向视频文件路径,其它四项均为选填。
time
用拍数表示视频的开始时间。
scale
表示视频的缩放方式。在制作谱面时,你需要考虑在谱面不同宽高比下的显示情况。为此,scale
有三种值可选:
cropCenter
(默认):等比例放大视频直到视频能恰好填满屏幕;inside
:等比例缩放视频,保证整个视频能显示在屏幕内;fit
:强制拉伸视频到整个屏幕。
alpha
和 dim
分别代表视频的不透明度(若透明,则会显示下方的曲绘)和昏暗程度,他们都是 动画变量
。
解锁动画
请注意解锁动画和视频背景的区别
解锁动画允许在玩家初次游玩谱面前播放一段视频。
使用方法
目前 UI 还没做好,稍微有些麻烦。下面的步骤建议在 Windows 上进行。
如果你的谱面已经有了 info.yml
,请跳转到第三步。
- 在 Phira 中导入你的谱面;
- 在
data/charts/custom
中搜索你的谱面 ID(如9067228
),找到对应的文件夹,在里面找到info.yml
,拷贝到你的谱面文件夹里(注意不是导入到 Phira 后的文件夹,是 pez 解压出来的文件夹); - 向你的谱面文件夹(注意不是导入到 Phira 后的文件夹,是 pez 解压出来的文件夹)内加入解锁动画,假设文件名为
unlock.mp4
; - 在
info.yml
中找到unlockVideo: null
一行,替换为unlockVideo: unlock.mp4
(如果没有这一行,则加上这一行); - 重新导入你的谱面。
Phira 活动说明
Phira 已经成功~~(存疑)~~举办了一些活动, 本文主要介绍在目前的项目结构下, Phira 活动相关的机制
基础概念
活动
活动和活动页面是一一对应的关系, 活动页面的设计采用 UML 语言, 相关的文档见此处
活动具有一个 owner
, 通常设置为活动主办方的个人或团体账户, 活动隐藏时仅对服务器管理员和其 owner
可见
活动还具有起始时间, 开始时间前活动可见但无法参与, 在开始时间服务器会向全服务器玩家发送游戏内消息告知活动已开始, 结束时间后活动无法报名参加
活动可以锁定, 锁定后所有人均无法报名参加, 但不影响可见性
活动参加后会产生排行榜, 具体运算规则需要单独设置, 一般保持活动锁定
谱面合集
实际上谱面合集可以任意创建, 与活动关联并不紧密
活动页面中可以引用一个或多个谱面合集, 谱面合集中的谱面不会受到 hidden
等字段的限制, 总是对所有玩家可见
举办活动
要举办一次活动, 活动主办方需要向 TeamFlos 时任社区管理员或指定外部联络人员(下称对接人员)提供相关材料, 具体为
- 活动流程, 或活动策划大纲
- 活动页面设计 UML 源码
- 活动页面设计中用到的美术素材(考虑到服务器负载和可用性等问题, 推荐采用 self host 的方式, 向 TeamFlos 提供的源码中包含相关素材的 URL 即可)
- 活动需要利用的谱面合集与其包含相应的谱面
在此基础上有额外需求请和对接人员确认, 以便及时进行可行性评估。
UML 文档
UML 文件是 UI 描述文件, 可以用来创建于代码解耦的可定制界面.
语法
UML 文件格式是纯文本的人类可读格式. UML 由 Element(元素), 注释和变量定义语句构成, 每个元素占据一或多行.
坐标
坐标原点在进入活动页后下滑一屏后的左上角处, x
轴向右, y
轴向下.
屏幕宽度总是 2
, 可以通过宽高比计算屏幕高度, 参见内置变量.
数据类型
UML 中有如下数据类型:
Float
单精度浮点数, 任意的数字都是 Float
.
Rect
矩形. 由 [left, top, width, height]
(分别是左上角 x
坐标, 左上角 y
坐标, 矩形宽度, 矩形高度)定义.
Rect
在定义后可以访问一些只读属性, 具体包括:
Rect.l
或Rect.x
: 左上角x
坐标Rect.t
或Rect.y
: 左上角y
坐标Rect.w
: 矩形宽度Rect.h
: 矩形高度Rect.r
: 右下角x
坐标Rect.b
: 右下角y
坐标Rect.cx
: 中心x
坐标Rect.cy
: 中心y
坐标
Bool
布尔值. 值是 true
或 false
, 目前只能用于元素的属性中.
String
字符串. 用双引号括起来的文本, 用于表示按钮行为, 颜色, URL, 具体如下:
Color
: 颜色. 可以是十六进制 RGB 或 ARGB 颜色值(如"#ff0000"
或"#7fffffff"
)或颜色名(如"red"
). 目前可用的颜色名有:"white"
"black"
"red"
"blue"
"yellow"
"green"
"gray"
Action
: 按钮行为. 可用的值有:"join"
: 参与该活动"open:url"
: 打开指定 URL
URL
: 网址
表达式
表达式是由 Float
, 变量, 运算符和函数组成的. 表达式经过计算是 Float
类型的值.
运算符
可以使用的运算符有:
+
: 加法-
: 减法*
: 乘法/
: 除法()
: 括号, 用于改变运算顺序==
: 是否等于, 成立时值为1
, 否则为0
!=
: 是否不等于, 成立时值为1
, 否则为0
>
: 是否大于, 成立时值为1
, 否则为0
<
: 是否小于, 成立时值为1
, 否则为0
>=
: 是否大于等于, 成立时值为1
, 否则为0
<=
: 是否小于等于, 成立时值为1
, 否则为0
函数
可以使用的函数有:
sin(x)
: 正弦函数cos(x)
: 余弦函数tan(x)
: 正切函数abs(x)
: 绝对值函数exp(x)
: 指数函数atan2(x)
: 反正切函数ln(x)
: 自然对数函数sig(x)
: 符号函数,x
为+0
或正数时返回1
, 为-0
或负数时返回-1
step(x, y)
: 阶跃函数,x < y
为0
时返回1
, 否则返回0
floor(x)
: 向下取整函数ceil(x)
: 向上取整函数round(x)
: 四舍五入函数max(x, y...)
: 最大值函数min(x, y...)
: 最小值函数clamp(x, min, max)
: 将x
限制在min
和max
之间
其中三角函数和反三角函数使用弧度制.
变量
变量是 UML 中可以被读取或者写入的值, 定义方式如下:
let name = value
定义一个变量 name
, 其值为 value
. value
可以是 Float
或 Rect
类型的值, 或结果是 Float
或 Rect
类型的表达式.
变量可以被重复定义, 但只有最后一次定义的值会被使用. 定义变量后, 你可以在新建元素或定义其他变量时使用这个变量.
内置变量
UML 内置了如下变量:
t
: 当前时间, 单位为秒top
: 实际高度与实际宽度之比o
: 滑动距离joined
: 是否已经参与了该活动, 参与了时值为1
, 否则为0
$h
: 用户能滚动的最大高度, 通过设置该变量的值来限制用户的滚动范围
UML 并不具备事件功能, 可以利用
t
,o
和按钮的按下时刻来为页面添加动态效果, 如动画, 换页等.
全局变量
全局变量是在 UML 文件的任何地方都可以被读取或者写入的变量. 全局变量的定义方式如下:
global name = @type
其中, type
只能为 btn
. 此后, 你仍然可以通过 let
的方式写入全局变量, 只有最后一次定义的值会被使用.
活动界面在每一帧都会被重画, 这意味着 UML 在每一帧都会重新计算所有变量的值.
但按钮的一些属性需要在帧之间保持不变, 因此需要使用全局变量.
元素
元素是活动界面中的可视或可交互的对象, 定义方式如下:
type(attr1: value1, attr2: value2, ...) { Hello! }
其中, type
为元素的类型, attrN
和 valueN
分别为属性名称和属性值. 不同类型的元素的属性也各不相同, 其中一些是必填, 会在介绍元素时具体标注.
元素的 id
属性为所有元素共有的可选属性, 在元素被定义后, 其 id
属性将自动对应一个表示该元素绘制范围的 Rect
变量.
后面的 { Hello }
是可选内容, 代表该元素的文字内容. 只对 p
(段落元素)生效.
段落元素 p
p(id, x, y, ax, ay, size, ml, mw, bl, c) {
Text
}
id
(选填): 元素 IDx
(选填, 默认为0
): 文本x
坐标y
(选填, 默认为0
): 文本y
坐标ax
(选填, 默认为0
): 文本横向对齐,[0, 1]
间的浮点数. 为0
时, 按照x
坐标左对齐, 为1
时右对齐,0.5
时居中ay
(选填, 默认为0
): 文本纵向对齐, 具体意义同上size
(选填, 默认为1
): 文本大小ml
(选填, 默认为false
): 文本是否多行渲染, 只在设置了mw
的条件下有效mw
(选填): 文本最大宽度. 在单行模式下, 超出范围的文本将被省略;多行模式下将自动换行bl
(选填, 默认为true
): 纵向对齐时是否按照基准线对齐c
(选填, 默认为"white"
): 文本颜色Text
(必填): 文本内容
p
的位置并不通过完整的矩形来定义, 但你仍可以用p
的id
属性来获取其矩形区域.
图片元素 img
img(id, url, r, c, t)
id
(选填): 元素 IDurl
(必填): 图片 URL, 用双引号括起来r
(必填): 图片显示的位置矩形, 类型为Rect
c
(选填, 默认为"white"
): 在图片上叠加的颜色. 若颜色半透明, 图片也会半透明t
(选填, 默认为cropCenter
): 图片的裁剪方式. 可选值有:cropCenter
: 保持图片比例, 从图片中心裁剪,保证r
被完全填满inside
: 保持图片比例, 使图片完全显示在r
中fit
: 拉伸为r
的大小
谱面合集元素 col
col(id, cid, r, rn, rh)
id
(选填): 元素 IDcid
(必填): 谱面合集 IDr
(必填): 谱面合集显示的位置矩形, 类型为Rect
rn
(选填, 默认为4
): 一行显示的谱面数rh
(选填, 默认为0.3
): 谱面高度
按钮元素 btn
btn(id, r, action)
id
(选填): 元素 IDr
(必填): 按钮显示的位置矩形, 类型为Rect
action
(选填): 按钮的点击行为, 可用的值参见数据类型
在元素被定义后, 其 id
属性除了表示该元素绘制范围的 Rect
变量, 还具有以下属性:
id.pressing
: 按钮是否正在被按下, 被按下时值为1
, 否则为0
id.last
: 按钮上次被按下的时刻, 从未被按下时值为-1
id.count
: 按钮被按下的次数
注释
你可以用 #
开头的行添加注释, 除了部分特殊格式的注释(注释表达式), 注释不会被执行, 可以用来提示当前代码的作用.
注释行在 #
后至少有一个字符, 否则会导致报错.
注释表达式
注释表达式是一种特殊的注释, 它是为了兼容旧版本设计的, 可以被执行的注释. 注释表达式的格式如下:
#>exp(attr1: value1, attr2: value2, ...)
其中, exp
表示该表达式的类型, 括号内的是该表达式的属性, 是可选内容, attrN
和 valueN
分别为属性名称和属性值. 不同类型的表达式的属性也各不相同, 其中一些是必填, 会在介绍表达式时具体标注.
下面逐个介绍各类型的注释表达式:
结束表达式 #>pop
#>pop
注释表达式以行为单位, 作用于数行内的元素, 变量等, 其作用范围是从表达式所在行开始到下一个 #>pop
表达式所在行结束. 因此, 多数其他表达式后面都必须跟一个 #>pop
表达式, 表示结束当前表达式的作用范围.
旋转表达式 #>rot
#>rot(angle, cx, cy)
angle
(必填): 旋转角度, 单位为弧度cx
(必填): 旋转中心x
坐标cy
(必填): 旋转中心y
坐标
平移表达式 #>tr
#>tr(dx, dy)
dx
(选填, 默认为0
): 横向平移距离dy
(选填, 默认为0
): 纵向平移距离
透明度表达式 #>alpha
#>alpha(a)
a
(选填, 默认为0
): 透明度, 取值范围为[0, 1]
,0
为完全透明,1
为完全不透明
矩阵表达式 #>mat
#>mat(x00, x01, x02, x03, x10, x11, x12, x13, x20, x21, x22, x23, x30, x31, x32, x33)
x00
(选填, 默认为0
): 矩阵第一行第一列的值x01
(选填, 默认为0
): 矩阵第一行第二列的值x02
(选填, 默认为0
): 矩阵第一行第三列的值x03
(选填, 默认为0
): 矩阵第一行第四列的值x10
(选填, 默认为0
): 矩阵第二行第一列的值x11
(选填, 默认为0
): 矩阵第二行第二列的值x12
(选填, 默认为0
): 矩阵第二行第三列的值x13
(选填, 默认为0
): 矩阵第二行第四列的值x20
(选填, 默认为0
): 矩阵第三行第一列的值x21
(选填, 默认为0
): 矩阵第三行第二列的值x22
(选填, 默认为0
): 矩阵第三行第三列的值x23
(选填, 默认为0
): 矩阵第三行第四列的值x30
(选填, 默认为0
): 矩阵第四行第一列的值x31
(选填, 默认为0
): 矩阵第四行第二列的值x32
(选填, 默认为0
): 矩阵第四行第三列的值x33
(选填, 默认为0
): 矩阵第四行第四列的值
可以通过矩阵表达式实现元素的缩放, 平移, 旋转等变换.
条件表达式 #>if
等
#>if(condition)
#>elif(condition)
#>else
#>fi
condition
(必填): 条件表达式, 可以是任意表达式, 当值为0
表示假, 否则表示真.
需要注意的是 #>if
并不适用 #>pop
来结束, 而是使用 #>fi
.
低版本兼容表达式 #>if-no-v2
#>if-no-v2
如果你的 UML 文件中使用了 V2 版本中没有的特性, 可以在使用这个表达式显示部分元素, 以提示使用低版本的 Phira 客户端的用户尽快升级.
注释表达式是 V2 版本的新功能, 只在 V2 版本中有效, 在 V1 版本中会被当作一般注释忽略.
#if-no-v2
表达式实际上的作用是忽略其作用范围内的内容.
如何调试
编译 phira-main
时加入参数 --features event_debug
, 然后在 Phira 客户端可执行文件同目录下新建名为 test.uml
的文件. 此后进入任意一个活动, 页面内容将与 test.uml
保持实时同步.
样例 UML
提供了几个 UML 的样例, 可供参考.
模板活动
此文件为 sjfhsjfh 于 2024-02-07 凌晨为 2024 羽笙杯创建的活动文件, 鉴于活动方要求过于抽象, sjfhsjfh 决定写一个仅包含基本功能的页面.
UML 源码
2024 圣诞夜惊魂
这是 sjfhsjfh 在
UML 源码
let w = 1920
let h = 1080
let rh = 2 * top
let bg_rect = [0, 0 - rh + o, 2, rh]
img(id: bg, url: "https://files-cf.phira.cn/events/xmas-2023-kedmue/bg.jpeg", r: bg_rect, t: cropCenter)
# Back button
let back_btn_rect = [bg_rect.x + 100 / w * 2, bg_rect.t + 50 / h * rh, -45 / w * 2, 75 / h * 1.18]
img(id: back_btn_img, url: "https://files-cf.phira.cn/ltc-arrow.png", r: back_btn_rect, t: fit)
btn(id: back_btn, r: back_btn_rect, action: "exit")
let chart_h = 0.4 * top
# left-top
let dx1 = 0.003 * sin(90 * t) + 0.01 * sin(11.5 * (1 + t / 100000) * t)
let dy1 = 0.004 * sin(87 * t) + 0.008 * sin(11.3 * (1 + t / 10000) * t)
let s1 = 1 + 0.03 * sin(10 * exp(1.01 * ln(t)) - 2 * t)
let col_r1 = [bg_rect.l + 0.87 + dx1 + 0.45 * (1 - s1), bg_rect.t + 0.452 * rh + dy1 + chart_h * (1 - s1), 0.45 * s1, chart_h * s1]
col(id: col_xmas, cid: 12762, r: col_r1, rn: 1, rh: chart_h * s1)
# left-bottom
let dx2 = 0.003 * sin(89 * t) + 0.01 * sin(12 * (1 + t / 99000) * t)
let dy2 = 0.004 * sin(88 * t) + 0.0069 * sin(15 * (1 + t / 11000) * t)
let s2 = 1 + 0.03 * sin(10.45 * exp(0.99 * ln(t)) + t)
let col_r2 = [bg_rect.l + 1.02 + dx2 + 0.45 * (1 - s2), bg_rect.t + 0.72 * rh + dy2 + chart_h * (1 - s2), 0.45 * s2, chart_h * s2]
col(id: col_xmas, cid: 12769, r: col_r2, rn: 1, rh: chart_h * s2)
# right-top
let dx3 = 0.003 * sin(91 * t) + 0.01 * sin(12.5 * (1 + t / 109000) * t)
let dy3 = 0.0035 * sin(86 * t) + 0.0082 * sin(11.6 * (1 + t / 9900) * t)
let s3 = 1 + 0.03 * sin(10.2 * exp(1.02 * ln(t)) - 3 * t)
let col_r3 = [bg_rect.l + 1.42 + dx3 + 0.45 * (1 - s3), bg_rect.t + 0.49 * rh + dy3 + chart_h * (1 - s3), 0.45 * s3, chart_h * s3]
col(id: col_xmas, cid: 12770, r: col_r3, rn: 1, rh: chart_h * s3)
let $h = 0
使用进阶
UML 是一种非常简单的语言, 但是通过组合简单的元素, 我们也能实现一些较为复杂的效果.
页面切换
如果我们希望活动界面包括多个页面, 可以通过在超出屏幕的部分放置其他页面的内容, 并通过整个页面所有元素的移动达到类似页面切换的效果.
例如, 我们可以这样放置横纵六个页面的内容:
|-----------|
| 3, 2 |
| |
|-----------|
| 2, 2 |
| |
|-----------|-----------|-----------|-----------|
| 1, 1 | 1, 2 | 1, 3 | 1, 4 |
| | | | |
|-----------|-----------|-----------|-----------|
可以在每个页面的左上角放置一个 o
方便计算每个页面上的元素的位置:
# o
let tmp_rect = [page_offset_x_1 + page_offset_x_2 + page_offset_x_3, page_offset_y_1 + page_offset_y_2, 0.1, 0.1]
img(id: o11, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
let tmp_rect = [o11.l + 2, o11.t, 0.1, 0.1]
img(id: o12, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
let tmp_rect = [o11.l + 4, o11.t, 0.1, 0.1]
img(id: o13, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
let tmp_rect = [o11.l + 6, o11.t, 0.1, 0.1]
img(id: o14, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
#>if(page_offset_y_1_ratio)
let tmp_rect = [o12.l, o11.t - 2 * top, 0.1, 0.1]
img(id: o22, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
#>fi
#>if(page_offset_y_2_ratio)
let tmp_rect = [o12.l, o11.t - 4 * top, 0.1, 0.1]
img(id: o32, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
#>fi
在以上的定义中, 我们通过 page_offset_x_n
和 page_offset_y_n
定义了 o11
的偏移量, 由于其他页面的偏移量都是相对于 o11
的, 所以在 o11
移动时, 其他页面的元素也会跟着移动.
此外, 我们还通过 #>if
设置了 o22
和 o32
绘制的时机. 这是为了防止纵向切换后, 用户在活动首屏看到当前页面上方页面的内容. 通过这个值, 我们可以将页面上方的内容移出屏幕.
为了得出这些偏移量的具体值, 我们需要使用定义一些用于切换页面的按钮, 并通过按钮的 last
属性计算出偏移量的值.
global btn_r_1 = @btn
global btn_l_2 = @btn
global btn_r_2 = @btn
global btn_l_3 = @btn
global btn_r_3 = @btn
global btn_l_4 = @btn
global btn_r_3 = @btn
global btn_l_4 = @btn
global btn_u_1 = @btn
global btn_d_2 = @btn
global btn_u_2 = @btn
global btn_d_3 = @btn
# btn_lr_1234
let tmp_rect = [o12.l - 0.1, o11.t + top - 0.05, 0.1, 0.1]
img(id: btn_r_1_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/right_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_r_1, r: tmp_rect, t:fit)
let tmp_rect = [o12.l, o12.t + top - 0.05, 0.1, 0.1]
img(id: btn_l_2_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/left_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_l_2, r: tmp_rect, t:fit)
let tmp_rect = [o13.l - 0.1, o12.t + top - 0.05, 0.1, 0.1]
img(id: btn_r_2_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/right_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_r_2, r: tmp_rect, t:fit)
let tmp_rect = [o13.l, o13.t + top - 0.05, 0.1, 0.1]
img(id: btn_l_3_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/left_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_l_3, r: tmp_rect, t:fit)
let tmp_rect = [o14.l - 0.1, o13.t + top - 0.05, 0.1, 0.1]
img(id: btn_r_3_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/right_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_r_3, r: tmp_rect, t:fit)
let tmp_rect = [o14.l, o14.t + top - 0.05, 0.1, 0.1]
img(id: btn_l_4_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/left_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_l_4, r: tmp_rect, t:fit)
# btn_ud_123
let tmp_rect = [o12.l + 1 - 0.05, o12.t, 0.1, 0.1]
img(id: btn_u_1_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/up_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_u_1, r: tmp_rect, t:fit)
#>if(page_offset_y_1_ratio)
let tmp_rect = [o22.l + 1 - 0.05, o12.t - 0.1, 0.1, 0.1]
img(id: btn_d_2_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/down_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_d_2, r: tmp_rect, t:fit)
let tmp_rect = [o22.l + 1 - 0.05, o22.t, 0.1, 0.1]
img(id: btn_u_2_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/up_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_u_2, r: tmp_rect, t:fit)
#>fi
#>if(page_offset_y_2_ratio)
let tmp_rect = [o32.l + 1 - 0.05, o22.t - 0.1, 0.1, 0.1]
img(id: btn_d_3_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/down_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_d_3, r: tmp_rect, t:fit)
#>fi
其中按钮 id
最后的编号表示按钮所在的页面.
在定义了按钮后, 我们可以通过按钮的 last
属性计算出偏移量的值:
let animation_duration = 0.7
let animation_speed = 1 / animation_duration
let a = btn_r_1.last
let b = btn_l_2.last
# back and forth
let x = min((t - a) * animation_speed, 1)
let y = max(1 - (t - b) * animation_speed, 0)
# a > b ? x : y
let page_offset_x_1_ratio = (a > b) * x + (a <= b) * y
# easeInOutQuad
let x = 4 * page_offset_x_1_ratio * page_offset_x_1_ratio * page_offset_x_1_ratio
let y = 1 - (-2 * page_offset_x_1_ratio + 2) * (-2 * page_offset_x_1_ratio + 2) * (-2 * page_offset_x_1_ratio + 2) / 2
let page_offset_x_1_ratio_eased = (page_offset_x_1_ratio < 0.5) * x + (page_offset_x_1_ratio >= 0.5) * y
let page_offset_x_1 = page_offset_x_1_ratio_eased * -2
关于纵向切换的偏移量,我们可以通过类似的方法计算出.
这样, 我们就完成了页面切换效果.
完整的 UML 代码如下:
let animation_duration = 0.7
let animation_speed = 1 / animation_duration
global btn_r_1 = @btn
global btn_l_2 = @btn
global btn_r_2 = @btn
global btn_l_3 = @btn
global btn_r_3 = @btn
global btn_l_4 = @btn
global btn_r_3 = @btn
global btn_l_4 = @btn
global btn_u_1 = @btn
global btn_d_2 = @btn
global btn_u_2 = @btn
global btn_d_3 = @btn
# ---
let a = btn_r_1.last
let b = btn_l_2.last
# back and forth
let x = min((t - a) * animation_speed, 1)
let y = max(1 - (t - b) * animation_speed, 0)
# a > b ? x : y
let page_offset_x_1_ratio = (a > b) * x + (a <= b) * y
# easeInOutQuad
let x = 4 * page_offset_x_1_ratio * page_offset_x_1_ratio * page_offset_x_1_ratio
let y = 1 - (-2 * page_offset_x_1_ratio + 2) * (-2 * page_offset_x_1_ratio + 2) * (-2 * page_offset_x_1_ratio + 2) / 2
let page_offset_x_1_ratio_eased = (page_offset_x_1_ratio < 0.5) * x + (page_offset_x_1_ratio >= 0.5) * y
let page_offset_x_1 = page_offset_x_1_ratio_eased * -2
# ---
# ---
let a = btn_r_2.last
let b = btn_l_3.last
let x = min((t - a) * animation_speed, 1)
let y = max(1 - (t - b) * animation_speed, 0)
let page_offset_x_2_ratio = (a > b) * x + (a <= b) * y
let x = 4 * page_offset_x_2_ratio * page_offset_x_2_ratio * page_offset_x_2_ratio
let y = 1 - (-2 * page_offset_x_2_ratio + 2) * (-2 * page_offset_x_2_ratio + 2) * (-2 * page_offset_x_2_ratio + 2) / 2
let page_offset_x_2_ratio_eased = (page_offset_x_2_ratio < 0.5) * x + (page_offset_x_2_ratio >= 0.5) * y
let page_offset_x_2 = page_offset_x_2_ratio_eased * -2
# ---
# ---
let a = btn_r_3.last
let b = btn_l_4.last
let x = min((t - a) * animation_speed, 1)
let y = max(1 - (t - b) * animation_speed, 0)
let page_offset_x_3_ratio = (a > b) * x + (a <= b) * y
let x = 4 * page_offset_x_3_ratio * page_offset_x_3_ratio * page_offset_x_3_ratio
let y = 1 - (-2 * page_offset_x_3_ratio + 2) * (-2 * page_offset_x_3_ratio + 2) * (-2 * page_offset_x_3_ratio + 2) / 2
let page_offset_x_3_ratio_eased = (page_offset_x_3_ratio < 0.5) * x + (page_offset_x_3_ratio >= 0.5) * y
let page_offset_x_3 = page_offset_x_3_ratio_eased * -2
# ---
# ---
let a = btn_u_1.last
let b = btn_d_2.last
let x = min((t - a) * animation_speed, 1)
let y = max(1 - (t - b) * animation_speed, 0)
let page_offset_y_1_ratio = (a > b) * x + (a <= b) * y
let x = 4 * page_offset_y_1_ratio * page_offset_y_1_ratio * page_offset_y_1_ratio
let y = 1 - (-2 * page_offset_y_1_ratio + 2) * (-2 * page_offset_y_1_ratio + 2) * (-2 * page_offset_y_1_ratio + 2) / 2
let page_offset_y_1_ratio_eased = (page_offset_y_1_ratio < 0.5) * x + (page_offset_y_1_ratio >= 0.5) * y
let page_offset_y_1 = page_offset_y_1_ratio_eased * 2 * top
# ---
# ---
let a = btn_u_2.last
let b = btn_d_3.last
let x = min((t - a) * animation_speed, 1)
let y = max(1 - (t - b) * animation_speed, 0)
let page_offset_y_2_ratio = (a > b) * x + (a <= b) * y
let x = 4 * page_offset_y_2_ratio * page_offset_y_2_ratio * page_offset_y_2_ratio
let y = 1 - (-2 * page_offset_y_2_ratio + 2) * (-2 * page_offset_y_2_ratio + 2) * (-2 * page_offset_y_2_ratio + 2) / 2
let page_offset_y_2_ratio_eased = (page_offset_y_2_ratio < 0.5) * x + (page_offset_y_2_ratio >= 0.5) * y
let page_offset_y_2 = page_offset_y_2_ratio_eased * 2 * top
# ---
# o
let tmp_rect = [page_offset_x_1 + page_offset_x_2 + page_offset_x_3, page_offset_y_1 + page_offset_y_2, 0.1, 0.1]
img(id: o11, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
let tmp_rect = [o11.l + 2, o11.t, 0.1, 0.1]
img(id: o12, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
let tmp_rect = [o11.l + 4, o11.t, 0.1, 0.1]
img(id: o13, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
let tmp_rect = [o11.l + 6, o11.t, 0.1, 0.1]
img(id: o14, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
#>if(page_offset_y_1_ratio)
let tmp_rect = [o12.l, o11.t - 2 * top, 0.1, 0.1]
img(id: o22, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
#>fi
#>if(page_offset_y_2_ratio)
let tmp_rect = [o12.l, o11.t - 4 * top, 0.1, 0.1]
img(id: o32, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/blank.png", r: tmp_rect, t:fit)
#>fi
# btn_lr_1234
let tmp_rect = [o12.l - 0.1, o11.t + top - 0.05, 0.1, 0.1]
img(id: btn_r_1_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/right_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_r_1, r: tmp_rect, t:fit)
let tmp_rect = [o12.l, o12.t + top - 0.05, 0.1, 0.1]
img(id: btn_l_2_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/left_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_l_2, r: tmp_rect, t:fit)
let tmp_rect = [o13.l - 0.1, o12.t + top - 0.05, 0.1, 0.1]
img(id: btn_r_2_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/right_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_r_2, r: tmp_rect, t:fit)
let tmp_rect = [o13.l, o13.t + top - 0.05, 0.1, 0.1]
img(id: btn_l_3_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/left_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_l_3, r: tmp_rect, t:fit)
let tmp_rect = [o14.l - 0.1, o13.t + top - 0.05, 0.1, 0.1]
img(id: btn_r_3_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/right_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_r_3, r: tmp_rect, t:fit)
let tmp_rect = [o14.l, o14.t + top - 0.05, 0.1, 0.1]
img(id: btn_l_4_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/left_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_l_4, r: tmp_rect, t:fit)
# btn_ud_123
let tmp_rect = [o12.l + 1 - 0.05, o12.t, 0.1, 0.1]
img(id: btn_u_1_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/up_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_u_1, r: tmp_rect, t:fit)
#>if(page_offset_y_1_ratio)
let tmp_rect = [o22.l + 1 - 0.05, o12.t - 0.1, 0.1, 0.1]
img(id: btn_d_2_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/down_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_d_2, r: tmp_rect, t:fit)
let tmp_rect = [o22.l + 1 - 0.05, o22.t, 0.1, 0.1]
img(id: btn_u_2_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/up_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_u_2, r: tmp_rect, t:fit)
#>fi
#>if(page_offset_y_2_ratio)
let tmp_rect = [o32.l + 1 - 0.05, o22.t - 0.1, 0.1, 0.1]
img(id: btn_d_3_bkg, url: "https://teamflos.github.io/phira-docs/assets/uml/advanced/page_switch/down_arrow.png", r: tmp_rect, t:fit)
btn(id: btn_d_3, r: tmp_rect, t:fit)
#>fi
let $h = 2 * top - 0.02
构建指南
什么,移动设备?,我不会,长大后再学习
Windows:here
Linux:here
Cargo 安装
Windows
-
点击 这里 下载构建工具安装程序。
-
双击打开
rustup-init.exe
后出现安装窗口;输入2
然后输入y
然后再次输入2
,然后输入x86_64-pc-windows-gnu
,最后一路回车开始安装,直到输出Rust is installed now. Great!
- 注意:千万不要直接回车安装 MSVC,这在后续构建将会出现大量问题!
-
前往 MSYS2 官网下载 MSYS2 安装程序,下载完成后双击打开,如无特殊需求,一路下一步即可,直到提示
Finished the MSYS2 Setup
,点击右下角的按钮后将弹出一个窗口,输入以下指令,安装过程一路回车即可。pacman -Sy && pacman -Syu
pacman -S mingw-w64-x86_64-toolchain
- 如果您无法连接到 github,您也可以去 缓存站 下载 MSYS2, 注意,缓存站只能保证您可以下载,不能高速下载,也不一定是最新版。
-
打开命令提示符(cmd)或 PowerShell,输入
cargo -V
检查是否成功安装,若返回版本号则安装成功,若出现其他提示,请见 Windows 常见问题。 -
按下图所示,修改环境变量
-
打开命令提示符(cmd)或 PowerShell,输入
gcc -v
检查是否成功安装,若返回版本号则安装成功,若出现其他提示,请见 Windows 常见问题。
Windows 常见问题
Q. 双击运行下载成功后的构建工具闪退
A. 请不要更改文件名
Linux
Debian 分支 Linux 系统
- 打开终端,输入以下命令:
sudo apt update
sudo apt install cargo -y
- 若无报错,输入
cargo -V
检查是否输出版本号,若出现其他输出,请见 Linux 常见问题
其他系统待补充
Linux 常见问题
Q. 输入 cargo
时输出 bash: /usr/bin/cargo: No such file or directory
或 cargo: command not found
A. 未成功安装 cargo,请检查安装完成后是否输出了其他信息。
Windows
准备阶段
- 确保系统安装了 cargo,可以在命令提示符(cmd)或者 PowerShell 使用
cargo -V
检查系统是否安装了 cargo。如果提示以下信息:'cargo' 不是内部或外部命令,也不是可运行的程序或批处理文件。
cargo : 无法将“cargo”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
- 请点击 这里 按步骤安装构建工具
- 从 GitHub 下载源码到本地:
- 若您安装了 git 工具,请使用
git clone https://github.com/TeamFlos/phira.git
将仓库克隆到本地。 - 若您没有安装 git 工具,您也可以在 Phira 仓库页面点击 Code 按钮选择
Download ZIP
将代码下载到本地,随后将代码解压到本地任意目录。 - 如果您无法连接到 GitHub,您也可以使用 git 加速网站提供的加速地址克隆与下载。
- 若您要构建指定版本的 Phira,请前往 release 页面在 Assets 中选择下载
Source code(zip)
到本地,解压到任意路径即可。 - 警告:为了防止玄学问题,我们不建议路径中包含除了 ASCII 编码包含字符以外的任何字符。
- 若您安装了 git 工具,请使用
- perl,您可以在命令提示符(cmd)或者 PowerShell 使用
perl -v
检查系统是否安装了 perl,如果没有,请搜索并打开MSYS2 UCRT64
输入pacman -S perl
安装 perl - 静态库文件,您可以 直接下载 或者在 缓存站 下载静态库文件,下载完成后直接解压到代码根目录下,如果提示覆盖文件,请点击覆盖。
开始构建
- 在命令提示符(cmd)或者 PowerShell 切换到代码根目录(如
D:\phira\
) - 在命令提示符(cmd)或者 PowerShell 使用
cargo build -r --bin phira-main
,如果不出意外,在openssl-sys(build)
时,您将卡很久很久,请不要退出,这是正常的。 - 构建完成后,在
.\target\release\
目录下您可以找到编译完成的主程序 - 复制
.\assets\
目录中的所有文件到.\target\release\assets\
,至此,构建流程全部完成,您可以直接运行phira-main.exe
检查资源文件是否完整。
- 注意:在此文档编写时,代码目录下的资源文件并不完整,如果您发现主程序闪退,您可以前往 release 页面下载任意版本,获取资源文件
32位版本
- 在命令提示符(cmd)或者 PowerShell 切换到代码根目录(如
D:\phira\
) - 下载上面的静态库文件解压到
phira\prpr-avc\static-lib
或自行构建 - 在命令提示符(cmd)或者 PowerShell 使用
cargo build --target=i686-pc-windows-gnu --release --package phira-main
,如果不出意外,在openssl-sys(build)
时,您将卡很久很久,请不要退出,这是正常的。 - 构建完成后,在
.\target\release\
目录下您可以找到编译完成的主程序 - 复制
.\assets\
目录中的所有文件到.\target\release\assets\
,至此,构建流程全部完成,您可以直接运行phira-main.exe
检查资源文件是否完整。
有关静态库的构建(以i686-pc-windows-gnu为例)
在sh上操作(此处使用msys2)
git clone https://git.ffmpeg.org/ffmpeg.git --depth=1
cd ffmpeg && mkdir build && cd build
../configure --disable-programs --disable-doc --disable-everything --disable-debug --arch=i686 --target_os=mingw32 --cross-prefix=i686-w64-mingw32-
make
note:这里有个坑。。。如果报错的话尝试把 msys64\mingw32\bin 这个目录下的 i686-w64-mingw32-gcc-ar.exe , i686-w64-mingw32-gcc-nm.exe , i686-w64-mingw32-gcc-ranlib.exe 复制粘贴一份然后重命名成 i686-w64-mingw32-ar.exe , i686-w64-mingw32-nm.exe , i686-w64-mingw32-ranlib.exe
接着把build文件夹下的所有形如 *.a
的文件复制到 phira\prpr-avc\static-lib\i686-pc-windows-gnu
就可以啦
常见问题
Q. 报错 failed to send request: 操作超时
A. 请检查网络环境,确保您可以正常访问 GitHub
Q. 报错 failed to send request: 无法解析服务器的名称或地址
A. 检查 DNS 或更换 DNS,更换后请刷新 DNS 缓存
Q. 构建过程中报错 error: failed to run custom build command for openssl-sys v0.9.99
A. 缺失 perl,请检查是否正确安装 perl 后再试
Q. 构建报错error occurred: Failed to find tool. Is gcc.exe installed? (see https://github.com/rust-lang/cc-rs#compile-time-requirements for help)
A. 请检查是否安装了 MSYS2
以及检查是否配置了环境变量
Q. 出现以下报错:
Error building OpenSSL dependencies:
Command: "make" "depend"
Failed to execute: program not found
A. 缺失 make
指令,请前往 MSYS2 终端中使用 pacman -S make
安装此命令。
Q. 报错包含 This perl inplementation doesn't produce lnix like paths
A. 使用的 perl
不适用于 gcc
,请删除原有 perl
的环境变量或者直接卸载原有的 perl
。
Q. 报错包含undefined reference to libiconv
A. 使用的 libiconv
有问题,请在 MSYS2 终端中使用 pacman -S libiconv
Q. 太麻烦了
A. 这样,直接去 release 页面下吧微软我真谢谢你
Linux
准备阶段
-
确保系统安装了 cargo,可以在终端使用
cargo -V
检查系统是否安装了 cargo,如果没有安装,请点击 这里按步骤安装构建工具 -
从 GitHub 下载源码到本地:
- 若您是纯终端,建议使用 git 工具,请使用
git clone https://github.com/TeamFlos/phira.git
将仓库克隆到本地。 - 若您使用了桌面环境,您也可以在 Phira 仓库页面点击 Code 按钮选择
Download ZIP
将代码下载到本地,随后将代码解压到本地任意目录。 - 如果您无法连接到 GitHub,您也可以使用 git 加速网站提供的加速地址克隆与下载。
- 若您要构建指定版本的 Phira,请前往 release 页面在 Assets 中选择下载
Source code(tar.gz)
到本地,解压到任意路径即可。 - 警告:为了防止玄学问题,我们不建议路径中包含除了 ASCII 编码包含字符以外的任何字符。
- 若您是纯终端,建议使用 git 工具,请使用
-
静态库文件,您可以 直接下载 或者在 缓存站 下载静态库文件,下载完成后直接解压到代码根目录下,如果提示覆盖文件,请点击覆盖。
-
构建过程需要库文件补充,输入以下命令即可:
sudo apt update
sudo apt install libasound2-dev
sudo apt install libgtk-3-dev
开始构建
- 打开终端,切换到代码根目录
- 输入
cargo build -r --bin phira-main
,直到编译完成。 - 复制
.\assets\
目录中的所有文件到.\target\release\assets\
,至此,构建流程全部完成,您可以在带有桌面环境的情况下直接运行phira-main
检查资源文件是否完整,若您没有桌面环境,程序将会闪退(实测 WSL 无法兼容,如果在 WSL 下运行将会闪退),至此,构建流程结束。
- 注意:在此文档编写时,代码目录下的资源文件并不完整,如果您发现主程序闪退,您可以前往 release 页面下载任意版本,获取资源文件
常见问题
Q. 构建输出 failed to connect to GitHub
A. 请检查网络环境。
Q. 构建时报错 The pkg-config command could not be found
A. 缺失 pkg-config
指令,使用 sudo apt install pkg-config -y
安装即可。
Q. 构建报错 failed to run custom build command for alsa-sys v0.3.1
A. 缺失库文件
开发/运营过程中出现的意外事件
其实全都是 sjfhsjfh 的错
长风的柳絮
此事件发生于 2023-07-10 18:53 (此为 sjfhsjfh 在内部群发送消息 "我来磕大头了" 的时间)
事件经过
sjfhsjfh 在手动操作数据库(使用 SQL 语句)封禁用户 "长风的柳絮"时, 一不小心将未写完的 UPDATE 语句发送出去(没有加上 WHERE
关键字, 也没有写好 SET banned = TRUE
), 导致所有用户的用户名被修改为 "长风的柳絮"
此事件耗费 Mivik 一整个下午和晚自习进行恢复(从日志中), 并未造成太多数据损失
后记
凡事都要两面看, 此事过后 Mivik 给审核组长提供了一个方便快捷的封禁按钮, 以防止类似事件再次发生. 不幸的是, sjfhsjfh 的扫帚星属性并未因此事件而消失, 见 v0.6.0 更新消息
v0.6.0 更新消息
此事件发生于 2024-01-01 23:16 (此为 Mivik 在上架讨论群内发送消息 "阿我草你干了啥?" 的时间)
同 长风的柳絮 事件类似, 为手动操作数据库造成的严重后果. 此事件也被称为 "长风的柳絮 2"
事件经过
sjfhsjfh 企图修改误放出的错误公告, 且心急如焚, 故不当操作导致所有用户的所有消息内容被修改为新的公告内容(即 v0.6.0 更新公告)
此事件耗费 Mivik 约一个晚上进行恢复, 当天的消息全部丢失
6thPecJam
此事件发生于 2024-02-10 凌晨约 00:40, 相较于预计的 PecJam 提交通道开启时间 2024-02-10 00:00, 已经推迟超多半个小时
注: 2024-02-09 为除夕, 大家都在守岁, 正好等着 PecJam 交稿
事件经过
sjfhsjfh 眼看大家在群里询问为何 PecJam 提交通道还未开启, 在焦急之中对 Phira 后端代码进行了胡乱修改, 提交时甚至没有进行编译检查, 最后不得不请 Mivik 从温暖的被窝中出来帮忙修复, 实在是罪大恶极
天空之城
此事件发生于 2024-04-13 19:00 (此为 大鸽子咕咕咕 在上架讨论群内发送“谁想要业绩可以看看这个,四票打回了还是这个状态,不知道是不是bug”的时间)
事件经过
评议员在打回谱面时若网络情况较差就会出现已拒绝但没有打回的现象,由此吸引了14位评议员先后参与了天空之城的打回操作
以下为参与了打回的评议员列表:
无人通过
14 人拒绝:超爱吃盐的盐盐,twotthrees233,右果ary,WinterPT,scylic,远空星灵,SSuent_,-zerouth-,StR-1,sjfhsjfh,Max3957,Kedmue,Dr.Asriel,Naptie
......为什么 sjfhsjfh 也在里面
最终因 Naptie 使用了 "润润润牌私有连接" 而成功触发打回操作,结束本谱战斗。 但战斗真的会结束吗?谁知道呢(
关于评议员的详细留言请看#15169 评议记录
Forever Young
此事件发生于 2024-05-25 凌晨 04:10 (此为 sjfhsjfh 在内部群发送消息 "吗的我又干蠢事了 [动画表情] 想原地紫砂" 的时间)
事件经过
sjfhsjfh 想干点事于是请求 Mivik 把动态分发的字体 bold.ttf
的步骤传授给我, 我来更新一下字符几来修复部分语言缺字的情况, 进展还算顺利, 可是到测试的时候, 根据反馈选择了法语随便点进一张 Stable 谱面(受害者出场: #16576 Forever Young), 再看到出问题的地方是删除确认提示框的时候脑袋短路了两秒, 觉得不太对但没停住手, 知道应该下载到本地删除本地文件, 但是下载了之后习惯性一翻到底点击删除在线. 这时候 sjfh 对着法语的 "Annuler" 和 "Confirmer" 两个按钮大脑开始飞速运转, 很明显能猜测到后者是确认, 但此时已经sjfh理智归零(考虑到此刻已经是明日方舟服务器每日计时更替之后了), 停留了没超过 0.5s 就点了确认, 于是就有了上面的聊天消息.
警钟敲烂