下载 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 做了大量无效/重
精华
82 赞
23 回复
如何提升你游戏中的UI美术水平截图
如何提升你游戏中的UI美术水平
一、请为你的游戏挑选一款合适的字体 很多开发者没有注意到其实UI界面中最重要的元素是字体,一款合适的字体能给整个游戏的界面感官带来蜕变。 请看案例 那么如何挑选适合自己的字体呢? 我总结出了一些小经验可供参考: 黑体:通用 宋体:古风、武侠、修仙 楷体:古风、武侠、修仙 圆体:卡通、动漫、Q版 科技:科幻、未来、电子 顺手分享一个免费可商用字体网站: 当然 如果你不知道自己的游戏适合什么风格
精华
126 赞
44 回复
01:29
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,截图
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,
视频内容只是一部分, 本游戏耗时3个月,目前完成进度百分之80, 是生存游戏,进入修仙界,走剧情找到宗门加入, 这时游戏才是真正开始, 提升实力境界后就可以变强,获取功法法术法宝, @视频内是游戏里挑战妖族入侵一种主流玩法,游戏就是围绕着这个来进行, 还有很多地方可以战斗,获得资源修炼变强,比如去藏书阁学习功法, 去法宝楼抽法宝,每一个宗门数十个法宝抽取 去修仙界探索,获得各自的资源,可能遇见妖兽
1 赞
3 回复
永恒虚空持续开发日记1——关于游戏中的新名词:场频截图
永恒虚空持续开发日记1——关于游戏中的新名词:场频
写在前边:首先感谢TapTap制造版主大大@汪崽 对于作品潜力的肯定,给与了我积分计划二的权限[表情_不好意思]。这边也在加紧开发改进各种功能中,期望尽快给大家带来成熟的版本。[表情_猫咪举手] 大家好,我是永恒虚空的制作人黑猫不详,大家叫我黑猫就好。从今天开始我将以后置开发日记的形式给大家分享我在TapTap制造中制作的游戏永恒虚空的开发过程,新人希望大家多多关照! 今天先给大家介绍我作品中最重
投票
3 赞
01:07
我给制造开发了一个半自动的开发工作流截图
我给制造开发了一个半自动的开发工作流
随着多模态模型识图能力增强,尝试一下AI 是否能自己修复bug,自己检查。 1,基于画面修复代码 2,点击TapTap制造 菜单,切换屏幕显示 3,截图验证 刚跑通一个小闭环,给大家分享一下 随着模型能力增强,Token费用走低,这种半自动开发是未来常态了
1 赞
2 回复
00:37
【积分申请】《最后的黎明》全球进化,游历全球,活下去!截图
【积分申请】《最后的黎明》全球进化,游历全球,活下去!
《最后的黎明》 是一款 2D 像素风、横版、末世开放探索 + 轻战斗的冒险游戏。参考《僵尸毁灭工程》的建筑搜刮与风险玩法,《死亡细胞》的战斗手感,《守墓人》《去月球》的叙事氛围。 世界观 灾难过后,文明陨落。 城市化为废墟,天空常年灰蒙,只有地平线尽头透出一线极淡的暖金色黎明。玩家扮演一名饱经沧桑的幸存者,破旧军绿外套、脏污绷带、单边护目镜、缠布的铁管武器,独自深入这座沉睡的城市,追寻那道光。 核
1 赞
1 回复
浅记《阿方的旅程》开发历程截图
浅记《阿方的旅程》开发历程
预计阅读时长10分钟,欢迎您先品鉴游戏,再回来看这篇“流水账” 这是我第一次实际接触“用自然语言制作游戏”,也是我第一次接触游戏开发工作,所以一切都是从零开始,跌跌撞撞,懵懵懂懂。[表情_豹哭] 我是通过获得的本次开发资格,6月22日晚,在微信公众号收到了测试资格的消息,最开始公布主题“跳跃”的时候,脑子里最先想到的就是几款非常经典的游戏「马里奥」「Flappy Bird」「Doodle Jump
10 赞
1 回复
2D 等轴测视角:完整理论截图
2D 等轴测视角:完整理论
2D 等轴测视角:完整理论 一、视角的本质 什么是等轴测 人站在高处斜着往下看地面,地面上的方格子不再是正方形,而变成了菱形。这就是等轴测的视觉本质。 俯视(正上方看下去): 等轴测(斜上方看下去): ┌──┬──┬──┐ ◇ ├──┼──┼──┤ ◇ ◇ ├──┼──┼──┤ ◇ ◇ ◇ └──┴──┴──┘ ◇ ◇ ◇ 正方形网格经过旋转 + 压缩,变成了菱形网格。 为什么是 2:1
6 赞
01:27
《大二暑假,我用AI做了一款属于自己的游戏》截图
《大二暑假,我用AI做了一款属于自己的游戏》
本人自制游戏现已加入taptap封闭测试(仅适配安卓机型)欢迎大家前来尝试(八月十号可以正式开始游玩) 如果有建议或者bug反馈可以加入qq群928501443 #发现好游戏 #游戏日常 #浅评一下 #今天游戏圈发生了啥
4 赞
1 回复
01:21
请都看过来~!!!截图
请都看过来~!!!
我是傻…… 很遗憾错的过了这次活动的报名,但刚好我的游戏也是在这个期间完成的,看了下大佬们的作品,感觉我做的还是太粗浅了。模型粗糙,UI也用的原始版本,很多地方都还需要打磨,可惜...这次赠送积分已经败光了。以后的积分大概只能先维护下游戏修复点bug,希望以后还能申请积分... 既然都这样了,我也想看看和大佬们的作品有多大的差距。 星三国/星三国纪元 (其实名字我都还没决定好) 游戏了一半灵感来自
2 赞
4 回复