如何让嗒啦啦实现杀戮尖塔风格的大卡悬浮浮窗
精华修改于04/1476 浏览开发心得
作者:浠涫的攻略机
为什么需要技艺大卡悬浮浮窗?
在卡牌类游戏中,玩家经常需要详细查看卡牌信息,但普通的 tooltip 往往不够用:
tooltip 太小:放不下详细描述、关键词释义、机制说明
视觉冲击力不足:普通的 tooltip 很难展示卡牌的美术设计
术语解释缺失:游戏特有术语(如"基调"、"叠奏"、"钉选"、"永恒")需要解释
无法展示完整效果:复杂技能需要完整的效果描述,而不只是一句话
在《节拍前夜》中,我们也遇到了同样的问题:技艺卡片有描述、有详情、有关键词、有术语,普通 tooltip 根本装不下!
解决方案
我们参考了《杀戮尖塔》的设计,做了一套大卡悬浮浮窗:

全屏半透明遮罩:点击背景任意位置关闭
原始卡牌等比放大:展示完整的卡牌视觉
左右术语释义面板:自动提取并解释文本中出现的术语
关键词高亮显示:用 DMPanel 美化术语标签
逐帧入场动画:防止首帧闪烁,体验更流畅
移动端和桌面端自适应:不同设备不同布局
核心功能与技术亮点

1. 全屏遮罩 + 一键关闭
-- 全屏半透明遮罩(点击关闭) overlay_ = UI.Panel { position = "absolute", left = 0, top = 0, width = "100%", height = "100%", zIndex = 9999, backgroundColor = { 0, 0, 0, 0 }, -- 初始全透明,逐帧渐入 justifyContent = "center", alignItems = "center", cursor = "pointer", onClick = function() SkillDetailModal.Close() end, children = { contentWidget }, }
技术要点:
zIndex = 9999:确保浮窗在最上层
backgroundColor 初始全透明,逐帧渐入:防止首帧闪烁
cursor = "pointer":鼠标悬停时变成手型,提示可点击
2. QueueOverlay 渲染顺序控制
-- 覆写 Render:通过 QueueOverlay 将整棵子树推迟到 overlay 渲染阶段, -- 确保视觉上在 Modal(也用 QueueOverlay)之后绘制,解决乐录等弹窗内打开时被遮挡的问题。 local origRender = overlay_.Render overlay_.Render = function(self, nvg) if not self:IsVisible() then return end UI.QueueOverlay(function(overlayNvg) origRender(self, overlayNvg) local renderList = self:GetRenderChildren() for i = 1, #renderList do UI.RenderWidgetSubtree(renderList[i], overlayNvg) end end) end -- 阻止正常树遍历渲染子节点(已由 QueueOverlay 处理) overlay_.CustomRenderChildren = function(self, nvg, renderFn) end
技术要点:
UI.QueueOverlay():把渲染推迟到 overlay 阶段
解决了在其他弹窗内打开时被遮挡的问题
与乐录、图鉴等弹窗完美配合
3. 逐帧入场动画(防止首帧闪烁)
-- 逐帧入场动画(手动插值,不用 transition,防止首帧闪烁) local ANIM_DUR = 0.22 -- 动画时长(秒) local OVERLAY_ALPHA_MAX = 160 -- 遮罩目标透明度 local CONTENT_SCALE_FROM = 0.85 local animTime = 0 local animDone = false -- 内容初始状态:缩小 + 透明(首帧就用插值值,不会闪末态) contentWidget:SetStyle({ scale = CONTENT_SCALE_FROM, opacity = 0 }) local origUpdate = overlay_.Update overlay_.Update = function(self, dt) if origUpdate then origUpdate(self, dt) end if animDone then return end animTime = animTime + dt local t = math.min(animTime / ANIM_DUR, 1.0) local ease = 1.0 - (1.0 - t) * (1.0 - t) -- easeOutQuad -- 遮罩背景渐入 local bgAlpha = math.floor(OVERLAY_ALPHA_MAX * ease + 0.5) self:SetStyle({ backgroundColor = { 0, 0, 0, bgAlpha } }) -- 内容缩放 + 透明度 local curScale = CONTENT_SCALE_FROM + (1.0 - CONTENT_SCALE_FROM) * ease contentWidget:SetStyle({ scale = curScale, opacity = ease }) if t >= 1.0 then animDone = true end end
技术要点:
手动插值,不用 transition:防止首帧闪烁
初始状态用插值值:首帧就显示正确的初始状态
easeOutQuad 缓动:动画更自然
遮罩和内容同步动画:体验更流畅
4. RGB 转 Hue 用于 DMPanel 色彩适配
--- RGB→Hue(0~360),用于术语标签根据颜色调整 DM 色相 ---@param r number 0~255 ---@param g number 0~255 ---@param b number 0~255 ---@return number hue 0~360 local function rgbToHue(r, g, b) r, g, b = r / 255, g / 255, b / 255 local max = math.max(r, g, b) local min = math.min(r, g, b) local d = max - min if d < 0.001 then return 278 end -- 灰色默认紫色 local h if max == r then h = ((g - b) / d) % 6 elseif max == g then h = (b - r) / d + 2 else h = (r - g) / d + 4 end h = h * 60 if h < 0 then h = h + 360 end return math.floor(h + 0.5) end
技术要点:
RGB 转 Hue:让术语标签的颜色和游戏术语颜色保持一致
灰色默认紫色:避免无色术语
0~360 范围:完美适配 DrawingMachine 的 hue 参数
5. 关键词自动提取和释义
--- 从技艺描述中提取所有可释义的词条 ---@param skill table 技艺定义 ---@return Widget[] 词条释义条目列表 local function buildTermEntries(skill) local entries = {} local seen = {} -- 1) 技艺自身的关键词(基调、叠奏、钉选、永恒) local keywords = SkillData.GetKeywords(skill.id) for _, kw in ipairs(keywords) do if not seen[kw] then seen[kw] = true local info = SkillData.LookupTerm(kw) if info then table.insert(entries, createTermEntry(kw, info)) end end end -- 2) 描述 + 详情文本中出现的游戏术语 local textPool = (skill.description or "") .. (skill.detail or "") for term, desc in pairs(SkillData.GameTermDict) do if not seen[term] and string.find(textPool, term, 1, true) then seen[term] = true table.insert(entries, createTermEntry(term, { text = desc, color = SkillData.GAME_TERM_COLOR, category = "term", })) end end -- 3) 描述中出现的属性关键词 for kw, _ in pairs(SkillData.KeywordDict) do if not seen[kw] and string.find(textPool, kw, 1, true) then seen[kw] = true local info = SkillData.LookupTerm(kw) if info then table.insert(entries, createTermEntry(kw, info)) end end end return entries end
技术要点:
三重提取:技艺自身关键词 + 描述中出现的游戏术语 + 描述中出现的属性关键词
seen 去重:避免重复显示同一术语
按优先级排序:技艺关键词在前,游戏术语在后
6. 移动端和桌面端自适应布局
if isMobile or #termEntries == 0 then -- 手机端 / 无术语:纵向布局(卡牌 + 下方术语) local colChildren = { cardContainer } if #termEntries > 0 then table.insert(colChildren, UI.Panel { width = scaledW, gap = 6, marginTop = 12, pointerEvents = "none", children = termEntries, }) end contentWidget = UI.ScrollView { maxHeight = "95%", scrollY = true, alignItems = "center", pointerEvents = "auto", children = { UI.Panel { alignItems = "center", paddingVertical = 16, pointerEvents = "none", children = colChildren, }, }, } else -- 桌面端:[左术语] [卡牌] [右术语] 横向三栏 local leftTerms, rightTerms = splitTermsLR(termEntries) local leftPanel = DMPanel { dmParams = DMData.styled("panel_dark", "frosted", { cornerRadius = 12, brightness = 10, saturation = 30, }), dmAlpha = 180, width = TERM_PANEL_W, gap = 8, padding = 10, justifyContent = "center", pointerEvents = "none", children = leftTerms, } -- ... 右面板类似 ... contentWidget = UI.Panel { flexDirection = "row", alignItems = "center", gap = 20, pointerEvents = "none", children = { leftPanel, cardContainer, rightPanel, }, } end
技术要点:
手机端:纵向布局,可滚动
桌面端:横向三栏,左右术语面板
无术语时:只显示卡牌
Settings.IsSmallScreen():检测设备类型
7. 术语交替分配到左右两列
--- 将词条交替分配到左右两列 ---@param entries Widget[] ---@return Widget[] left ---@return Widget[] right local function splitTermsLR(entries) local left, right = {}, {} for i, entry in ipairs(entries) do if i % 2 == 1 then table.insert(left, entry) else table.insert(right, entry) end end return left, right end
技术要点:
简单的奇偶分配:左列放奇数位,右列放偶数位
平衡左右面板高度:视觉更协调
怎么在你的游戏中实现?
作为萌新开发者,你不需要自己手写所有代码,直接让嗒啦啦(AI)帮你搞定!
第一步:让嗒啦啦帮你分析需求
在对话框里直接说:
嗒啦啦,帮我实现一个杀戮尖塔风格的技艺大卡悬浮浮窗,要求: 1. 全屏半透明遮罩,点击任意位置关闭 2. 原始卡牌等比放大显示 3. 左右术语释义面板(桌面端) 4. 移动端纵向布局(可滚动) 5. 逐帧入场动画,防止首帧闪烁 6. 自动提取并解释文本中出现的关键词和术语
第二步:让嗒啦啦帮你创建组件
然后说:
嗒啦啦,帮我创建一个 SkillDetailModal 组件,包含完整的 API: - SkillDetailModal.Open(skill, { onClose }) - SkillDetailModal.Close() - SkillDetailModal.IsOpen()
嗒啦啦会帮你创建完整的组件文件,包含:
全屏遮罩
卡牌放大
术语提取
自适应布局
入场动画
第三步:让嗒啦啦帮你集成到游戏中
接着说:
嗒啦啦,帮我在 RewardScreen、ShopScreen、CodexScreen 等显示卡牌的地方, 添加长按或点击打开 SkillDetailModal 的功能
这样一套完整的技艺大卡悬浮浮窗就搞定了!
实战应用:快速上手指南
1. 基本用法
-- 打开技艺详情浮窗 SkillDetailModal.Open(skill, { onClose = function() print("浮窗已关闭") end }) -- 关闭浮窗 SkillDetailModal.Close() -- 检查是否已打开 if SkillDetailModal.IsOpen() then print("浮窗正在显示") end
2. 在卡牌组件中集成
-- 在 SkillCardWidget 中添加长按打开详情 local card = SkillCardWidget.Create(skill, { mode = "reward", onLongPress = function() SkillDetailModal.Open(skill) end })
3. 在图鉴中集成
-- 在 CodexScreen 中点击技艺图标打开详情 for _, skill in ipairs(skills) do local icon = UI.Image({ image = skill.icon, width = 48, height = 48, onClick = function() SkillDetailModal.Open(skill) end }) end
我们的不足与改进空间
虽然这套浮窗功能解决了很多问题,但我们也有一些做得不够好的地方:
1. 术语提取依赖字符串匹配
问题:关键词和术语提取用的是 string.find 简单匹配,如果术语包含在其他词中可能误匹配
教训:可以考虑用更精确的匹配方式,比如分词或正则表达式
2. 没有键盘快捷键
问题:没有 ESC 键关闭浮窗的快捷键
教训:可以添加键盘支持,提升可访问性
3. 术语标签的 DMPanel 配置可以更灵活
问题:术语标签的 DMPanel 参数是硬编码的,不够灵活
教训:可以做成可配置的,适应不同游戏风格
最佳实践建议
1. 合理设计术语字典
提前规划好游戏术语:
SkillData.GameTermDict:游戏特有术语(如"跟拍"、"蓄力"、"奏"、"鸣")
SkillData.KeywordDict:属性关键词(如"基调"、"叠奏"、"钉选"、"永恒")
SkillData.GetKeywords():技艺自身的关键词
2. 用好 DrawingMachine 的 styled 组合
-- 三层组合:语义预设 + 技法 + 手动微调 DMPanel { dmParams = DMData.styled("panel_dark", "frosted", { hue = hue, brightness = 20, saturation = 50, cornerRadius = 10, }), dmAlpha = 230, -- ... }
3. 逐帧动画 vs transition
逐帧动画:适合需要精确控制首帧状态的场景(防止闪烁)
transition:适合简单的动画,代码更简洁
4. 移动端和桌面端分开设计
桌面端:空间充足,可以用横向三栏布局
移动端:空间有限,用纵向可滚动布局
Settings.IsSmallScreen():检测设备类型
5. QueueOverlay 解决渲染顺序问题
如果你的浮窗在其他弹窗内打开时被遮挡,试试 UI.QueueOverlay():
-- 通过 QueueOverlay 将整棵子树推迟到 overlay 渲染阶段 UI.QueueOverlay(function(overlayNvg) origRender(self, overlayNvg) local renderList = self:GetRenderChildren() for i = 1, #renderList do UI.RenderWidgetSubtree(renderList[i], overlayNvg) end end)
结语
《节拍前夜》的技艺大卡悬浮浮窗展示了如何在游戏中实现一套完整的卡牌详情展示方案。通过全屏遮罩、卡牌放大、术语释义、逐帧动画等技术,我们不仅解决了玩家查看卡牌详情的需求,还为游戏增添了视觉冲击力和专业感。
这套系统的核心价值在于:
提升用户体验:完整展示卡牌信息,术语自动解释
增强视觉效果:卡牌放大展示,动画流畅自然
跨平台适配:移动端和桌面端不同布局
虽然在术语匹配、快捷键支持等方面还有提升空间,但作为卡牌详情展示功能,它已经很好地满足了游戏需求。希望这个方案能为其他开发者提供参考,让你的卡牌游戏也能有《杀戮尖塔》那样的专业感!
技术栈:UrhoX、Lua、DrawingMachine
适用场景:卡牌详情展示、术语解释、悬浮浮窗
开发难度:中等



