前言
这是一个阅读Project-Shoko的result
部分代码来理解如何编写皮肤和Beatoraja
的皮肤系统的流水账,内容可能未整理,随时间修改的可能性较大。
初始化
首先一个皮肤肯定要定义自己,然后被读取,解析,执行成一个java object。根据wiki的指示可以知道一个皮肤文件从一个.luaskin
文件开始,它会作为一个lua文件被执行,其返回值就是这个皮肤的定义:
1 | -- result/result.luaskin |
显然skin_config
是由beatoraja通过某种方式传下来的信息,目前不清楚这个分支有什么作用。让我们把这里的事情展开一下:
- 首先
Project-Shoko
有多个文件夹,分别对应不同的beatoraja的场景,例如select
,result
等 - 这样的场景文件夹下有一个
.luaskin
文件定义了这个场景的皮肤文件 - 执行这个
.luaskin
文件的返回值会被转换成java中的一个object实例。
wiki给的例子(不完整)是:
1 | -- example.luaskin |
他在java会被转换成一个bms.player.beatoraja.skin.SkinHeader
的实例,不难发现字段都是对应的。
不难猜到beatoarja里对应读取lua皮肤的文件是bms.player.beatoraja.skin.lua.LuaSkinLoader
,打开就能发现bms.player.beatoraja.skin.lua.LuaSkinLoader#load
这个方法:
1 | public Skin load(Path p, SkinType type, SkinConfig.Property property) { |
这个方法的行为应该已经非常明确了:
- 首先我们不用关心这个方法的调用路径,那不重要
- 参数中p很明确是文件路径,type是对应的场景(play, select, result等),而property可以根据
header.setSkinConfigProperty(property)
猜出来这是已配置的信息,注入到读取到的皮肤里 - 返回值是一个
Skin
实例,很明显就是一个皮肤的完整定义
读取到header后,beatoraja会继续:
读取这个皮肤的自定义的文件列表,这个事情是定义在
header.filepath
里的:local filepath = { {name = "Default Stagefile", path = "customize/stagefile/*.png"}, {name = "Result Background (CLEAR AAA)", path = "customize/bg/aaa/*.png"}, {name = "Result Background (CLEAR AA)", path = "customize/bg/aa/*.png"}, {name = "Result Background (CLEAR A)", path = "customize/bg/a/*.png"}, {name = "Result Background (CLEAR)", path = "customize/bg/clear/*.png"}, {name = "Result Background (FAILED)", path = "customize/bg/failed/*.png"}, } local header = { -- 省略 filepath = filepath }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- 这个步骤只需要提取其中的`name/path`作为k/v即可,不需要在意
- `lua.exportSkinProperty`这个方法最终将一个`table`导出到了一个全局变量`skin_config`里,通过检查其内容可以发现这是配置的信息:
- 
接着beatoraja会再解析执行一次这个lua文件,并将返回值处理成一个`Skin`实例。
那么现在就可以理解为什么`.luaskin`会写成这个样子了,这和读取一个纯文本的手法相同:
- 首先读取这个文件的前若干个字节,检查是否是某个特定值,例如java的class文件的前几个字节是`0xCAFFBABE`,通过这个方式可以知道这个文件确实是一个java的class文件。对应到皮肤这里就是先解释执行第一次,获得这个皮肤的头信息,即先要知道这个皮肤的“轮廓”,大概长什么样,有哪些文件要引入,有哪些配置
- 然后执行第二次读取,将配置和内容混合在一起得到完整的皮肤信息,并转换到java的实例里
> 这样的设计是一个很常见的方式,这里再举一个例子来说明这样做的原因:
>
> Project-Shoko的result有一个配置项`Play Side`,可取值是`1P/2P`。但是beatoraja肯定不能提前假定皮肤有哪些可配置的信息,这样的话自由度太低了,而且随着beatoraja的更新很多皮肤都要联动修改,所以皮肤的配置项都是由皮肤自己定义的。相对的,beatoraja也就不知道皮肤有哪些可能的配置了,因此它需要先问一下皮肤有哪些配置,这就是第一次读取获得的header信息。第二次就是把用户的配置再传给皮肤,这个例子里就是告知皮肤用户`Play Side`这个配置的值是`1P`。
>
> 整个手法的好处就是把设计配置项的权利交给了皮肤这边。
现在我们大概了解了皮肤的初始化步骤,这里做个总结:
- 首先皮肤这边需要定义一个`.luaskin`文件,这个文件会被beatoraja当作`lua`文件解释执行
- 这个文件的返回值根据`skin_config`这个全局变量是否存在来区分是第一次预读取头信息还是第二次读取完整皮肤信息
- 第一次读取时,返回的是`t.header`,内容很简单,就是一个皮肤的基本信息
- 第二次读取时,beatoraja会将用户的配置,文件路径等信息导出到全局变量`skin_config`中,此时返回的就是完整的皮肤信息
## 配置
那么,皮肤的自定义配置又是怎么定义的呢。首先不难想到自定义配置信息应该是在`header`里的,因为beatoraja配置皮肤的信息是在启动器里做的,此时肯定不需要加载完整的皮肤。不难发现代码里写的很明确:
```lua
-- result/resultmain.lua
local property = require("lua.mainproperty")
local header = {
property = property.property,
}
-- result/lua/mainproperty.lua
local module = {}
module.property = {
{name = "Play Side", item = {
{name = "1P", op = 900},
{name = "2P", op = 901}
}},
{name = "Stagefile", item = {
{name = "ON", op = 910},
{name = "OFF", op = 911}
}},
{name = "Judge Detail", item = {
{name = "Extra Fast/Slow Info", op = 920},
--{name = "Pie", op = 921} --WIP
}},
{name = "Network Info", item = {
{name = "On", op = 922},
{name = "Off", op = 923} --WIP
}},
}
return module
每个配置项是一个table,其中每个配置项有一个自己的名字name和可取值item,其中item又是一个table,内部就是一个简单的k/v定义。这里唯独比较奇怪的是op
,不过不难发现它就是保存到beatoraja的存档文件里的数值,只要互不相同就可以了。
通过在resultmain.lua
中寻找可以发现:
1 | -- result/resultmain.lua |
配置项就是通过beatoraja导出的全局变量skin_config
来获取的,然后具体皮肤可以根据配置项的取值来设置不同的值来控制不同的渲染结果。
至于配置项是如何展示到beatoraja里的,这里就先略过了,并不太重要。
完整的皮肤定义
现在可以进入到重点了,一个皮肤在第二次读取时会返回给beatoraja这个皮肤的完整定义
1 | local function main() |
目前我们只知道这个方法会返回一个完整的skin对象,beatoraja会将其转换为一个bms.player.beatoraja.skin.json.JsonSkin.Skin
对象,那么皮肤这边的定义就需要对应beatoraja的Skin类进行编写了,先简单看一下Skin类的轮廓:
内容有点多,不过首先需要明确的一点是整个JsonSkin
只是一个存储类定义的地方,他没有自己的字段。原始的lua文件的执行结果会被转换为一个JsonSkin.Skin
类的实例,然后被当作一个json定义皮肤读取到beatoraja中,后面这个步骤属于beatoraja内部的处理,暂时不深入。
通过阅读lua这边的代码可以发现大部分内容都是在定义skin.destination
:
1 | skin.destination = { |
可以猜测destination是指最终在beatoraja中渲染的组件。最开始的部分注释指出了是背景图,这部分代码也比较好理解,id是一个唯一标识,op是渲染这个dst的条件,dst是这个组件的坐标和长宽。显然对于背景图来说坐标是(0,0),长宽为整个窗口。
这里补充一下op的取值,其中
90, 91, 300, 301, 302
这几个值都不是定义在皮肤里的,而是定义在beatoraja里的,具体定义在bms.player.beatoraja.skin.SkinProperty
,而这里的设计也比较随意,见bms.player.beatoraja.skin.SkinObject#setDrawCondition(int[])
和https://github.com/Catizard/lr2oraja-endlessdream/blob/112c5ce0ce7fd3d7942afef572486b40d142b35f/core/src/bms/player/beatoraja/skin/Skin.java#L193
,如果op里的取值是定义在SkinProperty
里的话,那么按beatoraja的定义来,否则就是皮肤自定义的配置项,按皮肤的配置来。
到这里实际上大部分内容都可以阅读了,这里给出一张截图来对比代码
先从最下面的静态文字开始,它的定义很简单:
1 | if skin_config.option["Play Side"] == 900 then |
文字是不能直接定义坐标的,它也不是直接渲染出来的,例如id=offline
的text实际上还可以找到一个对应的destination:
1 | -- destination |
于是可以猜想到align的定义是相对于坐标的偏移,取值0/1/2分别代表了左中右。