原动画、动效与时间契约
研究日期:2026-10-09。本文只解析本地原包、原 Laya 引擎和静态解码源码;动画时长是资源声明或代码参数,不是录像测得的耗时。 原件复用应同时保留图像、atlas 帧顺序、pivot、骨骼、关键帧、时间源、Tween 和调用循环参数,不能只保留最终静态截图。
可复核的资源清单
- animation-index.json:18 个文件中 25 条 JSON timeline(含
.ani与Prefabs/BuffUI.json),全部声明 24 fps。 - skeleton-animation-index.json:42 个
.sk文件的版本、动画名、毫秒时长、节点/关键帧计数、轨道与 SHA256,全部完成静态解析。 - effect-atlas-index.json:8 个 effect atlas,记录原始帧列表顺序与默认节奏。
- inspect-animations.py:按包内引擎 binary schema 读
.sk,不启动游戏,不执行 SDK。
原 effect 子包已在原首局实际加载后补提取;当前目录 res/effect 包含全部 8 个 atlas 与各自 PNG。它们之前未缓存的缺口已经补齐;是否每种模式动态加载到位仍需运行验证。
源码证据:原 artifacts/original/unpacked/game.js 的 libs/min/laya.core.min.js、libs/min/laya.ani.min.js define 模块;方便阅读的静态格式化副本 laya.core.module.formatted.js、laya.ani.module.formatted.js。游戏调用证据均来自 bundle.decoded.js 的 js/bundle.js,字符偏移基于该解码文件;格式化副本 可按方法名搜索。
.ani timeline 的帧语义
引擎 _setUp 把 interval 设置为 1000/frameRate(core 30059);_calculateDatas 扫描最高关键帧 index 后再加 1(30125–30140),_createGraphicData 逐帧建立 Graphics(32639–32661)。因此 index=15 的资源包含 0…15 共 16 帧;默认循环周期为 16/24秒,不是 15/24秒。最后一个关键帧到达时间仍为 15/24秒,这两个量必须区分。
JSON 的 action:0 不能单独证明循环与否。循环取决于调用 play(start,loop,name);Laya Animation.play 默认 loop=true(core 33105–33122),游戏可显式传 false,在 COMPLETE 后回收对象。资源的 tween:true/false、tweenMethod、alpha/scale/rotation/pivot 和 skin 换帧共同定义视觉。静态索引列出全部通道,以原 JSON 为准。
| 原资源(均在 gameAssets/asset/ 下) | 动画名 | 最高 index | 帧数 | 24 fps 名义周期 |
|---|---|---|---|---|
| EffectAni/AddHpSkillAnim.ani | ani1 | 15 | 16 | 666.667 ms |
| EffectAni/DarkSilkwormAni.ani | move | 10 | 11 | 458.333 ms |
| EffectAni/DarkSilkwormAni.ani | attack | 10 | 11 | 458.333 ms |
| EffectAni/DustbinFull.ani | ani1 | 6 | 7 | 291.667 ms |
| EffectAni/VirtualShadowAnim.ani | old | 10 | 11 | 458.333 ms |
| EffectAni/VirtualShadowAnim.ani | new | 10 | 11 | 458.333 ms |
| EffectAni/boxgodeffect.ani | ani1 | 17 | 18 | 750.000 ms |
| EffectAni/princessmirrorEffect.ani | ani1 | 135 | 136 | 5666.667 ms |
| Prefabs/BuffUI.json | ani1 | 80 | 81 | 3375.000 ms |
| devil1.ani | ani1 | 8 | 9 | 375.000 ms |
| diceAni.ani | ani1 | 15 | 16 | 666.667 ms |
| diceAni2.ani | ani1 | 15 | 16 | 666.667 ms |
| diceAni3.ani | ani1 | 15 | 16 | 666.667 ms |
| diceAni4.ani | ani1 | 15 | 16 | 666.667 ms |
| haloStar.ani | ani1 | 10 | 11 | 458.333 ms |
| meowAni.ani | ani1 | 15 | 16 | 666.667 ms |
| speed.ani | ani1 | 4 | 5 | 208.333 ms |
| stcAnim.ani | dd | 24 | 25 | 1041.667 ms |
| stcAnim.ani | open | 26 | 27 | 1125.000 ms |
| stcAnim.ani | openLoop | 2 | 3 | 125.000 ms |
| stcAnim.ani | people | 12 | 13 | 541.667 ms |
| tcAnim.ani | dd | 24 | 25 | 1041.667 ms |
| tcAnim.ani | open | 26 | 27 | 1125.000 ms |
| tcAnim.ani | openLoop | 26 | 27 | 1125.000 ms |
| trail.ani | ani1 | 57 | 58 | 2416.667 ms |
例如 speed.ani 在 index0/2/4 切换皮肤,pivot=(81,199),不能把它等同于匀速缩放。trail.ani 对多个 GraphicNode 进行位置/透明度变化;princessmirrorEffect.ani index135 是长达 136 帧的时间线。原引擎可直接解析这些 JSON,无需再造 CSS keyframes。
骨骼 .sk:名称和时长可静态读取
所有 42 个文件头是 LAYAANIMATION:1.7.0,动画类 DragonBone。原引擎 _onAsynLoaded 对除 LAYAANIMATION:02 外的版本调用 Parser01(ani 747–755)。Parser01(31–151)依次读取 UTF 版本/类/字符串表,animationCount、publicDataOffset、extensionOffset、useParent;每个动画读 name 索引、Float32 playTime、nodeCount,随后按 publicData 的 keyframeWidth 跳过各轨道数据。报告工具完全按此布局读取;不把从二进制中随便找到的字符串当作动画名。
getAniDuration 返回 playTime(ani 770–772);Skeleton 内也用 duration/1000 参与时间换算(3474)。所以本清单的单位是毫秒。骨骼采样/缓存频率、节点插值和状态中 playbackRate 仍由引擎和调用方决定,文件 playTime 不是最终可见动作耗时。
| 文件 | 动画及原 playTime |
|---|---|
| Skeleton/player/player_0.sk | attack 1066.667;idle 2000;walk 1300;walk5 833.333 ms |
| Skeleton/player/player_1.sk | attack 1000;idle/idle2/idle3 2000;walk/walk2 1233.333;walk5 766.667 ms |
| Skeleton/player/player_2.sk | attack 1000;idle/idle3 2000;walk 1466.667;walk3 1466.7;walk5 566.667 ms |
| Skeleton/player/player_3.sk | attack 2000;idle 2733.333;walk/walk3 1133.333;walk5 833.333 ms |
| Skeleton/player/player_4.sk | attack 1333.333;idle/idle2 2000;idle3/idle4 1000;walk 2000;walk5 833.333 ms |
| Skeleton/player/player_5.sk | attack 1000;idle/idle4 2033.333;idle3 2000;walk 2166.667;walk5 833.333 ms |
| Skeleton/StrongerTroll/mg_0.sk | daiji/gongji/zoulu 各 2000 ms |
| Skeleton/StrongerTroll/mg_1.sk | animation 0;gongji/idle/walk 各 2000 ms |
| Skeleton/StrongerTroll/mg_3.sk | daiji 2000;gongji/walk 1000 ms |
| Skeleton/StrongerTroll/mg_4.sk | daiji/zoulu 2000;gongji 3000 ms |
| Skeleton/StrongerTroll/mg_6.sk | daji/gongji/zoulu 900;shouji 100;siwang 500 ms |
| Skeleton/muscleBadger/mihuan.sk | gongji 433.333;gongji2 333.333;pao 666.667;shuizhao 1333.333 ms |
| Skeleton/Compound/dustbinOut.sk | animation 200;animation2 366.667 ms |
| Skeleton/combatDoor/skeleton.sk | animation 1900;smbf 2033.333 ms |
来源为表列原 .sk,精确值及其轨道在 skeleton-animation-index.json。res/mg/mg_*.sk 与 res/Skeleton/StrongerTroll/mg_*.sk 是不同资源,例如 res/mg/mg_0 多有 zouluzhuqingting、zuoshanxian。不能按相同 mg_0 文件名互换;mounts 的 idle_0…5/walk_0…5 也对应各角色坐骑姿态。
明确的速率覆盖:玩家速度技能调用 _player.playbackRate(5),恢复调用 rate(1)(bundle 39476、39019);坐骑展示存在 rate(3)(347256)。某些怪物攻击 play(1,false) 并由逻辑计时决定再次出手(解码字符873800附近)。因此 2秒骨骼片段不代表2秒攻击间隔。
atlas 逐帧效果
Hp.vgg 创建 Laya.Animation、loadAtlas,对应路径 gameAssets/asset/res/effect/{name}.atlas;效果第一次加载后计算 getGraphicBounds,按 bounds/2 设置 pivot。Hp.ncm 将效果加到场景,普通 zOrder=pos.y;ahu 标记置 Number.MAX_VALUE。暴气 trollBaoqiEffect 缩放 2.67,并把目标位置减去 (80,80)(解码字符191030)。这些定位/层级/缩放是原视觉契约。
Laya 默认 animationInterval=50ms(core 行16),Hp 没有修改 interval,所以未被其它调用修改的这些新实例是 20 fps,和 .ani 的24 fps不同。
| 效果 | 原帧数 | 默认名义播放时长 |
|---|---|---|
| bulletEffect | 8 | 400 ms |
| trollAttackEffect | 3 | 150 ms |
| trollBaoqiEffect | 12 | 600 ms |
| trollZH | 7 | 350 ms |
| buildDownEffect | 7 | 350 ms |
| build_vertigo | 5 | 250 ms |
| flowerAttack | 5 | 250 ms |
| bomb | 6 | 300 ms |
Hp 默认 loop=0,调用 play(0,false),COMPLETE 后回收到对象池并 removeChild/offAll;非0 loop 参数走 play(0,true) 并在指定 loop 毫秒后移除停止(字符191030–192100)。它不是 JSON timeline 的循环标志。原 atlas 也保留裁切 frame/spriteSourceSize/sourceSize;渲染应让 Laya 执行 atlas,不能将透明裁切帧拉伸成相同 CSS 方框。
按钮与数值反馈 Tween
来源:bundle CM 的 ahk/ftt/xtl/gua/cxe/gzp,解码字符314604、320434、328468、302262、325606、306848;所有时长为传给 Laya.Tween 的毫秒参数。
| 交互 | 原参数 | 校准意义 |
|---|---|---|
| 按下按钮 ftt | scaleX/scaleY→原scale×0.9,50ms | 点击反馈不能只切换贴图 |
| 松开/移出 xtl/gua | scaleX/scaleY→原scale,50ms | 移出恢复也要执行;以绑定时原scale为基准 |
| 对象 cxe 弹跳 | 1→1+i,50ms;→1−2i,50ms;→1,50ms | 总150ms,幅度由调用参数决定,常见炮台i=.2 |
| Show | visible=true,scale→1,200ms | 显示时的展开反馈 |
| Alpha | alpha→目标,1000×传入秒数 | 是可参数化工具,不应固定所有对象1秒 |
| +金币 gzp | 初始alpha/scale=0;500ms→alpha1/scale e、上浮15;1000ms再上浮45;1000ms上浮45且淡出缩为0 | 连续2500ms,结束回对象池;持续收入可叠多条浮动数字 |
| 普通炮台枪口 udi | alpha→1 20ms,→0 20ms,回收 | 总40ms,5秒抽帧看不到它并不代表没有 |
gzp 在UI文案显示前把64变65,也保留原浮字白色、+符号、层级和生成位置。所谓「升级动效」并非只有一个资源:扣款/换等级贴图、cxe、buildDownEffect、声音和菜单变化可以共同发生;应以原调用链为准。
炮台、炮弹与受击节奏
普通炮台一条代码路径(解码字符431439–445000,格式化34266–34680)初始化 uzd=1、yyp=0,但 uzd getter 是 1/(zpf+1+ulj),所以该普通路径基础出手检查间隔为 0.5秒。计时 Lx.rvy=mxj/1000,ubs(t,interval) 判断 interval<=rvy−t(字符245127、246672),单位是秒。高级/特殊炮台有不同 getter:基础 1/(zpf+yyp),rapid+3、morerapid+3或5、angel+1.5、crazy+5(字符953157),不可把全部炮台统一写成0.5秒。
普通炮台 attack→nad对象池→rfl 初始化90×90炮弹、pivot=(45,45)、scale=.2、ytl=500;jsx 先对准目标,再 G_.pqq(node,target,180,ytl),并 afj(node,1,1,.35)(字符437766、439101)。高级炮台另一条 rfl 设 ytl=900,cannon尺寸135、giantcannon171(952270)。
速度语义已查明: pqq 第3参保存 pvl(转向速度),第4参保存 suc(位移速度);G_.pgu 每更新用 suc/60 位移、pvl/60 转角(2334641)。普通弹500→每更新约8.333地图单位,高级弹900→15;180是转向,不是弹速。位移按 abs(dx)/(abs(dx)+abs(dy)) 与 y 分量分配,使用曼哈顿归一化;不能换成欧氏归一化而声称同轨迹。靠近目标两轴都<5时隐藏,击中检查另用平方距离<400,即20单位范围(普通炮台 onUpdate 字符435000附近)。
该移动器按 /60 的更新步长工作,最终每秒速度还依赖原更新调度。网页省电/后台降帧后不能自动声称500单位/实际秒保持不变;直接复用原更新器能保留其行为,若做dt修正会改变原契约,应单独证明。
门、怪物血条与离场
BuildBloodStrip.json 根90×20、pivot=(45,10);槽UI/HP.png为90×20、sizeGrid=3,9,6,7;内容UI/BuildHP.png为86×16、x2/y11、pivotY10、sizeGrid=2,10,2,10。门 yuj setter直接设置内容 scaleX=生命比例(字符560300–561450)。按门旋转0/90/180/270度,血条分别放(45,10)/(10,45)/(45,80)/(80,45),血条反转自身rotation保持水平;这条路径没有补间掉血动画。
TrollBloodStrip.json 同尺寸但红色TrollHP,有LV.标签、font20、描边1。怪物 yuj setter即时改实际 nlk 红条到比例,同时把 mah追随层 scaleX Tween到该比例,800ms(字符656500–658150);这不是门的反馈。原怪物方向翻转和血条层级也要保留。
标准怪物逻辑默认低血逃跑阈值 ydr=.3;初始化会从配置读取 zac,原配置ydr=[.3,.3,.3],重新选择/逃跑概率zde=[.5,.4,.3](字符870300、887946、2405100)。攻击中每 hfe 秒考虑改目标/逃跑;普通默认level<5时10秒、>=5时20秒,阶梯难度可覆盖 escapeAfterLev/escapeThinkingTime(字符895500附近)。gjt 在未逃跑且 hp<=ydr 调 arb;arb设置escape状态,寻路到guv位置,切移动/飞行动画(893954)。
逃到目标 btk开始回血阶段;sjx每1秒调用ghr,Normal按addHpSpeed回,Plot地图11每次+.2×最大血,其余+.1×最大血,满血后清逃跑标记、重新找目标(873439、888480)。Normal的addHpSpeed需要继续追配置才能给精确固定值;不能把所有模式写成「每秒10%」。
原骨骼mg_6带shouji100ms和siwang500ms,但这里只证明资源片段存在,尚未证明当前模式在每次受击/离场都播放这两段。不能把资源名存在写成实机必然触发。
录像时间边界与验收
当前 01-navigation/index.json 有51张导航帧,第一帧源时间0、最后362.126667秒;相邻最小间隔5秒,最大跳跃116.7秒(215.371667→332.071667)。这些数值来自索引记录,说明保留了原视频时间且存在空档,不能按文件序号×5给全部帧补时间。原MOV总时长364秒由录制侧报告,本文未独立量测MOV容器。
该稀疏导航序列只适合找阶段与关键状态,不足以测40/50/150/350/800ms动效。25–67秒的10fps开场抽帧正在由主代理生成;即使10fps也无法可靠判定40ms枪口闪现。后续若做像素/节奏验收,应从高帧率原录像选择同一源时刻,不要用两个不同时相截图计算视觉误差。
可校准顺序:同设备比例下固定原舞台640设计宽 → 对齐原存档/角色/模式 → 对齐资源版本/atlas/pivot → 对齐动画名与loop/playbackRate → 对齐同一个关键帧/出手/受击时刻 → 最后对比像素。静态原件复用能保留动作轨迹与美术细节;本报告不宣称已达到所有场景的动态像素一致。
人工验收的原暂停入口
原游戏管理器 T_.cfi() 调用 im.instance.cfi(),将更新开关 uhw=false,随后设置 Z_.instance.ciu.pause=true;T_.kzg() 做对应恢复(格式化 bundle 311060、310469)。im.tnz() 只有在 uhw 为真时遍历执行注册回调(6037)。原 Lx 注册的 TimeManager、HandleOnceEvent、HandleLoopEven 全在这个入口下(9783);时间累计 mxj += Laya.timer.delta 也在注册回调 krt 中(12899)。因此原暂停会冻结模拟时钟和对应游戏逻辑,适合暂停选房倒计时后人工检查。
这并不自动暂停 Laya 自身的 Animation、骨骼、Tween、全局 timer。截图验收中仍可能看到独立视觉动效继续运行;不能把逻辑暂停声称为渲染逐帧冻结。ciu.pause setter 在 Compound 模式为真时还派发 gm.lrs 事件;Normal 模式仍应走原方法,不能只修改 _pause。
构造入口在原 js/game.js 字符 2095803,唯一出现的 new T_(Dx[s[1886]])。在 web 适配构建时将该表达式桥接为 (window.__tpfyGame=new T_(Dx[s[1886]])),即可由 P 键调用 window.__tpfyGame.cfi() / .kzg()。解码副本对应字符 2774250 的唯一 new T_(Dx["mpv"]),格式化行 358599。桥接只暴露原实例;不重建管理器,不改倒计时或经济数值。按键状态由浏览器桥接单独保存,避免反复按下触发多次切换。