Minecraft Wiki:沙盒/Tutorial:自定义字体
前言[编辑源代码]
自定义字体在原版地图制作中有着很多应用。在学习本教程前,你需要先学习:
基础[编辑源代码]
字符[编辑源代码]
对于世界上的语言文字,计算机内部最终会将其分解和编码为一系列的基本组成单位。比如汉语的基本组成单位是一个一个汉字,而英语的基本组成单位是一个一个拉丁字母。
计算机中为语言文字最终编码的这些基本构成单位,可称为字符(Character)。
字符代码[编辑源代码]
要在计算机中表示字符,需要为每个字符指定一个数字编号。比如,在一个字符系统中,只有26个英文小写字母,那么可以有如下的对应关系:
字符 | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | q | r | s | t | u | v | w | x | y | z |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
编号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | l2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
这个编号也可以称为字符代码(Character Code)。字符代码是对字符的抽象定义,其可以在数学上形式化。
统一码[编辑源代码]
为了适应更多国家的语言文字,需要扩大所制定的代码范围。
统一码(Unicode)是一种常见的标准字符代码。由于该码可以容纳当今世界上几乎所有国家的语言文字,故又称为万国码。Unicode所表示的所有字符即统一码字符集(Universal Coded Character Set)。
网站SYMBL提供了关于各种字符Unicode的详细信息,你还可以在该网站上找到很多其他有用的工具。
Unicode将每个字符都使用一个整数来表示,该整数一般为16进制形式。该16进制形式的字符代码,即统一码的码位(Code Point)或码点。在文章中表示码位时,一般加上U+
前缀。例如,A
的码位可以表示为U+0041
或0041
。
对于Unicode字符集中的每个字符,其码位有2种格式:
- UCS-2:最初,使用固定2字节长度的字符代码来表示(范围是0000到FFFF)。例如
U+0041
。 - UCS-4:之后,为了容纳更多字符,允许使用固定的4字节长度的字符代码来表示(范围是00000000到FFFFFFFF)。例如
U+0001f9d9
。
对于UCS-2所表示的全体字符,可以被称为包含在基本多文种平面(Basic Multilingual Plane)中的全体字符——它包含了Unicode一开始所制定的所有基本字符。随着时间推移,UCS-2无法表示更多文字。为了扩充,需要使用更大的数字来表示,这时便提出辅助平面——收录了包括表情符号、更多的中日韩统一表意文字、楔形文字和埃及圣书体等的文字。
- 基本多文种平面的范围是0000到FFFF。
- 辅助平面有很多个。对于第一辅助平面(多文种补充平面),其总范围是10000到1FFFD。对于第二辅助平面(表意文字补充平面),其总范围是20000到2FFFD。
统一码的计算机实现[编辑源代码]
你有没有想过,计算机如何识别统一码?
例如,对于如下文本:
ABC
如果直接采用相应字符的UCS-2的二进制形式,则为:
000000 01000001 000000 01000010 000000 01000011
这里的空格是为了方便读者看清各个二进制位,并不代表实际的计算机存储也带有这些空格。显然,这种编码形式会留下很多0,导致额外空间浪费。并且,这种直接转换的方式导致无法准确区分各个字符的所在位置。比如,对于如下的二进制序列:
000000000000000000000000000000
其包含多个位置可以截取出一段16位二进制数字。以下是一种截取方式:
000 0000000000000000 00000000000
显然,这不够灵活,且容易导致一些非严格的编码行为。且如果在一台计算机中还存在其他编码方法,这也会产生兼容性问题。
- UTF-8
上面提到,为了适应计算机存储,需要对统一码进行转换。于是提出8位统一码转换格式(8-bit Unicode Transformation Format)。
- UTF-8的长度是可变的,其允许使用1到4个字节编码Unicode字符。
- 对于可以表示为1个字节长度代码的字符,直接将其Unicode码点转换为二进制。
- 对于需要表示为2个字节以及更长长度的字符,需要增加长度和间隔信息。
比如,对于Unicode码点为0180的字符,直接转换为二进制是
0001 1000 0000
显然,该码点至少需要使用2字节才能完整表示。为了让程序在从左到右的扫描过程中知道该二进制序列时一个2字节的编码整体,需要添加2个前导1:
11000110000000 |
2个前导1和后续的6位二进制数“000110”已经组成了一个完整的字节,所以将其归为一组。之后,还有剩余的6位二进制数“000000”,为了能在从左到右扫描时知道该二进制数和前部分相关,需要添加前导“10”:
1100011010000000 |
现在,就形成了完整的2字节UTF-8编码:
1100 0110 1000 0000 |
同理,对于3或4字节的UTF-8编码,首先应分别带有3或4个前导1,之后对字符码位的每6位二进制数之间插入间隔二进制数“10”。
- UTF-16
对于某些国家的语言,其字符采用UTF-8可能会浪费一些空间,比如汉字,在编码为UTF-8时,每个字节前都会有2位的前导二进制,而这本身和码点信息无关。为了更精简和统一地表示这些字符,可以考虑使用UTF-16。
- 对于每个Unicode字符,UTF-16可能会编码为2个字节,也可能会编码为4个字节。
- 对于基本多文种平面中的所有字符,其形式即码点的UCS-2形式——其长度为固定的2字节,可直接使用4个16进制数表示。
- 对于第一辅助平面(多文种辅助平面),其需要使用4字节来表示——这4个字节其实是2个UCS-2形式的码点组合而成,即代理对(Surrogate Pairs)。代理对需要使用8个16进制数表示。
UTF-16的代理对是将一些UCS-4形式的码点映射到一些未被正常使用的UCS-2形式码点。
- 在SNBT中使用Unicode
在25w09a/1.21.5及其以上版本,允许在SNBT字符串中使用Unicode——你可以在SNBT的字符串片段中定义以下字符字面量:
- 基本多文种平面
- 2位16进制形式。比如\x41(字符A)。
- 4位16进制形式。比如\u0100(字符Ā)。
- 辅助平面
- 使用代理对,写作两个4位16进制形式。比如\ud83e\uddd9(字符🧙)。
- 不使用代理对,写作一个8位16进制形式。比如\U0001f9d9(字符🧙)。
要注意,你只能定义符合以上位数的字符字面量。比如,将4位16进制Unicode\u0100写为3位16进制Unicode\u100是不允许的。
字形[编辑源代码]
每个字符,计算机要显示出来,则要对应相应的显示图形。这些图形可称为字形(Glyph)。
在原版Minecraft中,所有的字符通常对应到的字形都是位图图片。这使得我们可以方便的使用一般的图片编辑器便可修改字符对应的外观。而在Minecraft以外的地方,为追求显示清晰,字符对应的字形通常都是矢量轮廓,并且会封装成TrueType或OpenType格式的字体文件以供取用。TrueType字体在Minecraft内若要使用,在正确使用相关提供器时,游戏会将矢量轮廓烘焙成位图。
字形提供器[编辑源代码]
字形提供器是自定义字体的核心部分,它将字符和应渲染的图像在内部建立关联。
位图[编辑源代码]
位图字形提供器可建立字符的统一码到位图图片的映射关系。
你可以定义若干个单独的字符,同时设定height
来缩放图片。为防止覆盖常见字符,一般使用Unicode基本多文种平面(BMP)中的私用区字符(\ue000
到\uf8ff
),最大可定义6400个字符。
为了方便,你可以直接对原版命名空间中default.json
文件进行修改——所添加的字体会直接生效。如果要保持字符的相对独立性,你也可以在你自己的命名空间中添加一个字体文件,但在使用时你必须为文本组件指定font字段。

minecraft/font/default.json
{
"providers": [
{
"type": "bitmap",
"ascent": 16,
"height": 16,
"file": "mcw:test/bitmap_test.png",
"chars": [
"\ue001"
]
},
{
"type": "bitmap",
"ascent": 32,
"height": 32,
"file": "mcw:test/bitmap_test.png",
"chars": [
"\ue002"
]
},
...
}
下图均使用/title
命令来显示字体:
title @s actionbar "\ue001\ue002\ue003\ue004"

你也可以如下定义4个Unicode字符,目标图像mcw:test/bitmap_test.png
将被均分为4张16高度的图片:

minecraft/font/default.json
{
"providers": [
{
"type": "bitmap",
"ascent": 16,
"height": 16,
"file": "mcw:test/bitmap_test.png",
"chars": [
"\ue001\ue002",
"\ue003\ue004"
]
},
}
分割时会以图片大小整除以矩阵大小,最终结果会向下取整,所以有时可能会出现丢失像素的情况。例如,将一张64×64的图片被从上到下均分为三等分,由于64除以3向下取整后为21,即所分割的每张图片高度均为21,故原图的最后一行像素被丢弃。
要注意的是,字符矩阵chars的行长度必须保持一致,否则无法正常加载位图。
你还可以通过设定ascent
的值来改变字形顶端到基线的距离。这可控制字形在UI中的上下位置。
![]() |
![]() |
左右偏移[编辑源代码]

字符的左右偏移可通过space
字形提供实现。
如下,字符\uf814
将向左偏移14个单位:

minecraft/font/default.json
{
"providers": [
{
"type": "space",
"advances": {
"\uf814": -14
}
}
}
为了方便使用,你可以通过语言文件为其设定键名:

minecraft/lang/zh_cn.json
{
"container.mcw.title_test": "\uf814§f\ue001"
}
你可以通过偏移实现在原本的UI区域外绘制图像(如右图)。要注意,箱子标题默认会被染为灰色。为了防止图片变灰,我们需要添加一个§f
。
应用[编辑源代码]
数值条[编辑源代码]

很多时候我们希望在UI界面上增加一个数值条,用以显示新增的数值信息,比如魔力值、水分值等。这些数值信息一般记录在记分板中。为了能够让每个一个分数都对应一个数值条状态,我们首先需要对原始图片进行扩增——绘制其所有状态下的图形并整合到一张图中。
如图,一般我们只需在一开始绘制完满状态的数值条,然后使用程序生成其变化后的其他状态。本例的魔法值条即在上一张图片的基础上绘制黑色矩形,同时从右到左改变绘制起点,如此循环,最后即可得到一张正方形图集。
为了使位图字形提供器正确切分并处理,我们还需要生成一个字符矩阵。在程序上,我们只需在生成每张图片的同时,将图片编号转换为对应Unicode即可。对于空白位置(比如最后一行),我们还需进行判断并使用空白符(\u000
)占位。这样才能保证字符矩阵的每一行长度相等。
如果你在程序编写上感到困难,不妨使用AI。
from PIL import Image, ImageDraw, ImageColor
import math
import argparse
import json
def next_power_of_two(n):
"""计算大于等于n的最小2的幂"""
return 1 if n == 0 else 2 ** math.ceil(math.log2(n))
def draw_on_image(img, x, y, width, height, fill, outline, border):
"""在图片上绘制矩形并返回新图片"""
img_copy = img.copy()
draw = ImageDraw.Draw(img_copy)
# 计算右下角坐标
x2 = x + width - 1
y2 = y + height - 1
# 绘制带边框的填充矩形
draw.rectangle(
[(x, y), (x2, y2)],
fill=fill,
outline=outline,
width=border
)
return img_copy
def create_tile_atlas(original_img, output_path, count,
x, y, width, height,
fill, outline, border):
"""生成包含多个相同修改图片的图集"""
# 生成基础修改图片
# base_tile = draw_on_image(
# original_img,
# x, y, width, height,
# fill, outline, border
# )
base_tile = original_img
tile_width = base_tile.width
tile_height = base_tile.height
# 计算图集参数
cols = math.ceil(math.sqrt(count))
# atlas_size = next_power_of_two(cols * tile_width)
atlas_size = max(tile_height * cols, tile_width * cols)
# 创建图集画布
atlas = Image.new('RGBA', (atlas_size, atlas_size))
tiles_per_row = atlas_size // tile_width
tiles_per_col = atlas_size // tile_height
# 排列图片
line_division = []
division_matrix = []
counter = 0
for i in range(tiles_per_col):
for j in range(tiles_per_row):
if counter >= count:
line_division.append('\\u0000')
continue
# print(r'\u' + hex(i + 1 + 0xe000)[2:])
line_division.append(r'\u' + hex(counter + 1 + 0xe000)[2:])
# row = i // tiles_per_row
# col = i % tiles_per_row
x_pos = j * tile_width
y_pos = i * tile_height
if counter != 0:
base_tile = draw_on_image(base_tile, x - width * counter, y, width, height, fill, outline, border)
atlas.paste(base_tile, (x_pos, y_pos))
counter += 1
division_matrix.append(''.join(line_division))
line_division.clear()
# division_matrix.append(hex(i + 1 + 0xe000).replace("0x", '\\u'))
with open('matrix.csv', 'w+') as outfile:
outfile.write(json.dumps(division_matrix, indent=2).replace('\\\\', '\\'))
atlas.save(output_path)
print(f"生成图集:{output_path} ({atlas_size}x{atlas_size})")
print(f"包含 {count} 个图块,排列方式:{tiles_per_row}x{tiles_per_row}")
def parse_color(color_str):
"""解析颜色字符串"""
if color_str.lower() in ('none', 'transparent'):
return None
try:
return ImageColor.getrgb(color_str)
except ValueError:
raise ValueError(f"无效颜色值:{color_str}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description='生成带重复修改图片的正方形图集',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
# 必需参数
parser.add_argument('input', help='原始图片路径')
parser.add_argument('output', help='输出图集路径')
parser.add_argument('count', type=int, help='要生成的图块数量')
# 矩形参数
parser.add_argument('x', type=int, help='矩形左上角X坐标')
parser.add_argument('y', type=int, help='矩形左上角Y坐标')
parser.add_argument('width', type=int, help='矩形宽度')
parser.add_argument('height', type=int, help='矩形高度')
# 样式参数
parser.add_argument('--fill', default='none', help='填充颜色')
parser.add_argument('--outline', default='black', help='边框颜色')
parser.add_argument('--border', type=int, default=1, help='边框宽度')
args = parser.parse_args()
try:
# 加载原始图片
original = Image.open(args.input).convert('RGBA')
# 验证颜色参数
fill_color = parse_color(args.fill)
outline_color = parse_color(args.outline)
# 执行生成流程
create_tile_atlas(
original,
args.output,
args.count,
args.x, args.y,
args.width, args.height,
fill_color,
outline_color,
args.border
)
except Exception as e:
print(f"错误:{str(e)}")
exit(1)
以上程序最终将输出一张图片和一个字符矩阵文本文件,可以辅助编写资源包字体文件。
在数据包方面,你还需根据你的数值等级计算对应的图片编号,并将编号和每个字符对应。
例如以下程序,将记分项magic_point
和Unicode建立对应关系,并生成title
命令:
text_list = []
max = 77
for num in range(0, max + 1):
# print(hex(num)[2:].zfill(4))
if num == max // 2:
command = f'''
execute if score @s magic_point matches {max - num + 1}.. run return fail'''
text_list.append(command)
command = f'''
execute if score @s magic_point matches {max - num} run title @s actionbar {{"text":"\\uf801\\ue{hex(num + 1)[2:].zfill(3)}","font":"mcw:magic_point_bar"}}'''
text_list.append(command)
text = ''.join(text_list).replace('\\\\', '\\')
with open(r'custom_item_throwable_fireball/data/lbj/function/actionbar/show_magic_point.mcfunction', mode='w+') as outfile:
text = f'''
scoreboard objectives add max_magic_level dummy
scoreboard objectives add magic_level dummy
scoreboard players set #const var 100
scoreboard players operation #temp var = @s magic_level
scoreboard players operation #temp var *= #const var
scoreboard players operation #temp var /= @s max_magic_level
scoreboard players set #const var {max}
scoreboard players operation #temp var *= #const var
scoreboard players set #const var 100
scoreboard players operation #temp var /= #const var
scoreboard players operation @s magic_point = #temp var
''' + text
outfile.write(text)

以上程序的后半部分用以计算magic_level
所对应的magic_point
(可当做图片编号)。由于在实际应用中,我们往往希望玩家的魔力值随升级而改变,所以我们需要使用额外的magic_level
来记录当前玩家实际的魔力等级。另外,我们还需max_magic_level
记分项来存储最大魔力等级,这样,我们就可以计算出魔力等级所占最大魔力等级的百分比值,根据该百分比值和最大魔力值即可计算得到当前魔力值magic_point
。
我们还需考虑实际的显示位置。例如,我们可以通过修改ascent
以及添加“负空格”(上述程序中的\\uf801
)来对齐生命条。
最终,高频执行命令:
execute as @p run function lbj:actionbar/show_magic_point
在你改变magic_level
时,可以看到魔力值同步变化。
屏幕图像[编辑源代码]

要直接在屏幕上显示图形,一般利用/title
输出位图字体即可。
你可以规定任何带有自定义数据selected_trigger的物品都会在玩家选中时执行函数。在任意一个tick函数中实现该检测:

.../function/tick.mcfunction
execute if items entity @s weapon.mainhand *[custom_data~{selected_trigger:1b}] run function test:player_selected_paper
如果玩家手持物为纸,则输出相应的位图。这里\uf801
是使用space
字形提供器制作的反向空格(这里的偏移值为-16),用以将图形偏移到屏幕中央。\ue001
即所定义的位图(总大小为256×256,非空白像素居中,height设置为64,ascent设置为32)。

test/function/player_selected_paper.mcfunction
execute unless items entity @s weapon.mainhand paper run return fail
title @s times 0 3 0
title @s title [{"text":"\uf801\ue001","font":"mcw:title","shadow_color":[0,0,0,0]}]