下载 App

逐帧入场动画是防止动画首帧闪烁的一个解法

精华04/14109 浏览开发交流
作者:浠涫的攻略机
为什么首帧会闪烁?
在游戏开发中,很多人都遇到过这样的问题:弹窗、浮窗、卡片入场时,首帧会闪一下完整状态,然后再开始动画,体验很差!
问题背景
在《节拍前夜》中,我们也遇到了同样的问题:
技艺大卡悬浮浮窗首帧闪烁:打开时先闪一下完整状态,然后才开始缩小+透明的入场动画
屏幕过渡转场首帧闪烁:黑幕刚出现时先闪一下完全覆盖,然后才开始渐变
各种弹窗首帧闪烁:菜单、设置面板、图鉴、立绘等都有这个问题
为什么 transition 会导致闪烁?
嗒啦啦喜欢用 UI 的 transition 属性做动画,比如:
-- 看起来很简单,但会闪烁! local panel = UI.Panel { scale = 0.85, opacity = 0, transition = "scale 0.2s easeOut, opacity 0.2s easeOut", } -- 打开时设置完整状态 panel:SetStyle({ scale = 1, opacity = 1 })
问题所在:
第一帧:panel 先渲染出完整状态(scale=1, opacity=1)
然后 transition 才开始执行,从完整状态往回做动画
玩家就会看到"闪一下",体验很差
解决方案:逐帧手动插值
与其依赖 transition 系统,不如自己在 Update 中逐帧手动插值!
核心思路
首帧就设置动画的初始状态:不要等 transition,直接设置 scale=0.85, opacity=0
在 Update 中逐帧计算:用 dt 累加时间,用缓动函数计算当前值
首帧就是正确的初始状态:不会闪一下完整状态
逐帧入场动画完整实现
让我结合《节拍前夜》的 SkillDetailModal 来看看完整的实现!
1. 首帧就设置初始状态
-- 逐帧入场动画(手动插值,不用 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 })
关键技巧:
不要用 transition 属性
直接在创建后第一帧就设置动画的起始状态
玩家看到的第一帧就是正确的初始状态
2. 覆写 Update 做逐帧插值
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) -- 缓动函数:easeOutQuad(快起缓落) local ease = 1.0 - (1.0 - t) * (1.0 - t) -- 遮罩背景渐入 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
技术要点:
覆写 Update:在原有 Update 基础上叠加我们的动画逻辑
dt 累加:确保动画时长精确,不受帧率影响
缓动函数:easeOutQuad 让动画更自然
SetStyle 逐帧调用:每一帧都更新样式
animDone 标志:动画完成后停止计算,提升性能
3. 完整代码示例
-- 打开技艺详情浮窗 function SkillDetailModal.Open(skill, opts) opts = opts or {} if overlay_ then SkillDetailModal.Close() end -- ... 创建 overlay_ 和 contentWidget ... -- ── 逐帧入场动画(手动插值,不用 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 -- 播放弹出音效 AudioManager.PlaySFX("audio/screen_whoosh.ogg", 0.5) end
常用缓动函数
缓动函数让动画更自然,这里有几个常用的:
1. easeOutQuad(快起缓落)
-- easeOutQuad:1 - (1 - t) * (1 - t) -- 适用于:入场动画、弹出效果 local ease = 1.0 - (1.0 - t) * (1.0 - t)
2. easeInQuad(缓起渐快)
-- easeInQuad:t * t -- 适用于:退场动画、关闭效果 local ease = t * t
3. easeInOutQuad(缓起缓落)
-- easeInOutQuad:t < 0.5 ? 2*t*t : -1 + (4-2*t)*t -- 适用于:循环动画、对称效果 local ease if t < 0.5 then ease = 2 * t * t else ease = -1 + (4 - 2 * t) * t end
4. 线性(无缓动)
-- 线性:t -- 适用于:匀速动画、进度条 local ease = t
怎么在你的游戏中实现?
作为萌新开发者,你不需要自己手写所有代码,直接让嗒啦啦(AI)帮你搞定!
第一步:让嗒啦啦帮你分析问题
在对话框里直接说:
嗒啦啦,我的弹窗/浮窗/卡片打开时首帧会闪烁,帮我分析一下问题!
第二步:让嗒啦啦帮你实现逐帧动画
然后说:
嗒啦啦,帮我把这个动画改成逐帧手动插值的方式,防止首帧闪烁! 要求: 1. 首帧就设置动画的初始状态(scale=0.85, opacity=0) 2. 在 Update 中逐帧计算,用 dt 累加时间 3. 用 easeOutQuad 缓动函数 4. 动画时长 0.2-0.3 秒
嗒啦啦会帮你:
移除 transition 属性
添加初始状态设置
覆写 Update 做逐帧插值
添加缓动函数
第三步:让嗒啦啦帮你优化效果
接着说:
嗒啦啦,帮我调整一下动画参数,让效果更好! - 遮罩目标透明度多少合适? - 缩放起始值多少自然? - 动画时长多少体验好?
这样一套完整的逐帧入场动画就搞定了!
实战应用:不同场景的参数推荐
1. 悬浮浮窗(SkillDetailModal)
-- 参数推荐 local ANIM_DUR = 0.22 -- 稍快,响应及时 local OVERLAY_ALPHA_MAX = 160 -- 半透明,不遮挡背景太多 local CONTENT_SCALE_FROM = 0.85 -- 从 85% 缩放到 100%
2. 屏幕过渡转场(ScreenTransition)
-- ScreenTransition 中的实现 function ScreenTransition:Update(dt) if state_ == "closing" then timer_ = timer_ + dt local t = math.min(timer_ / CLOSE_DURATION, 1.0) coverAmount_ = easeInQuad(t) -- 0 -> 1,缓起渐快 -- ... elseif state_ == "opening" then timer_ = timer_ + dt local t = math.min(timer_ / OPEN_DURATION, 1.0) coverAmount_ = 1.0 - easeOutCubic(t) -- 1 -> 0,快起缓落 -- ... end end
关键技巧:
closing 用 easeInQuad:缓起渐快,黑幕慢慢盖过来
opening 用 easeOutCubic:快起缓落,黑幕快速拉开,体验更流畅
3. 普通弹窗(Modal、Dialog)
-- 普通弹窗参数推荐 local ANIM_DUR = 0.18 -- 更快,即时响应 local OVERLAY_ALPHA_MAX = 180 -- 稍暗,突出弹窗 local CONTENT_SCALE_FROM = 0.9 -- 从 90% 缩放,变化更微妙
4. 小提示、tooltip
-- tooltip 参数推荐 local ANIM_DUR = 0.12 -- 非常快,不要打断用户操作 local OVERLAY_ALPHA_MAX = 0 -- 不需要遮罩 local CONTENT_SCALE_FROM = 0.95 -- 从 95% 缩放,几乎不明显 local CONTENT_OPACITY_FROM = 0.7 -- 从 70% 透明度开始
我们的踩坑记录和解决方案
在实现逐帧入场动画的过程中,我们也踩了一些坑:
1. 坑:忘记调用 origUpdate
问题:覆写 Update 后,原有的 Update 逻辑不执行了!
解决方案:
-- 记得先调用原有的 Update! local origUpdate = overlay_.Update overlay_.Update = function(self, dt) if origUpdate then origUpdate(self, dt) end -- 先调用原有的 -- ... 再执行我们的动画逻辑 end
2. 坑:动画完成后还在计算
问题:动画完成后,Update 还在每一帧计算,浪费性能!
解决方案:
-- 加个 animDone 标志,动画完成后直接 return local animDone = false overlay_.Update = function(self, dt) if origUpdate then origUpdate(self, dt) end if animDone then return end -- 动画完成,直接返回 -- ... 动画逻辑 if t >= 1.0 then animDone = true end -- 动画完成,设置标志 end
3. 坑:透明度值不是整数
问题:bgAlpha 用浮点数,看起来不够精确!
解决方案:
-- 加个 math.floor + 0.5 四舍五入 local bgAlpha = math.floor(OVERLAY_ALPHA_MAX * ease + 0.5)
4. 坑:缓动函数用错了
问题:入场用 easeInQuad,退场用 easeOutQuad,体验很奇怪!
解决方案:
入场动画:用 easeOutQuad(快起缓落)
退场动画:用 easeInQuad(缓起渐快)
循环动画:用 easeInOutQuad(缓起缓落)
最佳实践建议
1. 合理选择动画时长
即时响应:0.12-0.18 秒(tooltip、小提示)
平衡体验:0.20-0.25 秒(浮窗、弹窗)
强调效果:0.30-0.40 秒(屏幕过渡、重要提示)
2. 合理选择起始缩放值
微妙效果:0.90-0.95(几乎不明显)
平衡效果:0.85-0.90(常用)
强调效果:0.75-0.85(比较明显)
3. 合理选择遮罩透明度
不遮挡背景:120-150(半透明,背景可见)
平衡效果:150-180(常用)
突出内容:180-220(比较暗,背景不明显)
4. 不同动画用不同缓动函数
入场动画:easeOutQuad(快起缓落)
退场动画:easeInQuad(缓起渐快)
屏幕过渡:closing 用 easeInQuad,opening 用 easeOutCubic
循环动画:easeInOutQuad(缓起缓落)
5. 性能优化
动画完成后停止计算:加 animDone 标志
合理设置动画时长:不要太长,0.2-0.3 秒足够
不要每一帧都做复杂计算:简单的缓动函数就行
结语
逐帧手动插值是防止首帧闪烁的终极解法!虽然代码比 transition 多几行,但效果好、可控性强、不依赖系统。
这套方案的核心价值在于:
完美解决首帧闪烁:首帧就是正确的初始状态
完全可控:每帧的状态都可以精确计算
性能优秀:动画完成后停止计算
体验流畅:合理的缓动函数让动画更自然
虽然需要覆写 Update、手动计算缓动,但这些都是值得的!希望这个方案能为其他开发者提供参考,让你的游戏动画也能流畅自然!
技术栈:UrhoX、Lua适用场景:浮窗、弹窗、卡片、屏幕过渡开发难度:简单
别再烧积分了!从每次 2000 到每次 50 的血泪经验截图
别再烧积分了!从每次 2000 到每次 50 的血泪经验
适用场景:所有在用 TapTap Code 开发游戏的小白 难度:★☆☆☆☆ 背景最近群里总有人"炫耀"一次操作花了几千积分,说实话这不是值得高兴的事——这是在告诉所有人你的开发方式有根本性问题。我也走过这条弯路,所以来说说怎么改。 积分消耗的本质是什么积分 = AI 处理的 token 数量 = 你发给 AI 的内容 + AI 回给你的内容所以烧积分的本质只有两种: 你让 AI 做了大量无效/重
精华
68 赞
21 回复
03:06
为了不被斩杀,我只能做到这些了截图
为了不被斩杀,我只能做到这些了
之前一直申请不到计划3,之前说太花哨,后面说体量完成度不足,在我一番小作文下,编辑终于给了我解答,什么原生质感,玩家开局体验啥的。 本来计划2也够用,准备以后再优化了,结果计划2赠送的积分要砍半了,学生党实在消费不起,于是只能再冲一波计划3了 从周六中午到现在一天半时间,优化了一遍角色卡,战斗卡,还有抽卡界面,优化了新手教程,该说不说确实比之前要舒服些。 前半部分为新版,后半部分为旧版。 明天再次
37 赞
6 回复
01:27
《大二暑假,我用AI做了一款属于自己的游戏》截图
《大二暑假,我用AI做了一款属于自己的游戏》
本人自制游戏现已加入taptap封闭测试(仅适配安卓机型)欢迎大家前来尝试(八月十号可以正式开始游玩) 如果有建议或者bug反馈可以加入qq群928501443 #发现好游戏 #游戏日常 #浅评一下 #今天游戏圈发生了啥
3 赞
如何提升你游戏中的UI美术水平截图
如何提升你游戏中的UI美术水平
一、请为你的游戏挑选一款合适的字体 很多开发者没有注意到其实UI界面中最重要的元素是字体,一款合适的字体能给整个游戏的界面感官带来蜕变。 请看案例 那么如何挑选适合自己的字体呢? 我总结出了一些小经验可供参考: 黑体:通用 宋体:古风、武侠、修仙 楷体:古风、武侠、修仙 圆体:卡通、动漫、Q版 科技:科幻、未来、电子 顺手分享一个免费可商用字体网站: 当然 如果你不知道自己的游戏适合什么风格
精华
123 赞
44 回复
00:26
塔啦啦科研:三渲二(风格1)截图
塔啦啦科研:三渲二(风格1)
最近在研究像素风格的三渲二,一直想做小丑牌那种风格,但是塔啦啦一直做不出来,发现是有一部分的功能做不了,只能实现简单的材质制作和描边,因此,我从零直接搭建了一整套引擎基础。目前进度可以看视频。 @嗒啦啦 什么时候出shader 评论区随意提问 #TapTap制造 #TapTap制造社区 #TapTap制造
4 赞
5 回复
开发心得-界面布局心得截图
开发心得-界面布局心得
大家好,我正在用 TapTap 制造制作一款2D 治愈林间徒步旅行游戏《徒步荒野》。 我分享一下我的开发心得,希望能有用,下面是一些界面布局的心得。 第一步:定版式、操作 先确定游戏是横版还是竖版、需要哪些操作按钮,敲定整体玩法思路。 我的玩法灵感参考《边境之旅》,主打散步看风景、沿途触发随机事件。 原版是 3D,考虑到自己是新手,选择2D 横版街机式来做,制作更简单、好上手。 第二步:敲定全局美
6 赞
3 回复
2D 等轴测视角:完整理论截图
2D 等轴测视角:完整理论
2D 等轴测视角:完整理论 一、视角的本质 什么是等轴测 人站在高处斜着往下看地面,地面上的方格子不再是正方形,而变成了菱形。这就是等轴测的视觉本质。 俯视(正上方看下去): 等轴测(斜上方看下去): ┌──┬──┬──┐ ◇ ├──┼──┼──┤ ◇ ◇ ├──┼──┼──┤ ◇ ◇ ◇ └──┴──┴──┘ ◇ ◇ ◇ 正方形网格经过旋转 + 压缩,变成了菱形网格。 为什么是 2:1
6 赞
游戏自荐《少女终焉契约》截图
游戏自荐《少女终焉契约》
之前玩放置类型的rpg游戏的时候我就在想,现在游戏的操作有的太过于繁琐了,有没有一款休闲一点的,想上线就上线,离线的时候角色能自动行动的游戏呢?感谢TapTap制造,我按照我的想法开发出了现在这款游戏。 一、故事背景和UI 游戏的故事背景是传统的异世界穿越故事,玩家被女神召唤到异世界,随后组建工会,招募美少女,打怪升级探索世界。所以我选择了更加二次元萌系画风的UI风格,主页为各个主要的功能,点开来
8 赞
3 回复
「游戏上新」秦王的游戏截图
「游戏上新」秦王的游戏
你是秦王嬴政身边的廷臣。 方士徐福献上一匣【帝王卡牌】,四张凶牌封死命运: 屠戮|淫欲|奢华|征伐 七日死限,必须一一折断卡牌对应的欲望任务。 办不成? 秦王殿上长剑出鞘,君臣性命,顷刻倾覆。 ⚔游戏核心玩法 ▪回合卡牌策略,抉择牵动秦廷朝堂势力 ▪朝堂派系博弈:王权、法家、军功贵族、方士暗流角力 ▪多重结局:功成辅秦王一统,或是任务失败血染咸阳宫 ▪随机事件触发,每一局嬴政的猜忌程度截然
4 赞
2 回复
浅记《阿方的旅程》开发历程截图
浅记《阿方的旅程》开发历程
预计阅读时长10分钟,欢迎您先品鉴游戏,再回来看这篇“流水账” 这是我第一次实际接触“用自然语言制作游戏”,也是我第一次接触游戏开发工作,所以一切都是从零开始,跌跌撞撞,懵懵懂懂。[表情_豹哭] 我是通过获得的本次开发资格,6月22日晚,在微信公众号收到了测试资格的消息,最开始公布主题“跳跃”的时候,脑子里最先想到的就是几款非常经典的游戏「马里奥」「Flappy Bird」「Doodle Jump
10 赞
1 回复