悬浮文字(全息显示 / 头顶名牌 / 计分板 / 伤害飘字)¶
原文:原创教程(ぴよまる),API 依据 Minecraft 1.20.1(Forge 47.x + KubeJS 6.1)实测行为
「悬浮文字」在 Minecraft 里能拆成五件完全不同的东西,新手最常见的踩坑就是拿一种方案去硬套另一种需求。先看清这张表,再往下翻对应章节:
| 需求 | 用什么 | 会跟着存档走吗 | 章节 |
|---|---|---|---|
| 世界里漂浮的固定文字(招牌、地标、价格牌) | text_display 展示实体 |
❌ 需自己重建 | §1 §2 |
| 玩家头顶名字/称号(一直有,别人也能看见) | displayName + 队伍前缀 |
✅ 天然持久 | §3 |
| 屏幕右侧的排行榜/状态栏 | 计分板 Scoreboard | ✅ 存档自带 | §4 |
| 怪物头顶一直飘着的「血量」 | 名字(name)+ 队伍后缀 | ✅ 持久 | §5 |
| 打字后冒出、0.5 秒消失的飘字 | ActionBar / Title(原版 HUD) | — | §6 |
| 走在路上头顶飘的「+5」伤害数字 | 需客户端粒子/渲染,KubeJS 做不到 | ❌ | §7 ⚠️ |
1. 世界悬浮文字:text_display 基础¶
minecraft:text_display 是展示实体(Display Entity),没有碰撞、没有 AI、不参与战斗,只负责在某个坐标显示一段文字。详细机制见「虚拟实体生成」篇,这里只讲悬浮文字的实战用法。
// server_scripts/hologram_basic.js
ServerEvents.loaded(event => {
event.server.runCommandSilent(
'summon minecraft:text_display 100 85 100 ' +
// 文字内容:JSON 文本组件,支持颜色/加粗/多段
'{text:\'{"text":"欢迎来到落英服","color":"gold","bold":true}\',' +
' billboard:"center",' + // 永远正对玩家(公告牌模式)
' see_through:1b,' + // 隔墙可见(地标必开)
' shadow:1b,' + // 文字描边,浅色背景下更清楚
' line_width:300,' + // 超过这个宽度自动换行(单位:像素,约 150 个中文字宽度=300)
' background:0,' + // 背景色 ARGB,0 = 完全透明
' Tags:["hologram"]}'
)
})
1.1 换行与多行¶
text_display 没有 \n——原版换行靠两个手段:
| 方式 | 写法 | 效果 |
|---|---|---|
| 自动换行 | line_width:200 |
超过宽度自动折行 |
| 手动换行 | 文本组件里塞 "\n" |
强制断行 |
// 多行:手动 \n + 自动换行配合
// ⚠️ NBT 字符串里 \n 必须写成 \\n(先过 JSON 转义,再过命令解析)
'summon minecraft:text_display 100 85 100 ' +
'{text:\'{"text":"§6第一行\\n§f第二行\\n§7第三行"}\',billboard:"center",line_width:300}'
⚠️ 三层转义陷阱:KubeJS 字符串 → JSON 文本组件 → 命令 NBT。
\n要写成\\n,"要写成\'...\'外层用单引号。写不对就整条 summon 失败(控制台会报错)。
1.2 常用 text_display NBT 完整表¶
| 字段 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
text |
JSON 字符串 | 显示的文字(文本组件) | '{"text":"你好"}' |
billboard |
枚举 | 朝向:fixed/vertical/horizontal/center |
center(始终正对) |
line_width |
int | 自动换行宽度(像素) | 200~300 |
background |
int | 背景色 ARGB | 0(透明)/0x40000000(半透明黑,注意 NBT 里写 1073741824 十进制或 0x40000000) |
see_through |
byte | 隔墙可见 | 1b(地标) / 0b(室内) |
shadow |
byte | 文字阴影 | 1b |
default_background |
bool | 是否用原版默认背景 | false |
text_opacity |
byte | 不透明度 -1~127 | -1(不透明) |
alignment |
枚举 | left/center/right(配合多行) |
center |
view_range |
float | 可见距离(默认 1.0 = 64格 × 倍数) | 2.0f(128格可见) |
transformation |
16 float | 矩阵:缩放/旋转/平移 | 见 §1.3 |
1.3 放大 / 缩小文字¶
文字大小不是字号,而是靠 transformation 矩阵缩放:
// 缩小到 0.5 倍(小字贴墙) || 放大到 3 倍(远距离地标)
// 前 9 位是 3x3 缩放/旋转矩阵,第 10-12 位是平移,最后一位必须是 1f
'{...,transformation:{left_rotation:{},right_rotation:{},scale:[0.5f,0.5f,0.5f],translation:[0f,0f,0f]}}'
// 旧写法:transformation:[0.5f,0f,0f, 0f,0.5f,0f, 0f,0f,0.5f, 0f,0f,0f, 1f]
💡 1.20.1 两种写法都认。推荐用对象写法(
scale+translation),可读性强、不会数错位数。平移单位是方块(translation:[0f,0.5f,0f]= 上移半格)。
2. 实战:可配置的世界悬浮文字(含持久化)¶
直接 summon 的 text_display 服务器一重启就消失(展示实体不写区块)。生产环境一律用「JSON 配置 + 启动重建」。
// server_scripts/holograms.js
// 配置文件:kubejs/config/holograms.json (首次启动自动生成示例)
const HOLO_FILE = 'kubejs/config/holograms.json'
// ① 读取配置(没有就写一份示例出来)
function readHolograms() {
let data = JsonIO.read(HOLO_FILE)
if (!data) {
data = {
holograms: [
{ id: 'spawn_welcome', world: 'minecraft:overworld', x: 100, y: 85, z: 100,
text: '§6§l欢迎来到落英服', lineWidth: 300, scale: 1.0, seeThrough: true },
{ id: 'spawn_rule', world: 'minecraft:overworld', x: 100, y: 83, z: 100,
text: '§f游戏规则:禁止外挂 / 禁止卡BUG', lineWidth: 300, scale: 0.7, seeThrough: true }
]
}
JsonIO.write(HOLO_FILE, data)
}
return data.holograms || []
}
// ② 清除旧的全息(按 tag,避免重启/重载时叠罗汉)
function clearHolograms(server) {
server.allLevels.forEach(level => {
level.runCommandSilent('kill @e[type=minecraft:text_display,tag=holo]')
})
}
// ③ 生成一条(world 支持自定义维度,跨维度必须带维度前缀)
function spawnHologram(server, h) {
let text = h.text.replace(/"/g, '\\"') // 文本里的双引号转义
let nbt = '{text:\'{"text":"' + text + '"}\',' +
'billboard:"center",' +
'see_through:' + (h.seeThrough ? '1b' : '0b') + ',' +
'shadow:1b,background:0,' +
'line_width:' + (h.lineWidth || 300) + ',' +
'view_range:2.0f,' +
'transformation:{left_rotation:{},right_rotation:{},' +
'scale:[' + (h.scale || 1) + 'f,' + (h.scale || 1) + 'f,' + (h.scale || 1) + 'f],' +
'translation:[0f,0f,0f]},' +
'Tags:["holo","holo_' + h.id + '"]}'
server.runCommandSilent(`execute in ${h.world} run summon minecraft:text_display ${h.x} ${h.y} ${h.z} ${nbt}`)
}
// ④ 统一重建入口(启动 / 指令都走这里)
function reloadHolograms(server) {
clearHolograms(server)
let list = readHolograms()
list.forEach(h => spawnHologram(server, h))
console.log(`[全息文字] 已重建 ${list.length} 条`)
}
// ⑤ 服务器启动时自动重建
ServerEvents.loaded(event => reloadHolograms(event.server))
// ⑥ 管理员指令:/holo reload 热重载(改完 JSON 不用重启服)
ServerEvents.commandRegistry(event => {
event.register(
event.commands.literal('holo')
.then(event.commands.literal('reload')
.requires(s => s.hasPermission(2))
.executes(ctx => {
reloadHolograms(ctx.source.server)
ctx.source.player.tell('§a全息文字已重载')
return 1
}))
)
})
💡 跨维度
execute in minecraft:the_nether run summon ...是唯一靠谱的写法;summon永远在命令执行者所在维度生成,不写execute in就会跑到主世界。
2.1 动态文字(服务器 TPS / 在线人数 / 实时比分)¶
想让文字内容变化(不是位置),两种办法:
// 办法 A:直接改实体(1.20.1 改 text 有难度,命令不行)→ 推荐:删了重建
// 办法 B:用 server.scheduleInTicker 定时重建(每 5 秒 = 100 tick)
ServerEvents.loaded(event => {
event.server.scheduleInTicker(event => {
let server = event.server
let online = server.players.length
let tps = server.tickCount % 2 // 占位示意,真实 TPS 需 mod 提供
server.runCommandSilent('kill @e[type=minecraft:text_display,tag=holo_stat]')
server.runCommandSilent(
'execute in minecraft:overworld run summon minecraft:text_display 100 88 100 ' +
'{text:\'{"text":"§e在线人数: §a' + online + '"}\',billboard:"center",' +
'see_through:1b,background:0,view_range:2.0f,Tags:["holo","holo_stat"]}'
)
}, 100) // 每 100 tick(5 秒)
})
⚠️ 动态重建别写太快:每 tick 重建会产生大量实体创建/销毁包,卡玩家客户端。5 秒(100 tick)是安全下限,比分这类需要实时性强的,用 §4 的计分板或者字幕后缀方案。
3. 玩家头顶名牌(displayName / 队伍前缀)¶
想让「玩家头顶/聊天/Tab 显示称号」,不要用 text_display(那玩意不会跟着人走)。正确做法是改 displayName:
// server_scripts/player_nameplate.js
// 玩家进服 → 按称号改头顶名字
PlayerEvents.loggedIn(event => {
let player = event.player
player.persistentData.title = player.persistentData.title || '新人'
player.displayName = Text.of([ // Text.of 支持数组拼接
'§6[', player.persistentData.title, '] ', // 前面加称号
player.username // 原始名字
])
})
// 称号变了 → 立即刷新(不用重进服)
function refreshNameplate(player) {
player.displayName = Text.of(['§6[', player.persistentData.title, '] ', player.username])
}
⚠️ 陷阱一:
player.username是原始名字,player.name返回的可能是被改过的Component。赋值用player.username拼。 ⚠️ 陷阱二:displayName改了服务端文件不存,重进服会重置成原始名 → 所以要在PlayerEvents.loggedIn里每次重新设置。
3.1 护甲架/名牌实体(head 悬浮字)¶
想要「跟着玩家头顶一直飘的文字」又不想用客户端 mod,可以用原版 marker 实体骑乘玩家 + text_display 骑乘 marker:
// 给玩家挂一个悬浮文字(跟随移动)
function attachNameplate(server, player, text) {
let uuid = player.uuid.toString()
server.runCommandSilent(`kill @e[type=minecraft:text_display,tag=pt_${uuid}]`)
server.runCommandSilent(
`execute at ${player.username} run summon minecraft:text_display ~ ~2.3 ~ ` +
`{text:'{"text":"${text}"}',billboard:"center",see_through:0b,shadow:1b,` +
`background:0,view_range:1.0f,Tags:["pt_${uuid}"]}`
)
// 骑乘到玩家身上(这样会跟着走)
server.runCommandSilent(`ride @e[type=minecraft:text_display,tag=pt_${uuid},limit=1] mount ${player.username}`)
}
⚠️ 骑乘 text_display 到玩家身上是给所有人看的,且每加一个玩家就多一个实体——人数多别用。原版 f3 里能看到重复实体。真正的「只给某个玩家看的文字」在纯服务端做不到(那需要 §7 的客户端渲染)。
4. 屏幕悬浮文字:计分板(Sidebar / 下方列表)¶
「屏幕侧边排行榜」用原版计分板,KubeJS 直接跑命令即可,且存档自带、不用重建:
// server_scripts/scoreboard_hud.js
ServerEvents.loaded(event => {
let s = event.server
s.runCommandSilent('scoreboard objectives add hud dummy {"text":"§6洛英服务器"}')
s.runCommandSilent('scoreboard objectives setdisplay sidebar hud')
})
// 更新某一行(用「假玩家」名作为行的显示文本,`§r` 保证每行内容唯一)
function setHudLine(server, key, value) {
server.runCommandSilent(`scoreboard players set "${key}" hud ${value}`)
}
// 每 5 秒刷新在线人数
ServerEvents.loaded(event => {
event.server.scheduleInTicker(ctx => {
setHudLine(ctx.server, '§e在线玩家:', ctx.server.players.length)
setHudLine(ctx.server, '§aTPS:', 20) // 真实 TPS 需 API
}, 100)
})
💡 侧边栏最多 15 行,同一分值会乱序 → 每行分值必须不同(用 15,14,13… 倒序排)。
5. 怪物/实体头顶血条文字¶
生物身上常看到「Lv.5 僵尸 ❤ 20/20」这种,纯原版也能做:名字 + 队伍后缀 + 定时更新。
// server_scripts/mob_healthbar.js
// 思路:把血量拼进 customName(名字),每 0.5 秒更新一次
const MOB_TAG = 'hp_bar'
ServerEvents.entitySpawn(event => {
let e = event.entity
if (!e.creature) return // 只处理生物(非生物没血)
e.tags.add(MOB_TAG)
e.persistentData.hpBar = true
})
// 每 10 tick 扫一遍(只在玩家附近的实体上跑,省性能)
ServerEvents.loaded(event => {
event.server.scheduleInTicker(ctx => {
let server = ctx.server
server.players.forEach(p => {
// 半径 16 格内的血条生物
p.level.getEntitiesWithin(AABB.of(p.x - 16, p.y - 16, p.z - 16, p.x + 16, p.y + 16, p.z + 16)).forEach(e => {
if (!e.tags.contains(MOB_TAG)) return
let hp = Math.ceil(e.health)
let max = Math.ceil(e.maxHealth)
e.customName = Text.of([`§f${e.type} §c❤ §e${hp}§7/§e${max}`])
e.customNameVisible = true
})
})
}, 10)
})
⚠️ 性能:
getEntitiesWithin每 10 tick 对每个玩家扫一次,10 人服没问题;50 人服建议改成事件驱动(EntityEvents.hurt时只更新被打的那只)。
6. 屏幕中央飘字:Title / ActionBar¶
玩家屏幕正中/物品栏上方那一行字,是原版 HUD,用 server.runCommandSilent 打 title / actionbar:
// 中央大标题(淡入 10 tick / 停留 40 tick / 淡出 10 tick)
player.server.runCommandSilent(
`title ${player.username} times 10 40 10`
)
player.server.runCommandSilent(
`title ${player.username} title {"text":"§6§l游戏开始","color":"gold"}`
)
player.server.runCommandSilent(
`title ${player.username} subtitle {"text":"§f祝你好运"}`
)
// 物品栏上方小字(ActionBar,3 秒自动消失,会覆盖前一条)
player.server.runCommandSilent(
`title ${player.username} actionbar {"text":"§a+5 金币"}`
)
⚠️
title命令第一个参数必须是玩家名,且名字带空格时要引号包裹。KubeJS 里用player.username(不用双引号)最稳。 💡 玩家名太长/有特殊字符时,用execute as <player> run title @s title ...更安全。
// 更稳的写法:execute as 绕开名字解析
player.server.runCommandSilent(
`execute as ${player.username} at @s run title @s actionbar {"text":"§a+5 金币"}`
)
7. ⚠️ 漂浮的伤害数字(KubeJS 做不到)¶
「打怪冒出 +5」「暴击金色 -23」这类世界空间里飘起来再消失的数字,实现方式是:
- 原版:ParticleTypes.CRIT(暴击星)+ 客户端文字渲染,没有原生伤害数字
- 常见 mod:Damage Indicators / ToroHealth / Neat 这类客户端 mod(纯客户端安装,服务端不用管)
KubeJS 能做的极限: 1. 在原位置生成粒子(只能画星星/爱心,画不了数字):
EntityEvents.hurt(event => {
let e = event.entity
e.level.spawnParticles('minecraft:crit', e.x, e.y + 1.5, e.z, 0.2, 0.2, 0.2, 8, 0.1)
})
🎯 结论:想要真正的伤害飘字,装客户端 mod(Damage Indicators 系),别指望 KubeJS。这是渲染层的事,服务端脚本无解——别浪费时间。
8. 速查表¶
| 需求 | 一行写法 | 备注 |
|---|---|---|
| 世界固定文字 | summon minecraft:text_display x y z {text:'{...}',billboard:"center"} |
重启消失,需 §2 重建 |
| 多行 | 文本组件里 \n(脚本里写 \\n) |
或 line_width 自动折行 |
| 放大文字 | transformation:{left_rotation:{},right_rotation:{},scale:[2f,2f,2f],translation:[0f,0f,0f]} |
1.20.1 对象写法 |
| 隔墙可见 | see_through:1b |
地标/公告必加 |
| 文字背景 | background:0 / 1073741824(半透明黑) |
ARGB 十进制 |
| 跨维度生成 | execute in <dim> run summon ... |
否则跑到主世界 |
| 头顶称号 | player.displayName = Text.of([...]) |
每次 loggedIn 重设 |
| 侧边栏 | scoreboard objectives setdisplay sidebar <obj> |
最多 15 行,分值唯一 |
| 中央大标题 | title <玩家> title {"text":"..."} |
先 times 设时长 |
| 物品栏上方小字 | title <玩家> actionbar {"text":"..."} |
3 秒消失 |
| 伤害飘字 | ❌ KubeJS 做不到 → 客户端 mod | 粒子只能画星星 |
9. 常见报错与排查¶
| 现象 | 原因 | 修法 |
|---|---|---|
summon 报 Invalid NBT |
JSON 文本里的 " 没转义 / \n 少一层 |
外层用单引号,文本内 " → \\" |
| 文字不显示但实体存在 | 生成在别的维度 | 检查是否漏了 execute in |
| 文字飘在半空往下掉 | 浮空实体受重力?—— 展示实体无重力,多半是 y 坐标算错 |
用 execute at @p run summon ... ~ ~2 ~ 对齐 |
| 重启就没了 | 展示实体不写区块 | 用 §2 的 JSON 重建方案 |
| 叠了好几层 | 重建前没 kill 旧实体 |
统一 tag,先 kill 再 summon |
| 中文变问号 | 客户端字体缺字 / 服务端编码 | 用 UTF-8 保存脚本;1.20.1 自带中文字形 |
| 玩家头顶文字重进服消失 | displayName 不持久 |
在 PlayerEvents.loggedIn 里重设 |
本文为 KubeJS 1.20.1 中文文档原创篇目。API 依据 Minecraft 1.20.1 Forge 47.x 与 KubeJS 6.1,展示实体机制自 1.19.4 起稳定。