下载 App

UI按键动画设计

07/28

一、本次反复失败的根因(三个坑)

现象根因
按钮完全不可见(空白/小白点)入场动画把 scale/alpha 初始设 0,靠 Update 驱动 Tween 补到 1;但该模板页面级 Update 时序不可靠,补间不推进 → 永远停在 0
背景/按钮颜色反转用了 nvgRGBA(r,g,b,255) 整数版,出现通道映射异常
矩形在、文字消失文字色≈填充色;或 DrawStrokedText 描边盖住了填充
最核心的一条认知:NanoVGRender(即 _Render)是唯一每帧 100% 可靠的回调;Update 不可信。所有"每帧推进"的逻辑(动画、计时)必须放进 _Render。

二、三条铁律

  1. 静态可见:按钮构建时 scale=1, alpha=1,绝不用动画"让它出现"。动画只做 hover/press 增强。
  2. 帧内驱动:动画在 _Render 里用 os.clock() 算 dt + 逐帧 lerp;鼠标事件只写目标值,不直接改显示值。
  3. 浮点色:一律 nvgRGBAf(0..1);文字纯白配深色按钮,不用描边文字。

三、架构:双层状态 + 帧内 lerp

鼠标事件 ──只写目标──▶ AnimState{当前值, 目标值, 旋转相位}
_Render 每帧 ─os.clock()dt──▶ lerp(当前→目标) ──▶ nvgTranslate/Rotate/Scale 绘制
  • 分离 X/Y lerp 速度 = GDScript scale:x 0.2s / scale:y 0.35s(Y 慢→拉伸感)
  • 旋转两阶段(flick 甩出 → return 弹回)= GDScript 的 rotation→target 再 →0 set_delay
  • 随机方向、clamp(WIDTH_FULL_ROT/w,0.5,1.0) 尺寸自适应,原样移植

四、关键代码

状态 + 常量
lua
local WIDTH_FULL_ROT, LERP_SCALE_X, LERP_SCALE_Y, LERP_ROT, FLICK_DURATION = 300, 0.18, 0.10, 0.25, 0.1 local function lerp(a,b,t) return a+(b-a)*t end self._anim[i] = { scaleX=1,scaleY=1,rotation=0, targetScaleX=1,targetScaleY=1, rotPhase=0,rotTimer=0,rotImpulse=0 }
鼠标事件只改目标(悬停=缩放+随机旋转甩出)
lua
local ratio = math.max(0.5, math.min(1.0, WIDTH_FULL_ROT / btn.w)) local dir = (math.random() > 0.5) and 1 or -1 a.targetScaleX, a.targetScaleY = 1.0 + 0.2*ratio, 1.0 + 0.2*ratio a.rotPhase, a.rotTimer, a.rotImpulse = 1, 0, 5.0*ratio*dir -- 离开时设 1.0 / rotPhase=0
_Render 内逐帧推进(含 dt 钳制 + 目标回拉)
lua
local dt = os.clock() - self._lastClock; if dt > 0.1 then dt = 0.016 end; self._lastClock = os.clock() for _,a in ipairs(self._anim) do a.scaleX = lerp(a.scaleX, a.targetScaleX, LERP_SCALE_X) -- X 快 a.scaleY = lerp(a.scaleY, a.targetScaleY, LERP_SCALE_Y) -- Y 慢 if a.rotPhase==1 then a.rotation=lerp(a.rotation,a.rotImpulse,LERP_ROT); a.rotTimer=a.rotTimer+dt; if a.rotTimer>=FLICK_DURATION then a.rotPhase=2 end elseif a.rotPhase==2 then a.rotation=lerp(a.rotation,0,LERP_ROT); a.rotTimer=a.rotTimer+dt; if a.rotTimer>=FLICK_DURATION or math.abs(a.rotation)<0.1 then a.rotation,a.rotPhase=0,0 end elseif math.abs(a.rotation)>0.01 then a.rotation=lerp(a.rotation,0,LERP_ROT*0.5) else a.rotation=0 end end -- 弹跳(scale>1.15)结束后把目标拉回悬停值/1.0 for i,a in ipairs(self._anim) do if self.pressedIdx~=i and a.scaleX<1.15 then local st = self.hoveredIdx==i and 1.0+0.2*math.max(0.5,math.min(1.0,WIDTH_FULL_ROT/self.buttons[i].w)) or 1.0 a.targetScaleX, a.targetScaleY = st, st end end
绘制(中心 pivot + 浮点色 + 纯白文字)
lua
nvgSave(vg); nvgTranslate(vg,btn.cx,btn.cy); nvgRotate(vg,a.rotation*math.pi/180); nvgScale(vg,a.scaleX,a.scaleY) nvgBeginPath(vg); nvgRoundedRect(vg,-halfW,-halfH,btn.w,btn.h,28); nvgFillColor(vg,nvgRGBAf(r,g,b,1.0)); nvgFill(vg) nvgFillColor(vg,nvgRGBAf(1,1,1,1)); nvgText(vg,0,0,btn.label,nil) -- 纯白,不描边 nvgRestore(vg)

五、何时仍可用旧的自建补间引擎

  • 单文件 Demo(自己掌控 Start+Update 主循环,Update 一定跑)→ 自建 Tween 可用。
  • Boot/Router/EventBus 多页面模板(页面 Update 不一定每帧转发)→ 必须用本节 _Render 帧内方案。
一句话总结:可见性靠静态值,动画靠 _Render 帧内 lerp,颜色靠 nvgRGBAf——把"每帧一定执行"和"可能不执行"的逻辑彻底分开,是这次成功的根本。
【更新】增加了一些任务截图
【更新】增加了一些任务
我从故事便签墙中,看到了大多数玩家都在和野兽、疾病博弈中倒下 我这两天补充了一些任务,有关合成物品的为主 在此感谢大伙儿的积极反馈
1 赞
4 回复
WASM 崩溃
nvgCreateImage 在 WASM 环境下无法正确加载图片文件(中文路径/资源未打包进虚拟文件系统),返回看似正数但实际无效的纹理句柄。Lua 中 0/-1 都是 truthy,if 判断通过后把无效句柄传给 nvgImagePattern,C 层访问空指针 → WASM 内存越界。 彻底解决方案:纯矢量绘制,零文件 I/O 完全放弃图片加载,用 NanoVG 基础图元(圆/椭圆/描边)绘
官方
【更新】平台新版本发布游戏变成了“预约”截图
【更新】平台新版本发布游戏变成了“预约”
今天更新之后,一直奇怪怎么看不到“热度”,变成了“预约” 后面才知道,需要去这里改为 提供游玩 【更新】 1、新增便签墙功能,每一局都有小故事 2、增加了自定义头像
3 赞
1 回复
拖到合成目标才留位
错误原因 我在上一轮加回收功能时,为了"修位置回正"加了 else 分支:当牌既没拖到回收区、也没合成目标时,强制把牌弹回原位。这破坏了 Stacklands 的基本交互——拖到空地就该留在那里。 -- ❌ 错误版本(多余的 else 导致拖到空地也弹回) if inRecycle and not card.locked then ...回收溶解... elseif self._mergeTar
官方
理性值跨局持久化
1. 数据常量(RationalityData.lua) M.INITIAL_VALUE = 100 -- 新玩家初始理性值 M.MAX_VALUE = 100 -- 理论上限 M.REMEDY_MAX_VALUE = 90 -- 药剂恢复封顶 M.REMEDY_REQUIRED_MAX_VALUE = 89 -- 药剂可用阈值(理性≤89) M.REMEDY_COST = 3 -- 药剂价格(宝
官方
大厅校准模式
一、整体架构 校准系统由四层组成: 层 文件 职责 状态容器 Client.lua 保存 lobbyCalibrationMode_/Drag_/Slots_/RestorePending_/SaveStatus_、路径常量、管理员标记 拖拽 + UI ClientUILobby.lua 指针事件包装、归一化坐标、实时预览、设置面板 序列化 ClientStorage.lua 本地 pref 读写
官方
卡牌:市场调研
skill_market_research 当前仅完成了: 加入随机技能池; 可以在技能节点被选中; 有新绘制图标; 有 Tooltip 设计说明; 能作为技能徽章显示。 但“选择后显示实时人数,并允许一次改选”的玩法逻辑尚未接入。 代码证据 1. Tooltip 明确标记为“设计中” scripts/bullbear/Client.lua:830 当前内容仍是: 技能 · 侦察 + 改选 · 设
官方
闪跳问题
下面是这轮三个问题(音量滑块、拖动闪跳、选角页闪跳/白屏)的可复用经验与对应关键代码。代码均为磁盘最终落地版本。 经验一:拖动控件别在回调里重建整棵树 滑块/开关这类"边拖边回调"的控件,onChange 里只能原地改值,绝不能触发 BuildXxxUI() 整树重建——否则正在拖的滑块会被销毁,拖动中断。音量百分比标签也是同理,用闭包持有引用做原地 SetText。 scripts/bullbe
官方
牌面文字反复重新布局和渲染
调查结论 问题已查清:牌面文字本身没有异常,也不是 NanoVG 重复绘制。真正原因是每名玩家或 AI 投币后,客户端都会销毁并重新创建整个战斗 UI。 左右牌文字、牌框、顶部头像、底部状态栏都在同一棵 UI 树中。因此本来只需要更新底部投币槽,却连带让红框内的牌面文字反复重新布局和渲染,表现为“闪烁几下”。 直接证据 Client.BuildPlayingUI() 每次先销毁现有根节点: scr
官方
【牌】兴登堡凶兆
这张卡的主题很适合《友谊的小船》,但按原始数值直接落地会有一个关键问题:右牌严格优于左牌,四人局会趋向全员选右,博弈不成立。 先评估原方案 选择 未扎堆 同侧≥3人 结论 左:追高 -15万 -40万 始终比右牌更痛 右:抄底 -10万 -25万 任意人数下都更优 无论同侧人数是多少,右牌都少亏: 未扎堆:右牌少亏 5万 扎堆后:右牌少亏 15万 所以理性玩家没有选择左牌的理由;最终通常会四人全选
官方