Seen: the sim moved a displaced enemy at once and it walked / swung again on the next tick; players compared with the official game (PR #392: 「被推/拉完的敌人会先站住一小会儿再走」). Research (scratchpad report r3): PRTS 失衡位移机制 — a force > 0 puts an enemy in the UNBALANCE state machine with a 0.1 s 失衡硬直 floor, ending when its speed is ≤ 0.1 m/s, on 浮空 or a forced state change; 异常效果图鉴/失衡免疫 「失衡期间无法自主移动、发动攻击、使用技能」; 异常效果 — not an abnormal status (水遁忍者's check still runs); 游戏数据基础 推力-位移近似对应表 位移时间 (whole frames at 30 fps); 推与拉 拉力 「作用时间默认为 1 s;若本次受力 等级 < −1,作用时间改为 0.5 s」, 急停 keeps the state to the window's end; 特殊机制 静态刚体 (0.1 s 保底); 术语释义 浮空. Now: constants.js PUSH_UNBALANCE {−2: 0.2, −1: 0.4, 0: 0.8, 1: 0.9, 2: 32/30, 3: 35/30}, PULL_UNBALANCE 1 / PULL_UNBALANCE_WEAK 0.5, UNBALANCE_MIN 0.1. battle/displacement.js: push() holds for its level's row (the −2 correction included), kept when the 特效 column or a wall shortens the slide [ASSUMED for the wall], the 0.1 s floor when a wall stops it at the first step [ASSUMED]; pull() holds for its window whatever the travel (≤ −3: none; an enemy the puller blocks: still no pull [ASSUMED, unchanged]); a 静态刚体 (data staticBody, not 失衡免疫 noDisplace) hit by force level ≥ −2 gets 0.1 s and no movement (_staticForce); _unbalance sets enemy.unbalanceUntil (a longer running one kept) and ends the attack clip's stand. ai.js updateEnemy: no walking (恐惧 / 诱导 too — PR #392's fear exemption is not taken), no new normal attack, a swing short of its frame cut, 浮空 clears it; a 重生 clears it (archetypes.js reborn). Not a stun: no status, no immunity, SP and the attack cooldown run. No global skill lock: enemy abilities keep ticking ([ASSUMED] scope). The move stays instant (the unbalanced() talents keyed off the jump unchanged). The displace fx carries `dur` (game s); render/units.js slideTo divides it by ctx.animRate (2× default: a 0.8 s push slides 0.4 real s); DISPLACE_SLIDE·√D is only the fallback for an fx without it. Not taken from #392: UNBALANCE_TRAVEL 0.6387·√tiles / the client's 0.3194 (derived from the page's μ = 0.5 单位假设, one frame high on every push row, the wrong shape for pulls) and the fear exemption. Tests: test/sim/unbalance.test.js (8: the push rows and walking after them; no attack in the hold, a feared enemy held; the −2 correction's 0.2 s; 特效 / wall-shortened rows and the first-step floor; pull windows incl. the 急停 and ≤ −3; 静态刚体 0.1 s without movement, 失衡免疫 never; 浮空 clears; the fx `dur` — the file fails on the parent); test/render/unitview.test.js (+1: the slide at 2× for a push and a pull, the fallback — fails on the parent); feedback1b-displacement B5 (the drone is now held 0.1 s per force, never moved). test/sim + test/content + test/render: 3433 pass. Docs: SIM.md (push / pull, displace, assumptions), PLAYING.md 推开 / 拉拽, research 13 §0 / §5. Golden — every moved battle has pushes / pulls (probe of the callers) and its enemies are now held; outcome moves: roster-002 (野鬃: killed 29→33, leaks 15→11), roster-003 (野鬃: deaths 3→7), roster-007 (薄绿: killed 110→118, leaks 53→47), roster-008 (薄绿: 45→47 / 42→40), roster-010 (歌蕾蒂娅: killed 45→44, leaks 57→58), roster-011 (莫斯提马: 78→79 / 4→3), roster-031 (钩索师 pulls, tier4: killed 64→72, leaks 12→4, deaths 6→5), roster-041 (钩索师: deaths 5→4), roster-043 (山: deaths 15→14), roster-044 (山: killed 107→103, leaks 12→16, deaths 15→14); bond-deputShip-low (薄绿: 18→21 killed, leaks 3→0, deaths 1→0), bond-kazimierzShip-high (野鬃: deaths 3→2); boss-boss_3-pair (锏: p2 deaths 3→2), boss-boss_4-pair (莫斯提马 / 歌蕾蒂娅: killed 11→12, boss HP 7442429→7375628, p2 leaks 2→1, deaths 19→18), boss-boss_6-pair (boss HP 6932482→6865243, p2 deaths 19→17), hidden-boss_8-pair (boss HP 12585770→12585798), hidden-boss_9-pair (锏: boss HP 3630612→3636991); standin-05 / 08 / 10 (预备干员-特种 pulls: killed 5→8 / 42→44 / 28→29, leaks down by 3 / 2 / 1); diy-101 (傀影: killed 22→21, leaks 92→93), diy-114 / 115 / 116 (黑键: 106→99 & 83→90 / 154→156 & 78→70 / 3→2 & 120→121), diy-137 (预备干员-特种: 68→79 / 79→68); matches through those battles — coop2-HARD-4, coop2-NORMAL-14-standins, coop2-NORMAL-3, coop2-NORMAL-70-diy, coop3-ABYSS-5, coop4-ABYSS-7, solo-ABYSS-2 (LP 2→4 at round 10), solo-HARD-2 (ai_0 now survives round 13: eliminated 13 → alive), solo-HARD-9-boosted (LP 399→398), solo-NORMAL-1. Other moves (damage, heals, snapshots only): roster-009/012/017/019/032/039/040/042/046, 13 bonds, boss-boss_2-pair, hidden-boss_10-pair, standin-03, diy-053/084/085/102/103/108/109, coop2-ABYSS-10-boosted, solo-ABYSS-1.
10 KiB
13 · 推拉(击退 / 拖拽)的官方实现:冲量 + 摩擦
这份文档回答两个问题:官方推拉到底怎么动,以及摩擦是谁的参数。结论全部来自官方客户端本体的
global-metadata.dat(il2cpp 元数据)、官方 gamedata(社区镜像 Kengxxiao/ArknightsGameData)与官方 AB 资源包,
并附上每一步的验证方式,便于复核。
0. 结论速览
| 问题 | 答案 |
|---|---|
| 官方怎么动 | 冲量 + 摩擦:给被推者一个力(m_force / m_forceVectorX/Y),速度逐帧被摩擦衰减直到停下 |
| 有固定时长/距离吗 | 没有:数据里不存在 move_duration / move_distance / move_tiles 键(moveDuration、moveTiles 属于 UI 类)⇒ 位移量是涌现的,这也是社区只能给出"推力-位移近似对应表"的原因 |
| "力度"从哪来 | 官方数据:skill_table.json 的 blackboard force(41 个技能带它),敌人侧在 enemy_database.json(8 条) |
| 摩擦是谁的参数 | 地板(地形)+ 单位质量,不是技能参数:GrasslandType { NORMAL_LAND, ICE_LAND, SLIME_LAND },每格是一条 GrasslandData { modeIdx, grasslandType, additionalFriction },运行时还会被 UpdateFrictionFactor / RestoreFrictionFactor 临时改写 |
| 卫戍协议里有几种地板 | 实际只有普通地面:11 张地图共 18 种格子字符,无冰面/黏液 ⇒ 本模式下摩擦恒定,滑行时长不必按地板分档 |
| 飞行目标怎么算 | 没有独立的空中摩擦(boatAirFactor / m_airBase 属于"船"玩法):飞行用的是同一套公式(单位摩擦 × 所在格子的 additionalFriction);差别只在运动模式(ENEMY_MOTION_TYPE_FLY、HEIGHT_CHANGED_TO_FLY_BLOCK_MOTION_MODE ⇒ 忽略地面阻挡),以及失衡免疫 / 静态刚体的空中单位根本不可位移 |
| 曲线形状 | 恒定减速 ⇒ d/D = 2k − k²(ease-out quad) |
| 推多久 / 拉多久 | 失衡(UNBALANCE)状态机的时长(0.2.2,PR #392 的思路、按来源重做):推 = PRTS 游戏数据基础「推力-位移近似对应表」的位移时间(受力等级 −2/−1/0/1/2/≥3 → 0.2 / 0.4 / 0.8 / 0.9 / 32⁄30 / 35⁄30 游戏秒,正好是 30 fps 的整帧);拉 = 推与拉的拉力作用时间(1 s,受力等级 < −1 时 0.5 s,急停后仍保持到结束);静态刚体受力 > 0 时 0.1 s 保底。客户端滑行用同一段游戏时间(默认 2 倍速 ⇒ 现实时间减半);T = 0.14·√D 只留作旧录像(fx 不带 dur)的回退 [ASSUMED] |
1. 官方类与字段(global-metadata.dat)
推拉能力的类簇(与 PRTS 的"推与拉"术语逐字对应):
Knockback · KnockBackWithDirection · KnockBackWithCharacterDirection · KnockBackTargetsByRootTile · DragTowardSource
_decreaseForceLevelWhenNotInDirection(PRTS"特殊修正"−2 力度)· _dontChangeFaceByDirection · _GetPushDir
TOO_CLOSE_TOLERANCE_SQR(PRTS"< 0.25 格转径向")
运动学字段(同一批类里):
m_force · m_forceVectorX/Y · forceScale · m_mass · m_velocity · get_mass · get_rigidbody2D
m_friction · m_frictionBase · m_frictionFactor · get_frictionFactor · m_frictionFactorAdditional · get_additionalFriction
UpdateFrictionFactor · RestoreFrictionFactor · _UpdateFrictionFactorAdditional · RestoreCached…
_ApplyForce · _UpdateVelocityAndPos · _UpdateAllUnitDeltaPos · _GetForceByDirectionAndDamage
m_bounceFrictionFactor · bounce · bounceease · _bounceTimesAsDamageTimes · _bounceBBKey · _bounceConditionValidator
unbalanceProtectDueTime
⇒ m_force*(冲量)+ m_velocity / _UpdateVelocityAndPos(逐帧速度积分)+ m_friction*(衰减)就是"冲量 + 摩擦";
UpdateFrictionFactor / RestoreFrictionFactor(改完还要还原)说明摩擦由外部临时提供,不是能力自带。
2. 地板(地形)摩擦
地板系统就在卫戍协议自己的格子类里(同簇可见 TileState { SHARED, MY_SIDE, MATE_SIDE }、
CooperateStartTile / CooperateEndTile / CoopFootballTile、get_modeIndex / get_tilePlayerSide /
_OnPlayerDying / _OnPlayerRevive):
_grasslandDatas · GrasslandType { NORMAL_LAND, ICE_LAND, SLIME_LAND } · GrasslandData { modeIdx, grasslandType, additionalFriction }
DynamicBuffQuickSandTile(流沙)· DynamicBuffTile · DirectionTile(带方向的格子)
⇒ 每格可带 additionalFriction,即"地板提供摩擦"。另有冰面相关因子(iceFactor、iceTile、OnIce、Frozen)
与"船/水/空"那套(m_boatFrictionFactor、m_boatAirFactor、m_airBase、m_waterFlowForce* —— 属于多索雷斯船玩法,
不是飞行单位)。
卫戍协议实际用到哪些地板
docs/research/05-maps.json(11 张地图)里出现过的全部格子字符:
# ×1776(墙/虚空) r ×580(道路) X ×477(分区隔断) f ×448(地面) a ×330 b ×250 A ×165
p ×77(预览栏) d ×76(装置) S ×66(起点) I / E ×33(路线起/终) h ×24(手牌栏) O ×22 m ×12 i ×8 g ×8 R ×4
没有冰面或黏液 ⇒ 本模式下所有可站地板摩擦一致。
3. 力度来自官方数据(不是我们拟定的)
excel/skill_table.json 的 blackboard 里 force 出现 357 次、41 个技能,例如:
| 技能 | 名称 | 力度(按等级) |
|---|---|---|
skchr_weedy_1 / _3 |
温蒂 炮管敲击 / 液氮大炮 | 0,1,2 / 1,2,3 |
skchr_forcer_1 / _2 |
见行者 护身射击 / 惊爆射击 | 0,1,2 / 1,2,3 |
skchr_panda_1 / _2 |
食铁兽 铁意六合 / 崩拳式 | 0,1,2 / 1,2,3 |
skchr_rope_1 / _2 |
崖心 勾爪发射 / 复式勾爪 | 0,1,2 / 0,1,2 |
skchr_glady_1 / _3 |
歌蕾蒂娅 缺水的大洋裂断 / 缺水的碎漩狂舞 | 0,1,2 / 0,1,2 |
skchr_sqrrel_1 / _2 |
阿消 水蒸气泵 / 高压水炮 | 0,1,2 / 0,1,2 |
skchr_moeshd_2 |
可颂 磁爆锤 | 1,2,3 |
sktok_archook |
钩索 token 发射 | 11 |
同一张表里 没有 friction / mass / knockback 键 ⇒ 力度是数据(可查表),摩擦与质量在引擎里。
靠"职业"就能推拉的干员
character_table.json 的 subProfessionId 有两个专门职业,普通攻击即推/拉,力不在技能 blackboard 里:
pusher(推击手):阿消 · 见行者 · 食铁兽 · 温蒂hookmaster(钩索师):暗索 · 歌蕾蒂娅 · 杏仁 · 雪雉 · 崖心
其余靠技能/天赋推拉的还有:Misery、灰烬、黑键、燧石、溯光星源、可颂、焰狐龙梓兰、傀影、莱伊、罗宾、圣聆初雪、
琳琅诗怀雅、乌尔比安,以及 勾爪 / 暴风雪 / 喷拒器 / 猎潮的骑士 / 流形 / weedy_token 等 token 与装置。
4. 顺带查清的两条官方机制(尚未建模)
- 撞墙弹跳:
bounce/bounceease/m_bounceFrictionFactor/_bounceBBKey,并且_bounceTimesAsDamageTimes—— 弹跳次数计入伤害次数(温蒂 / 推击手那类撞墙伤害的官方口径)。 - 失衡保护:
unbalanceProtectDueTime—— 连续推拉之间有保护窗口。
5. 客户端的做法(本次实现)
render/units.js:模拟位移是瞬时的(sim/battle/displacement.js 在一次调用里走完 0.1 格步进),随后敌人处于
失衡状态(0.2.2:不走路、不发动普通攻击,时长见 §0「推多久 / 拉多久」),快照只带终点,
所以客户端补上中间帧——UnitView.slideTo(x, y, { dur, friction }) 建立一次滑行,update() 逐帧做速度积分
(v ← max(0, v − a·dt)、d ← d + v·dt),终点由模拟给出(权威),v0 = 2D/T、a = v0/T 保证
T 秒内正好走完 D 且速度归零 —— 即官方"冲量 + 摩擦"的等效形式;T = displace fx 带来的失衡时长 dur(游戏秒)÷
播放倍率(ctx.animRate,默认 2 ⇒ 受力等级 0 的推 0.8 游戏秒 = 0.4 现实秒),滑行结束时模拟的失衡也正好结束;fx 不带
dur(0.2.2 以前的录像)时才回退到 T = 0.14·√D(夹 0.12–0.45 s)[ASSUMED]。friction 是地板因子(本模式恒为 1;将来有冰面就乘
< 1 ⇒ 滑得更久)。PR #392 的 0.6387·√格 / 0.3194 是用 PRTS 页面的「μ = 0.5 单位假设」推出来的,页面没有写出、也对不上
位移时间表(每档高约一帧),未采用。render/app.js 在插值器开始显示「以带终点的快照结尾」的那一段时就用 displace fx 启动滑行:
到达那张快照之前画面停在原处,之后从原处减速滑入模拟的实时位置(终点,以及敌人随后继续走的路),落地不跳。
(PR #380 合入时模拟层、协议、golden 未改;0.2.2 的失衡状态改了模拟层,displace fx 多带一个 dur。)
滑行途中死亡:滑行照常走完,尸体停在被推到的位置(模拟里它已经在那里;0.2.2 合入时改掉了原 PR 的「回到出发格」);
倒地的单位交给 setDown 放到它的格子上。
6. 复核方式
- 元数据:
node读global-metadata.dat,搜上列标识符(本文件所有类名/字段名都是这样得到的)。 - 力度表:
zh_CN/gamedata/excel/skill_table.json→ 各技能的levels[].blackboard里key === 'force'; 敌人侧levels/enemydata/enemy_database.json。 - 职业:
excel/character_table.json的subProfessionId ∈ {pusher, hookmaster}。 - 地板:
docs/research/05-maps.json的格子字符统计(本文件 §2)。
7. 附:官方 AB 资源包的读取要点(踩过的坑)
供以后要读官方战斗配置时参考(本次为定位"摩擦是不是数据"而打通):
Flags里的0x200(UsesAssetBundleEncryption)在本客户端上是虚设:blocksInfo 既没有 70 字节加密前导, 也不需要任何 16 字节 key(暴力扫global-metadata.dat144 万候选、GameAssembly.dll5 万候选均无命中即因此)。- blocksInfo 位置 =
align16(headerEnd);块数据是连续存储的(不要逐块对齐)。 - 压缩:
comp 3= 普通 LZ4 ✓;comp 4 / 5= Arknights 自定义 LZ4AK(nibble 交换后按 LZ4 解, 终止条件是op === uncompressedSize,严格相等)。 - 包内官方配置是带
$type的 JSON(如Torappu.Battle.Action.Nodes+SetBodyDirection), 内嵌类型树 ⇒ 字段名可直接读;但摩擦不在其中(本文档 §2 已说明原因)。