跳转至

悬浮文字(全息显示 / 头顶名牌 / 计分板 / 伤害飘字)

原文:原创教程(ぴよまる),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)
})
2. 用 §6 的 ActionBar 显示在玩家屏幕下方(不是世界里的飘字)。

🎯 结论:想要真正的伤害飘字,装客户端 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 起稳定。