文档部分内容可能写的不是很详细,也有可能漏掉了一部分,如果有搞不懂的地方欢迎在评论区提出,需要的话我会进行补充。

介绍

  现代工业中包含了很多的公用API,这篇文章就详细介绍一下其中的自动化GUI绘制API——graphics包的用法及工作原理。

  这套系统的主要目的是免去手动绘制GUI材质以及手动定位控件坐标的流程,使用代码在运行期计算控件的位置。

  系统的设计过程中参考了html + css + js,最终效果是通过一个pug文件、一个styl文件即可构建出一个基本的GUI图像,然后再使用代码加以支持,即可实现完整的功能。

结构文件

  我们使用一个pug文件来告诉代码一个GUI中包含哪些元素,这个文件应当放置在assets/[modid]/mi_files/gui/pugs内。

  pugs文件夹内分为两个文件夹:commonclient,前者用于放置双端GUI,后者用来放置客户端GUI(客户端GUI就是无需服务端支持即可实现的GUI,比如合成表查看器)。

  文件名将会被当作GUI的key(不用写modid),尽量让key由英文小写字母、下划线(或减号)及数字组成。

  声明元素的语法为:

tag#id.className1.className2(attributes)

  其中idclassNameattributes均可省略不写,但是如果该控件需要进行网络通信,则必须为其设定一个唯一的id

  attribute的格式为name=valuevalue可以使用双引号包围也可以不使用双引号包围,多个attribute用英文逗号分隔。

  attributes中我们允许填写布尔类型的值,大部分情况我们将属性存在记为true,不存在记为false

  然后通过缩进来控制各个元素的包含关系,标准缩进为4个空格,当然为了照顾一些功能不太好的编辑器,也可以使用制表符\t来表达缩进,一个制表符解析为4个空格

  下面我们给出一个例子:

1
2
3
4
5
6
7
8
9
10
11
mask
background
title(value="i18n:tile.mi.fluid_pump.name")
div#show
p
progress#fluid
p
progress#energy
p
progress#consume
button#switch

注意事项

  • tagclassName均仅允许包含下列字符:英文字母、-
  • attribute中不可包含英文逗号,如果确实需要包含,可以通过I18n或直接使用代码为其添加attribute

样式文件

  我们使用styl文件来描述GUI样式,该文件应当放置在assets/[modid]/mi_files/gui/styles/文件夹内,且以key.styl命名(key不需要写modid)。

  styl文件虽然是在尽量地模仿stylus,但是并没有支持stylus的所有语法,我们仅支持如下语法:

1
2
元素选择器
key value

  keyvalue仅允许通过空格分隔,不能使用冒号等其它符号,且行尾不能携带分号(除非是value本身有一个分号)。

  与pug不同,styl使用2个空格表达缩进,一个制表符等价于4个空格。(当然你每一级都用4个空格也可以正常解析)

  元素选择器也是支持了一部分:

  • 当我们通过tag选择元素时直接键入tagName即可
  • 当我们通过id选择元素时则需要在id之前添加一个井号(#)
  • 当我们通过className选择元素时需要在其前方添加一个英文句号(.)
    如果需要选择同时包含多个className的元素,可以通过.className1.className2的方式连接
  • 可以使用*选定所有元素
  • 如果需要选择被指定元素直接或间接包含的元素,使用空格分隔两个元素的选择器即可
  • 可以使用英文逗号让两个选择器并列,并列的选择器可以分行书写,但所有选择器必须持有相同的缩进且未中断的行以英文逗号结尾

  例如:

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
background
width 180
height 120
padding-top 8

button
display inline
width 20
height 25
margin-left 3

#show
display inline
margin-bottom 9
align-horizontal left

progress
progress-style rect
width 65%
height 6
margin 1 2

p
margin-top 2
margin-left 2

元素空间

  我们将一个控件的空间从外到内分为以下三部分:marginpaddingcontent

空间示意图

  其中:

  • 所有空间加起来的宽度(或高度)我们称之为spaceWidth(或spaceHeight
  • 除去margin的空间的宽度(或高度)我们称之为width(或height
  • content的宽度(或高度)我们称之为contentWidth(或contentHeight

  为了行文方便,下文不再写出heightheightwidth是完全一致的。

  设置width时有三大类,分别是:父级依赖型、子级依赖型、非依赖型。

  顾名思义,父级依赖要求在计算其尺寸时父级元素的尺寸已经确定,当我们尝试计算一个父级依赖型控件的尺寸时会从该控件的直接父级控件开始向上搜索,通过第一个非子级依赖型的控件来计算当前控件的尺寸。

  子级依赖型和非依赖型均可直接计算得出控件尺寸,不需要链式搜索。

  我们可以通过width来定义一个控件的width,我们支持以下语法:

格式作用是否为默认值依赖类型
auto根据子级控件的width计算子级依赖
number将控件的width设置为一个定值非依赖
number%按百分比计算将控件的width设置为父级元素的width父级依赖
calc(percent% +/- number)按百分比计算 + 加(或减)运算将控件的width设置为父级元素的width父级依赖
inherit直接复制父级控件的width父级依赖

注:`number`和`percent`表示任意整正整数

  我们可以通过以下方式设置margin(仅允许使用固定值作为value):

作用
margintop right bottom right同时设置四个方向上的margin
margintopAndBottom leftAndRight同时设置四个方向上的margin
margin-topnumber设置上方的margin
margin-rightnumber设置右侧的margin
margin-bottomnumber设置下方的margin
margin-leftnumber设置左侧的margin

  padding的设置与margin相同,将margin改为padding即可。

框的类型

  我们可以使用display来修改框的类型,目前我们支持三种类型:

作用缺省
def一个元素独占一行
inline内联元素,该元素会与相邻的inline元素公用一行
none不显示(同时不参与排版)

定位与排版

  我们可以使用positon来修改元素的定位方式,目前我们支持三种定位方式:

作用缺省
relative相对定位,修改元素坐标时会在元素当前位置的基础上进行偏移
absolute绝对定位,根据父元素的位置进行定位,不参与排版
fixed浮动定位,根据GUI进行定位,不参与排版

  我们可以使用align系列语句修改一个元素内子元素的排版方式,语法格式如下:

作用
alignvertical horzontal同时设置两个方向上的对齐方式
align-verticalvertical设置垂直方向上的对齐方式
align-horizontalhorizontal设置水平方向上的对齐方式

  vertical可为:topmiddlebottom,分别表示向上对齐、居中、向下对齐。

  horizontal可为:leftmiddleright,分别表示向左对齐、居中、向右对齐。

  我们还可以通过以下语句修改元素的坐标,具体效果会因为定位方式的不同而有区别:

作用
topnumber根据元素上边界进行偏移
rightnumber根据元素右边界进行偏移
bottomnumber根据元素下边界进行偏移
leftnumber根据元素左边界进行偏移

  当topbottom被同时设置时,会忽略bottom的值,leftright被同时设置时,会忽略right的值。

颜色的表示

  当前支持以10进制或16进制的方式来描述颜色:

格式描述示例
#RGB16进制表示颜色#FFF等价于#FFFFFF
#RGBA16进制表示颜色#0000等价于#00000000
#RrGgBb16进制表示颜色#A1FFBC
#RrGgBbAa16进制表示颜色#D2AAC06A
rgb(r g b)10进制表示颜色,数字用空白符或英文逗号分隔rgb(0 0 0)等价于rgb(0, 0, 0)
rgb(r g b a)10进制表示颜色,数字用空白符或英文逗号分隔,a可以为[0, 1]之间的小数,.5等价于0.5rgba(0 0 0 60)等价于rgba(0, 0, 0, 60)

颜色相关设置

作用
color颜色表达式设置前景色
background-color颜色表达式设置背景色
font-color颜色表达式设置文本颜色

元素描边

  元素的描边会被绘制到元素内部,不会增加元素尺寸。元素的描边有两个可修改的参数:颜色和厚度。

描述
bordertopAndBottom leftAndRight同时设置四个方向的描边颜色
bordertop right bottom left同时设置四个方向的描边颜色
border-weighttopAndBottom leftAndRight同时设置四个方向的描边厚度
border-weighttop right bottom left同时设置四个方向的描边厚度
border-topcolor weight设置上方描边的颜色和厚度
border-rightcolor weight设置右侧描边的颜色和厚度
border-bottomcolor weight设置下方描边的颜色和厚度
border-leftcolor weight设置左侧描边的颜色和厚度
border-topcolor设置上方描边的颜色
border-rightcolor设置右侧描边的颜色
border-bottomcolor设置下方描边的颜色
border-leftcolor设置左侧描边的颜色
border-top-weightweight设置上方描边的厚度
border-right-weightweight设置右侧描边的厚度
border-bottom-weightweight设置下方描边的厚度
border-left-weightweight设置左侧描边的厚度

按钮专有样式

描述缺省值
button-stylerect(矩形)、triangle(三角形)设置按钮样式rect
button-directiondirectiontop/uprightbottom/downleft设置按钮朝向right

  当将按钮设置为rectdirection会被忽略。

进度条专有样式

描述缺省值
progress-stylearrow(箭头)、rect(矩形)进度条样式arrow
progress-directiontop/uprightbottom/downleft进度条方向right
progress-text[boolean]是否显示文本false
progress-text-locationtopmiddlebottom进度条文本位置middle
progress-min-height[number]进度条最窄的地方的高度3
progress-min-width[number]进度条最窄的地方的宽度3

  进度条中的两个min值一般在绘制箭头形进度条时使用,用该值来控制箭头的矩形区域的尺寸。当进度条样式为矩形时这两个值用来控制长宽的最小值。

  当进度条长宽设置的不合理时,绘制可能会出现奇奇怪怪的效果。

注意事项

  • 如果元素选择器用到了id,那么为了性能考虑,该选择器仅使用id即可,因为id是唯一的
  • 尽量减少styl文件中的层级,以此提高性能
  • 代码解析styl文件时没有进行严格的语法判断,所以有时候写错了也能正常解析,但不一定能得出正确的效果
  • 每次加载styl文件后都会缓存解析结果,如果需要不关闭游戏重新解析可以使用/clearMiGraphics指令(切换资源包自动清除缓存)
  • styl文件使用按需加载的方式,只有打开GUI时才会触发样式表加载,可以调用GuiStyleParser.load手动触发指定样式表的加载

控件(标签)列表

  当一个控件需要进行网络交互时,我们必须为其分配一个id。这个网络交互操作可能是由控件本身发出的,也有可能是注册在控件之中的事件发出的。

半透明蒙版

  我们使用mask标签表示文本,对于该控件我们可以不指定控件的宽高,默认会以游戏窗体大小为控件宽高。

  默认的蒙版颜色为rgba(0 0 0 120),可以通过修改样式表中的background-color属性来修改蒙版颜色。

GUI背景

  我们使用background标签表示GUI背景板。

  我们可以使用样式表的以下值控制GUI样式:

  1. color- 控制背景板的填充色,默认rgb(198 198 198)
  2. background-color- 控制背景板的背景色,默认black
  3. border- 控制四个方向的阴影样式

GUI标题

  我们使用title标签表示GUI标题,对于该控件我们可以不指定控件的宽高和定位方式,API会自动计算控件尺寸并将其定位到父元素顶部中心位置。

  标题内容存储在attributesvalue项中,如果想要搭配I18n实现本地化可以让value的值以i18n:(不区分大小写,无空格间隔)开头,后跟key值即可。

  例如:

1
2
title(value = "这是一个普通的标题")
title(value = "i18n:key.simple")
1
key.simple=这是一个本地化的标题

玩家背包

  我们使用backpack标签表示玩家背包,对于该控件我们无需指定控件的宽度和定位方式,API会自动计算控件尺寸并将其定位到父元素底部中心位置。

  允许为一个GUI添加多个玩家背包,但没必要。

  由于该控件需要进行网络交互,所以必须为该控件分配一个id

进度条

  我们使用progress标签表示进度条,进度条目前支持且仅支持两种样式:箭头形和矩形。

  值得注意的是,当进度条样式为箭头形且长款不合理时,可能会绘制出奇怪的图案,箭头形一般采用15/22的比例。尽量让箭头指向的方向的宽度为奇数,否则绘制时三角形会变成一个梯形,后续也可能取消对非奇数的支持。

  进度条的数据存储在attributes中的valuemax中,分别表示进度条的值与最大值(最大值可为 0)。

燃烧进度条

  我们使用burn标签表示燃烧进度条(就是熔炉里面那个火焰形状的进度条),该控件无需也不能设定长宽。

  进度条的数据存储在attributes中的valuemax中,分别表示进度条的值与最大值(最大值可为 0)。

  与普通进度条不同的是,燃烧进度条的百分比计算是与普通进度条相反的,公式为:percent = 1 - (value / max)

按钮

  我们使用button标签表示按钮,目前按钮支持且仅支持两种样式:矩形和三角形。

  当按钮样式为三角形时,要求三角形的底边长度必须为奇数,当其长度为偶数时在绘制过程中会将长度减一使其变为奇数。而三角形的高则没有要求,当三角形的高过大时会在三角形底边的下面绘制一个矩形进行补充。

  按钮上显示的文本存储在attributes中的value项中,当文本超过按钮显示范围时默认不会被截断。

文本显示

  我们使用p标签表示文本,对于该控件我们无需手动指定控件的宽高,API会根据文本长度自动计算。

  控件显示的内容存储在attributesvalue项中,如果想要搭配I18n实现本地化可以让value的值以i18n:(不区分大小写,无空格间隔)开头,后跟key值即可。

物品框

  我们使用slot标签表示物品框,该控件默认长宽为18*18

  由于该控件需要进行网络交互,所以必须为其设定一个唯一的id

  控件的信息存储在attributes中:

  • index- 物品框在ItemStackHandler中的下标(默认0
  • priority- 物品框的优先级,数值越低优先级越高,当用户使用shift + 左键点击物品时,会优先将物品放入优先级高的物品框中(默认100
  • drop- 在关闭GUI时是否将物品框中的物品扔到世界中,不会清除ItemStackHandler中的数据(默认false
  • forbidInput- 是否禁止所有玩家向物品框放入物品(默认false
  • forbidOutput- 是否禁止所有玩家从物品框取出物品(默认false

  同时还有一部分代码层控制的数据:

  • handler- 物品框的ItemStackHandler对象,必须在GUI的初始化阶段完成对该值的初始化
  • inputChecker- 检查指定物品是否能够放入物品框
  • outputChecker- 检查指定物品能否被指定用户取出
  • onSlotChanged- 当物品框内的物品被修改时触发(如果用户没有调用handleronSlotChanged,则该方法不会触发)

输出物品框

  我们使用output标签表示输出物品框,该控件默认长宽为26*26

  由于该控件需要进行网路交互,所以必须为其设定一个唯一的id

  该控件是slot的特化版,除forbidInput强制为true以外没有其它区别。

物品矩阵

  我们使用matrix标签表示由若干个物品框组成的矩形矩阵。

  由于该控件需要进行网络交互,所以必须为其设定一个唯一的id

  控件的信息存储在attributes中:

  • index- 第一个物品框在ItemStackHandler中的下标(默认0
  • priority- 物品框的优先级(默认100
  • drop- 在关闭GUI时是否将物品框中的物品扔到世界中,不会清除ItemStackHandler中的数据(默认false
  • forbidInput- 是否禁止所有玩家向物品框放入物品(默认false
  • forbidOutput- 是否禁止所有玩家从物品框取出物品(默认false
  • xCount- X 轴方向上物品框的数量
  • yCount- Y 轴方向上物品框的数量

  同时还有一部分代码层控制的逻辑:

  • handler- 物品框的ItemStackHandler对象,必须在GUI的初始化阶段完成对该值的初始化
  • inputChecker- 检查指定物品是否能够放入物品框
  • outputChecker- 检查指定物品能否被指定用户取出
  • onSlotChanged- 当物品框内的物品被修改时触发(如果用户没有调用handleronSlotChanged,则该方法不会触发)

空盒子

  我们使用div标签表示一个空盒子,该控件被设计用于辅助排版,默认尺寸由子控件的尺寸计算。

代码层

  这里不会说明所有函数的用法,如果在这里没有找到用法说明这个函数的功能应该相当简单,以至于只需注释即可明确其所有功能。如果你看了注释仍然不能理解函数的功能或者运作细节,请在评论区说明你的问题。

事件系统

  GUI的运作离不开事件,不然很难实现按钮点击等功能。

事件基础

  我们通过Cmpt#addEventListener来为控件注册一个事件,函数原型如下:

1
fun addEventListener(name: String, listener: IGraphicsListener<*>)

  第一个字符串类型的参数表示事件名称,API自身支持的所有事件可在IGraphicsListener类中查看;第二个参数用来表示事件的执行体。

  注意:任何事件都应在客户端服务端按照相同顺序注册,除非你保证这个控件的所有事件都不需要进行双端通信!!!

  我们通过Cmpt#dispatchEvent来发布一个函数,函数原型如下:

1
fun dispatchEvent(name: String, message: ListenerData)

  其中第一个参数仍是事件名称,第二个参数表示触发事件时传递的信息,不同事件需求的信息类型不同,ListenerData类的具体内容我们后面再详细说明。

  所有API内置的事件都会自动触发,用户无需手动触发,只有当用户希望模拟事件的触发或者自行编写事件时需要手动发布事件。

  我们通过Cmpt#removeEventListener来删除指定事件,函数原型如下:

1
fun removeEventListener(name: String, listener: IGraphicsListener<*>)

  参数含义与addEventListener相同,不再赘述。

事件的传递

  与事件有关的数据均存储在ListenerData中,所有ListenerData均存有以下数据:

  • canCancel(Boolean)- 表示事件能否被取消
  • reverse(Boolean)- 是否反转事件的触发顺序(这个值下文详细说明)
  • prohibitTransfer(Boolean)- 是否禁止事件的传递
  • target(Cmpt)- 触发该事件的控件对象
  • cancel(Boolean)- 是否取消事件(当canCancelfalse时该项无效)
  • send2Service(Boolean)- 是否将事件的触发信息发送到服务端(默认为false,设为true时触发事件的控件必须持有唯一的id

  假设我们有如下结构的树:

1
2
3
4
mask
background
button#button
div

  当button控件触发鼠标点击事件时会先后触发buttonbackgroundmask的鼠标点击事件,而如果我们将reverse设置为true就会按照maskbackgroundbutton的顺序触发事件。

  不难发现,事件的传递遵循仅向父节点传递的原则。同时如果是客户端发送到服务端的事件触发信息,那么prohibitTransfer一定为true,即不进行任何形式的传递。如果需要在服务端进行事件传递就必须手写网络通信。

事件的触发

  这时候就到IGraphicsListener登场的时候了,当我们创建一个IGraphicsListener对象时仅需重写其中的active函数。

  active函数原型如下:

1
2
// T : ListenerData
fun active(`data`: T?)

  可以发现传入的值为可空类型,当事件是在服务端触发时,传入的值将会为null,因为API内置的事件网络通信仅传递事件的触发信息而不传送事件的具体数据。

控件对象

  所有控件都分为服务端对象和客户端对象,所有服务端类都从抽象类Cmpt派生,所有客户端类都从接口ICmptClient派生。

服务端

  一个服务端对象内部仅存储与服务端相关的数据(attributes除外):

  • 控件的节点信息(父节点、子节点)
  • 控件注册的事件列表
  • 控件的id

  API不会自动同步双端attributes中的数据(部分控件会同步部分数据),因为大多数情况服务端获取attribute并没有什么用处。

  我们可以通过BaseGraphicsCmpt中的以下函数获取其中的控件对象:

  • getElementByID- 通过ID获取对象
  • queryCmpt- 获取与表达式匹配的第一个控件
  • queryCmptAll- 获取所有与表达式匹配的控件
  • queryCmptLimit- 获取小于等于数量限制的数量的与表达式匹配的控件

  注:上述表达式与styl中的表达式格式与功能相同。

客户端

  客户端对象存储以下数据:

  • 服务端存储的所有数据
  • 样式表

  同时我们使用的时候有时可能会修改控件的className来修改控件的样式,这时会发现修改className后控件并没有发生变化。这是样式表缓存的原因,只需调用BaseGraphicsClient#updateStyleBaseGraphics#updateStyle即可(调用的时候可能会覆盖掉用代码修改的样式)。

打开GUI

  我们编写了几个拓展函数用于打开或关闭GUI:

1
2
3
fun EntityPlayer.openGui(key: ResourceLocation, x: Int, y: Int, z: Int)
fun EntityPlayer.openClientGui(key: ResourceLocation, x: Int, y: Int, z: Int)
fun EntityPlayer.closeClientGui(): Boolean

  其中的key就是GUI的key,注意需要加上modid,坐标是最终传入GUI中的坐标,一般该坐标用来指代触发该GUI的方块的坐标。

  上面的两个关于客户端GUI的函数在服务端调用均不会有任何效果且不会产生报错。


开发一个API真的很不容易
扫描下方打赏二维码支持一下吧ヾ(≧▽≦*)o