Skip to content
On this page

图片裁剪与动态血条

一张图片不一定要整张显示。ChaUI 的“源 X、源 Y、源宽、源高”可以从素材中选出一块区域,再把这块区域绘制到组件的宽高里。

这篇教程会完成两个效果:

  • 从一张 2×2 图集中显示右下角图标;
  • 制作随玩家血量缩短、纹理不会被压扁的血条。

开始前,请先完成制作第一个界面,了解页面、图片组件和编辑器属性面板的基本操作。

先分清两组宽高

图片组件同时有“布局宽高”和“源图宽高”,它们负责不同的事情。

属性决定什么单位
layout.widthlayout.height最终在界面上占多大GUI 像素
sourceXsourceY从原图哪个位置开始取原图像素
sourceWidthsourceHeight从原图取多大一块原图像素

源图坐标从左上角 (0, 0) 开始,X 向右增加,Y 向下增加。选中的区域会缩放到 layout.widthlayout.height,所以这两组尺寸不能混为一谈。

上图只展示当前概念涉及的关键字段。准备直接制作组件时,请继续使用后面的完整组件配置。

第一个练习:从 2×2 图集中取图标

教程图集是 64×64 像素,共有四格,每格 32×32 像素。右下角图标的左上角正好位于 (32, 32)

下载教程图集,放到当前游戏版本目录:

txt
resourcepacks/ChaUI/gui/tutorial/icon-atlas.png

从原图到最终图标

先看完整图集,再确定选区起点和大小,最后设置界面中的显示尺寸。三组尺寸按顺序填写,就不容易把源图像素和 GUI 像素混在一起。

这一步只需要关注下面这些差异字段:

yaml
layout:
  width: 64
  height: 64
image:
  sourceX: 32
  sourceY: 32
  sourceWidth: 32
  sourceHeight: 32

这是关键字段对照,不是完整组件。可直接使用的完整组件配置在下一节。

在编辑器中新建图片组件,填写:

编辑器属性填写值作用
图片路径gui/tutorial/icon-atlas.png使用刚才保存的图集。
源 X32跳过左边 32 像素。
源 Y32跳过上边 32 像素。
源宽32取一格宽度。
源高32取一格高度。
64最终放大到 64 GUI 像素宽。
64最终放大到 64 GUI 像素高。

对应组件配置如下:

yaml
- id: atlas_icon
  type: image
  parent: ""
  visible: true
  enabled: false
  pointerEvents: pass
  scale: 1
  opacity: 1
  z: 10
  layout:
    x: 20
    y: 20
    width: 64
    height: 64
  image:
    path: gui/tutorial/icon-atlas.png
    gifLoop: true
    gifLoopCount: 0
    sourceX: 32
    sourceY: 32
    sourceWidth: 32
    sourceHeight: 32
  events: {}

这里从原图取出的仍是 32×32 图标,只是最终显示为 64×64。改变布局宽高只改变显示尺寸,不会改成图集里的另一格。

坐标填错时会发生什么

选区大小相同并不代表会显示同一个图标。sourceXsourceY 决定从哪一格开始取图;两者都填 0 时,会取到左上角。

yaml
# 反例:起点在左上角,最终显示左上角图标
sourceX: 0
sourceY: 0

# 正例:起点移动到右下角,最终显示目标图标
sourceX: 32
sourceY: 32

这里只对比坐标差异;两种写法的 sourceWidthsourceHeight 都是 32

第二个练习:不会变形的动态血条

先下载两张 100×8 像素素材:

放到:

txt
resourcepacks/ChaUI/gui/tutorial/health-empty.png
resourcepacks/ChaUI/gui/tutorial/health-fill.png

血量变化时应该看到什么

空槽始终保持完整,只有上方填充层随血量变化。正确裁剪时,各段纹理的宽度不会改变;血量为 0 时,填充层不绘制。

yaml
# 两个字段使用同一个血量比例
layout:
  width: clamp(vals.player.health / vals.player.maxHealth, 0, 1) * 200
image:
  sourceWidth: clamp(vals.player.health / vals.player.maxHealth, 0, 1) * 100

这是公式关系的简要对照;完整图片组件仍需包含路径、坐标、源高和其他通用字段。

血条由两张图片叠在一起:

  1. health_empty 在下方,始终显示完整空槽;
  2. health_fill 在上方,同时缩短显示宽度和源图选区宽度。

假设玩家有 35% 血量。如果只把 layout.width 改成 35%,ChaUI 仍会把完整 100 像素纹理塞进较短区域,纹理刻度会一起被压扁。正确做法是同时把 sourceWidth 改成 35%,只截取纹理左侧 35 像素。

yaml
# 反例:完整纹理被压缩
layout:
  width: 70
image:
  sourceWidth: 100

# 正例:35% 的选区显示为 35% 的宽度
layout:
  width: 70
image:
  sourceWidth: 35

上面的固定数值用于直观看出 35% 血量时的区别。实际动态血条继续使用下面的完整公式配置。

核心配置是:

yaml
- id: health_fill
  type: image
  z: 11
  layout:
    x: 20
    y: 20
    width: clamp(vals.player.health / vals.player.maxHealth, 0, 1) * 200
    height: 16
  image:
    path: gui/tutorial/health-fill.png
    sourceX: 0
    sourceY: 0
    sourceWidth: clamp(vals.player.health / vals.player.maxHealth, 0, 1) * 100
    sourceHeight: 8

同一个血量比例分别乘以两个基准:

  • * 200 控制界面上最多显示 200 GUI 像素宽;
  • * 100 控制从 100 像素宽的原图中截取多少像素。

公式结果有小数时,源图坐标和尺寸会向下取整。例如结果为 73.8,实际截取 73 个源图像素。血量为 0 时,sourceWidth 得到 0,填充图片不绘制,只留下空血条。

可直接使用的完整页面

下面的页面同时包含图集图标和动态血条。素材按前面的目录放好后,可以将配置保存为 source_region_demo.yml

yaml
id: source_region_demo
version: 1
title: 图片裁剪与动态血条
size:
  width: 320
  height: 140
coordinateMode: absolute
display:
  mode: screen
  screen:
    dimBackground: false

vars: {}

papi:
  refreshTicks: 20
  values: {}

events:
  open: ""
  close: ""

methods: {}

elements:
  - id: atlas_icon
    type: image
    parent: ""
    visible: true
    enabled: false
    pointerEvents: pass
    scale: 1
    opacity: 1
    z: 10
    layout:
      x: 20
      y: 20
      width: 64
      height: 64
    image:
      path: gui/tutorial/icon-atlas.png
      gifLoop: true
      gifLoopCount: 0
      sourceX: 32
      sourceY: 32
      sourceWidth: 32
      sourceHeight: 32
    events: {}

  - id: health_empty
    type: image
    parent: ""
    visible: true
    enabled: false
    pointerEvents: pass
    scale: 1
    opacity: 1
    z: 10
    layout:
      x: 100
      y: 32
      width: 200
      height: 16
    image:
      path: gui/tutorial/health-empty.png
      gifLoop: true
      gifLoopCount: 0
      sourceX: 0
      sourceY: 0
      sourceWidth: 100
      sourceHeight: 8
    events: {}

  - id: health_fill
    type: image
    parent: ""
    visible: true
    enabled: false
    pointerEvents: pass
    scale: 1
    opacity: 1
    z: 11
    layout:
      x: 100
      y: 32
      width: clamp(vals.player.health / vals.player.maxHealth, 0, 1) * 200
      height: 16
    image:
      path: gui/tutorial/health-fill.png
      gifLoop: true
      gifLoopCount: 0
      sourceX: 0
      sourceY: 0
      sourceWidth: clamp(vals.player.health / vals.player.maxHealth, 0, 1) * 100
      sourceHeight: 8
    events: {}

vals.player.healthvals.player.maxHealth 来自当前客户端玩家,页面每次渲染都会重新计算。它们只适合显示,不应当作为服务端奖励、扣费或权限判断依据。

其他组件也能裁剪

源图公式不只属于图片组件:

组件可用选区
图片sourceX/Y/Width/Height
按钮普通、悬浮、选中三组选区
输入框普通、聚焦两组选区
物品槽可选静态背景选区
物品展示可选静态背景选区

这些字段都接受非负整数或公式。按钮与输入框需要分别配置各状态的选区;切换状态不会自动沿用另一张尺寸不同的图集区域。

在脚本中改变和读取选区

直接赋数字会把当前字段改成固定值;使用 formula(...) 才会继续跟随变量。下面的完整按钮事件会绑定公式,再读取当前结果和原公式:

yaml
events:
  leftClick: |-
    component("health_fill").image.sourceWidth = formula(
      clamp(vals.player.health / vals.player.maxHealth, 0, 1) * 100
    )
    vars.currentSourceWidth = component("health_fill").image.sourceWidth
    vars.currentSourceFormula = component("health_fill").image.expression("sourceWidth").source
    log("当前源宽:{vars.currentSourceWidth}")
    log("源宽公式:{vars.currentSourceFormula}")

脚本修改只影响当前打开的页面会话,不会写回服务端 yml。sourceWidth 读取的是这次渲染条件下算出的整数;expression("sourceWidth").source 读取的是保存的公式文字。

带端帽的血条怎么做

有些血条左右两端是圆角、金属扣或其他不能裁掉的图案。不要把整根血条做成一张会变化宽度的图片,建议拆成三层:

  1. 左端帽使用固定图片和固定宽度;
  2. 中间填充使用本教程的 layout.width + sourceWidth 同步公式;
  3. 右端帽使用固定图片,并让 X 跟随中间填充的右边。

这样血量变化时只裁剪中间的可重复区域,两端不会变形或突然消失。

常见问题

图片完全不显示

先清空四个源图字段测试整张图片。如果整图能显示,再检查选区是否越过图片边界。负数、非数字、未知变量、非有限结果或越界选区只会跳过当前素材,不会关闭页面。

源宽或源高填 0 后不显示

这是正常行为。sourceWidth: 0sourceHeight: 0 表示当前素材不绘制,适合让血量为 0 时隐藏填充层。

调整源宽后纹理还是被压扁

确认 layout.width 也使用了同一个比例。只改源宽会把较短的源区域重新拉满旧布局宽度;只改布局宽则会把完整源图压进较短区域。

编辑器保存后公式变成数字

当前版本会保留源图公式原文。输入时要先写完整公式再离开输入框,半截公式会被拒绝,不会覆盖上一次合法值。

公式读取的是当前值还是原文

component("id").image.sourceWidth 是当前整数;component("id").image.expression("sourceWidth").source 才是原始公式。两者用途不同,不要混用。

继续阅读图片组件了解完整字段,或阅读组件对象、布局公式与临时组件学习脚本动态绑定。