hero-doll-kit
v1.13.3
Published
雷曼式部件骨架成品包:41 个动作(含六个大招) + 四槽换装 + 可选的战斗表现层与残影层,Cocos Creator 3.8。npm 装包后跑 hero-doll-sync 把资源同步进工程 assets/resources/。
Readme
hero-doll 部件骨架包
雷曼式部件骨架的成品包:35 个动作 + 四槽换装(头盔/武器/上衣/鞋),零外部依赖——
动作是 Cocos 原生 cc.AnimationClip,运行时只有一个 HeroDoll 组件。
由骨架仓库 tools/pack-kit.py 打出,包里的 skins.ts 是生成物,别手改。
接入(Cocos Creator 3.8)
两种拿到包的方式,落地是同一个东西:
npm 方式(推荐,可跟版本更新)——在 Cocos 工程根目录:
npm i hero-doll-kit
npx hero-doll-sync # 把资源同步进 assets/resources/hero-doll/以后升级:npm update hero-doll-kit && npx hero-doll-sync。包自带固定 UUID 的 .meta,
原地覆盖后场景里的引用不会断。★Creator 只导入 assets/ 里的东西、不认 node_modules,
所以 sync 这步省不掉;同步是以包为准的整目录替换,别改 assets/resources/hero-doll/ 里的文件。
手工方式——整个 hero-doll/ 文件夹拷进目标工程的 assets/resources/ 下。
★连 .meta 一起拷。贴图的 .meta 里写死了 type: sprite-frame——少了它,Creator 会按
工程默认导入,3D 模板工程的默认是 texture,没有 spriteFrame 子资源,换装会静默失效
(Bundle resources doesn't contain hero-doll/skins/xxx/spriteFrame)。
然后(两种方式相同):
- 必须在 resources 里——换装贴图按路径动态加载;改了文件夹名就同步改组件的
basePath属性; - 把
hero-doll.prefab拖进场景(或instantiate出来); - 根节点挂上
HeroDoll组件; - 代码里:
const doll = node.getComponent(HeroDoll);
doll.play('walk'); // 35 个剪辑名见 skins.ts 顶部注释
doll.play('attack4'); // 120° 大横斩,持刀手与武器整体放大
doll.crossFade('attack', 0.1); // 带淡入切动作
doll.equip('helmet', 2); // 戴第 2 顶盔;0 = 穿回原件
doll.skinList('weapon'); // 给换装 UI 用:[{name, icon 路径}],第 0 项是原件
doll.bone('head'); // 取得骨骼节点,叠加注视/受击等程序控制
doll.slot('weapon'); // 取得手腕下的武器附件槽,可挂刀光/枪口特效
doll.setIK('foot-front', groundTarget, { weight: 1, maxDistance: 420 });
doll.setIK('hand-front', gripTarget, { followRotation: true });
doll.clearIK('foot-front'); // 解除单脚;不传参数则解除全部 IK
// 通用约束
doll.setTransformConstraint('hand-back', shieldGrip, {
positionWeight: 1, rotationWeight: 0.5,
});
doll.setAimConstraint('head', enemy, {
weight: 0.8, offsetAngle: 180, minAngle: -35, maxAngle: 35,
});
doll.setGroundConstraint('foot-front', groundNode, 0, 1);
doll.setRotationLimit('head', -40, 40);
doll.setPathConstraint('hand-front', [pathA, pathB, pathC, pathD], {
progress: 0, speed: 0.35, loop: true, weight: 1,
orientToPath: true, rotationOffset: 90,
});
doll.setPathProgress('hand-front', 0.5); // speed=0 时可手动驱动
doll.clearConstraints('head');
// Clipping:水面以下隐藏;也可用 ellipse 做圆形窗口
doll.setWaterline(180, true);
doll.setClipRect(-240, 100, 480, 520, 'ellipse');
doll.clearClipping();
// 帧事件统一从角色根节点发出
doll.node.on(HeroDoll.EVENT, (name: string, clip: string, payload: string) => {
if (name === 'hit') damageEnemy();
if (name === 'footstep') playStepSound(payload); // payload = front/back
});动画事件
攻击命中、脚步、落地、施法和拾取等时间点已经写进 .anim。运行时组件会把 Cocos 帧事件从
内部 rig 节点统一转发为根节点的 HeroDoll.EVENT,监听参数依次是事件名、剪辑名和标签。
事件只说明动画到达关键节拍,不会自动造成伤害或播放声音,具体游戏逻辑由监听方决定。
IK(浮空部件版)
setIK 接收场景中的目标节点,在 Animation 每帧采样之后修正对应骨骼的世界位置。把目标节点留在
地面即可钉住脚;把它挂到移动武器或方向盘上即可让手跟随。weight 可在动画姿势与 IK 之间混合,
maxDistance 防止部件离身体太远,followRotation 可让脚贴斜坡或让手跟随握点角度。
当前图片没有上臂/前臂和大腿/小腿,因此这是末端约束,不会计算肘膝弯曲;以后增加分段肢体时,
可以保持 setIK 的调用方式并把内部求解器升级成双骨 IK。
通用 Constraints
setTransformConstraint:按独立权重跟随目标位置和旋转,可附加世界坐标偏移;setAimConstraint:朝向目标,可设置图片方向校正、混合权重和最小/最大角;setGroundConstraint:骨骼不能低于固定世界高度或移动地面节点;setPathConstraint:沿两个以上控制点组成的 Catmull-Rom 曲线移动,可自动播放或手动控制进度;setPathProgress:直接设置路径归一化进度;setRotationLimit:限制最终世界旋转角;clearConstraints:清除单个部件或全部通用约束。
路径控制点可传 Node(实时读取世界坐标,路径会跟着节点移动)或 Vec3(固定世界坐标)。
orientToPath 会用曲线切线控制骨骼朝向,rotationOffset 用于校正图片本身的朝向。
每帧固定按“关键帧动画 → IK → Transform → Path → Ground → Aim → Rotation Limit”求解,最后的旋转限制 因此能够兜住 Aim 或 Transform 产生的过大角度。所有约束都不启用时没有额外姿势改动。
Clipping 遮罩
预制体在 hero-doll 与 rig 之间带独立 clip 容器。setClipRect 支持矩形和椭圆,坐标使用
角色局部坐标;setWaterline 是水面裁切快捷方式,可选择只显示水上或水下。运行时会移动 clip、
同时反向移动 rig,所以改变遮罩区域不会挪动角色和骨骼。clearClipping 可无损关闭遮罩。
HeroDollFighter(可选的战斗表现层)
HeroDoll 是纯播放器:你叫它播什么它播什么。HeroDollFighter 是盖在上面的一层,
管跳跃状态机、连招串接、命中顿帧/震屏/特效。两个都挂在同一个根节点上,不挂 Fighter
不影响 HeroDoll 的任何功能。
const fighter = node.getComponent(HeroDollFighter);
fighter.setWeapon(3); // 和 doll.equip('weapon', 3) 配套调用
fighter.attack(); // 连点会顺着当前档位的连段往下接
fighter.jump();
fighter.setMotion(vy, grounded); // 每帧把你的移动逻辑喂进来,它据此选 air/fall/land
if (fighter.locked) return; // 出招硬直中,别让玩家移动
fighter.weaponTier; // 'light' | 'mid' | 'heavy'连段按武器档位选,档位写在 skins.ts 的 tier 字段里(由 tools/gen-skins.py 生成):
| 档位 | 连段 | 武器 |
|---|---|---|
| light | punch | 空手(「无」) |
| mid | attack → attack2 → attack3 | 剑/刀/戟/枪等刃类 |
| heavy | attack4 → slam | 斧/锤/狼牙棒 |
这份表只是默认值,每个游戏的招式设计不一样 —— 比如某一招是你关卡设计的支点 (会真跃起、落地炸一片、带无敌帧,怪位和平台间距都照它摆),那它必须留在连段里。 两种改法,改完不影响连段状态机的其余部分:
fighter.setCombo('mid', ['attack4', 'attack4', 'slam']); // 换掉 mid 档
fighter.setCombo('mid'); // 不传 = 撤销,回到内置那份
fighter.chainOf('mid'); // 查当前实际用的是哪串或者在编辑器里填 comboOverride 属性,一行一档,留空则用内置的:
mid: attack4, attack4, slam
light: punch, punch★剪辑名必须在 prefab 的 _clips 里(35 个剪辑名见 skins.ts 顶部注释)。
setCombo 会先替你查一遍,对不上的会 warn 出来 —— 没登记的剪辑 play() 不播但照锁硬直。
特效搭配:哪把刀配哪道光归你
内置的推导是「武器档位 → fx/ 目录,同档里按出现次序分配变体,slam 钉死用紫新月」。
那是这套美术的编排,不是骨架的判断 —— 你的刀和你的光不一定这么配。
一个静态钩子接管全部三条,不设则行为完全不变:
import { FX_VARIANTS } from './hero-doll/skins';
HeroDollFighter.pickFx = ({ weaponIdx, tier, clip, onGround }) => {
if (onGround) return null; // 砸地那圈交回默认
if (clip === 'slam') return 'heavy/05'; // 收招大招用自家这套
return `${tier}/01`;
};返回相对 fx/ 的目录名;返回 null 或不设 → 落回内置推导。变体总数从 FX_VARIANTS
读(打包时数出来的),拼字符串前对一下就不会指到不存在的目录。
是静态的:一个工程一套美术编排,不用逐个实例配。
★预热只跑「当前武器 + 空剪辑名」那一套(换武器时问一次),按剪辑分叉的套子第一次 触发会晚一拍:那一下先不出,加载完从下一次起正常。
移动锁:攻击硬直和落地恢复是两回事
fighter.locked // 攻击中 or 落地恢复中 —— 别吃移动输入
fighter.attackLocked // 只有攻击硬直
fighter.landing // 只有落地恢复
fighter.landRecover // 落地恢复时长,默认 0.30(land 剪辑本身就是 0.30s)setMotion 只被攻击硬直拦。落地恢复那 0.30s 里人完全可能已经又离地了
(连跳、兔子跳),所以那段照样吃 setMotion,air/fall 播得出来。
拦移动输入用 locked,语义和以前一致。
朝向
刀气方向读的是世界缩放(getWorldScale().x),不是娃娃节点自己的 scale.x。
横版的常规做法是把翻转挂在更上层的角色节点上(影子、倾斜、跳跃放大都读同一个 scale),
娃娃节点自己的 scale.x 恒为正 —— 所以不必为了迁就特效层把翻转下沉到娃娃节点。
特效大小不是给倍率、是给目标屏幕宽度再反推缩放(FX_W),所以换任意画幅的图都自动对齐。
特效挂点默认取 node.parent——★别挂在角色节点下面,角色一走动已经甩出去的剑气会跟着平移。
帧事件之外:自己挑时机
上面那套是「播剪辑 → 帧事件 → 出特效」,时机写死在 .anim 里。
跟着物理弧线采样驱动姿势的接入方没有 play(),也就没有帧事件 —— 跳多高、多久落地每关都不同,
固定时长的播放对不上,落地冲击波也必须炸在真正着地那一刻,而不是剪辑的第 0.90 秒。
这三个入口是给这种情况的,走的是和 hit / land 帧完全同一段代码,
缩放、朝向、锚点、前伸那套照旧:
fighter.spawnSlash(); // 按当前武器档位甩一道
fighter.spawnSlash('slam'); // 指定剪辑名 → 走 SLASH_FX 的专属配色(下劈的紫剑气)
fighter.spawnSlash('slam', true); // onGround:砸地那圈,冒在脚下
fighter.feelHit(); // 顿帧 + 震屏,照 hitStop / hitShake / shakeTarget
fighter.strikeBolt(); // 砸地落雷,landBolt 关掉时自动空转★帧事件里的 land 分支是三件一起(特效 + 落雷 + 顿帧)。自己驱动时只调 spawnSlash
会少掉后两件,想还原完整的落地冲击就三个都调:
fighter.spawnSlash('slam', true);
fighter.strikeBolt();
fighter.feelHit();这几个入口是纯表现:不播剪辑、不锁移动、不推进连段。硬直和状态归你自己管。
HeroDollGhost(可选的残影层)
冲刺、滑铲、攻击、腾空时身后留一串正在淡出的自己。和 HeroDollFighter 一样是可选层:
不挂就零开销,挂了也不碰 HeroDoll 的任何状态。
唯一要你决定的是「此刻该不该留影」 —— 那是玩法。池子、间隔、淡出、摆位都在这层里, 因为它们跟着角色长相走:十个关卡各抄一份「怎么留影」,就是十份会一起腐烂的代码。
const gh = node.getComponent(HeroDollGhost);
gh.count = 4; // 池子大小(同时最多几道)
gh.life = 0.22; // 每道存活秒数
gh.gap = 0.055; // 每隔多久留一道
gh.alpha = 130; // 起始透明度,之后线性淡到 0
gh.tint = new Color(140, 210, 255, 255); // 染色;useTint 关掉则照抄本色
gh.parentNode = fxLayer; // 挂哪。不填取**娃娃节点**的父级 —— 见下面这条★
gh.behind = true; // 排到父节点最前,画在角色下层
update(dt) { gh.step(isDashing || isAttacking || airborne, dt); }另外两个入口:gh.emit() 不看间隔冷却,立刻补一道(起跳瞬间、命中帧这种精确时刻用);
gh.clear() 立刻收掉所有在场的残影。
★**parentNode 不填时取的是娃娃节点的父级**。在标准用法里(prefab 直接拖进场景、
每帧挪的就是娃娃节点)这是对的。但如果你把娃娃当视觉子节点挂在自己的角色节点下
(碰撞体、物理、HUD 锚点都在那一层,每帧挪的是外层),那么默认值恰好就是那个会动的节点 ——
留在原地的残影会跟着人一起平移,整条拖影塌成一坨。这时必须显式指定一个静止的层:
gh.parentNode = stageLayer; // 通常是角色节点的父级,不是娃娃节点的父级组件自己会查这一条:连续两次留影之间,父级动了、而且基本是驮着角色一起动的, 就在控制台 warn 一次(世界层在滚——视差、地图平移——不会误报,那种情况残影相对世界是静止的)。
想自己管池子
Boss 冲刺残影、定格回放、传送残像这类,池子和时机确实是关卡自己的事,用低一层的入口:
doll.ghostInto(target, tint?); // 把当前姿势拓一份到 target 下,返回成功与否
doll.clearGhost(target?); // 丢掉副本缓存(target 销毁后调)★ghostInto 只管「拓印」这一件事。副本根不带本体根节点的 transform ——
根变换由 target 承担,否则会和 target 叠两次。自己调的话把 target 摆到角色的世界位姿上。
拓印做了哪些事(自己写一份的话这些都得有)
这几条都是「不报错、只是画面不对」,列在这里省得再踩一遍:
| | |
|---|---|
| 克隆先拆组件再挂进场景 | instantiate 会把 HeroDoll / HeroDollFighter / Animation 一起复制走。挂进场景那一刻它们的 onLoad 就跑了 —— 每一道残影都会去加载自己的换装贴图、注册自己的帧事件、命中时甩自己的刀气。顺序必须是 instantiate → 拆 → 再 parent(未激活的节点不跑 onLoad)。这里用白名单:只留 UITransform / Sprite(含 MeshSprite)/ UIOpacity / Mask,你自己往娃娃节点上挂的脚本也一样不该在残影里跑 |
| 连 UITransform 一起拷 | 换装会改部件的 contentSize 和 anchorPoint(每件皮肤挂点锚点都不同)。只拷 position/scale/angle 的话,残影会拿原件的锚点去摆新装备的图,肩膀和武器错位 |
| 收全部节点,不只是带 Sprite 的 | clip / rig / spin / <部件>-fx 都是纯容器,但剪辑的整体位移、翻滚转体、挤压拉伸正写在这几层上。只收 Sprite 节点的话残影看着是「死的」 |
| MeshSprite 的四个标量也要拷 | 袍摆/披风的形状由 bend / wave / bulge / phase 决定,不在 transform 里。不拷的话本体在飘、残影是直筒(walk/run/dash/fall 最明显) |
| 残影挂在会动的节点外面 | 挂在每帧移动的节点下,留在原地的残影会跟着平移。注意 parentNode 默认取的是娃娃节点的父级 —— 娃娃自己就是角色时没问题,娃娃是某个会动的角色节点的视觉子节点时就得显式指定。根变换取的是世界位姿,所以翻转挂在更上层也认,娃娃节点自己再有位移(setWaterline)也算得对 |
skins.ts 里除了换装表还有什么
包里这几个导出是接入方会用到、但只看换装表发现不了的:
| | |
|---|---|
| tier | 武器档位,HeroDollFighter 靠它选连招和特效。自己实现战斗层的话也从这里读,别在代码里写死武器编号 |
| alt | 一件装备额外覆盖的部件。目前只有「无」用到:它除了换 hand-front,还带一只 hand-back |
| RIG_PER_PX | 节点坐标 ÷ 屏幕像素 = 11.5633。角色在节点坐标里 1827 单位高,美术意图是屏幕上 158px。凡是"想让某个东西在屏幕上有 N 像素"的换算都要过这个数 |
| FX_VARIANTS | 每档特效有几套配色,按武器在本档里的次序分配。改 fx/ 目录数就得同步改它,否则取模会指到不存在的目录 |
自己实现换装 / 自己驱动时间轴要注意
用 doll.play() 和 doll.equip() 的话下面三条都已经在组件里处理好了,不用管。
绕过它们自己实现的接入方才需要看:
equip要按"槽的部件 ∪ 该槽任何一件 skin 用alt覆盖到的部件"来遍历,不能只按SLOT[slot]那一份。因为「无」比别的武器多带一只hand-back:从「无」换成大剑时, 只遍历SLOT['weapon'] = ['hand-front']的话,那只后手会留在身上换不回去。手动
sample()驱动时间轴、不走play()的,切剪辑时必须自己把<部件>-fx层的 scale 归位。 各剪辑写的 fx 节点不是同一批——land/hurt/crouch写三个, 其余只写一个。没写到的那些不会被新剪辑覆盖,于是落地压扁的脚会带进待机姿势。MeshSprite的bend/wave/bulge/phase同理会残留。cast写 bend+bulge、walk写 wave+phase+bend、attack4一个都不写,从前两个切到attack4会留着上一个的形变。
挤压拉伸 / 网格变形
两档,都不用写运行时代码——数据在剪辑里,引擎自己采样。
挤压拉伸(非等比缩放):每根骨骼下有一层 <部件>-fx 节点专收动作缩放,
scl 轨的 x/y 分开发,保体积(sx*sy≈1)就是 squash & stretch。
land 着地压扁、crouch 蹬地拉长、hurt 挨打横压 都用了。
放在 fx 层是为了不覆盖换装比例。
网格变形(MeshSprite)+ 权重绑定:刚体旋转做不出来的东西——披风下摆、袍摆、翅膀弯曲。
被任何动作变形过的部件,预制体里挂的是 MeshSprite(继承 cc.Sprite,多四个属性),
把图切成 1×8 网格逐顶点位移。目前 fly 的袍摆飘动、cast 的聚气带袍用了。
四个属性由动画直接驱动,不存逐顶点关键帧——那会把 .anim 撑爆而且没法手调:
| | |
|---|---|
| bend | 顶边钉住、底边横移(弯曲/被风吹斜) |
| wave | 沿纵向的正弦横移(下摆飘动) |
| bulge | 中段横向鼓胀(呼吸/鼓包) |
| phase | wave 的相位(让波往下走) |
顶点位置由 MeshSprite.deform() 现算:布料沿纵向切成 6 节骨骼,顶端焊死在挂点上,
每个顶点绑到相邻两节、权重 (1-f, f)。那四个标量摊成每节的转角,走正向动力学算关节位置——
所以布料长度守恒,弯到 90° 是卷起来而不是被抻长。
这条公式在三处必须一模一样:
MeshSprite.ts / 骨架仓库的 hero_rig.mesh_deform() / 调参页——改一处三处一起改。
不走 3D MeshRenderer 是因为它和 UI 的层级排序打架:这套骨架的前后关系全靠节点顺序读
(子节点必然画在父节点之上),换 3D 渲染器等于把立身之本拆了。所以用 2D 的自定义 assembler。
结构
| | |
|---|---|
| hero-doll.prefab | hero-doll → clip → rig → spin → body-bone → 头/双手/双脚 bone → 图片 attachment。clip 管遮罩,翻滚/击飞转 spin;前手骨下预留 weapon-slot |
| anims/ | 35 个剪辑:转角度 + 位移(y 蹲压 / x 前伸),引擎原生采样,无自定义运行时 |
| parts/ skins/ icons/ | 本体贴图 / 39 件换装 / 选择格子图标 |
| skins.ts | 换装表:每件的挂点锚点 + 归一缩放,HeroDoll.equip 只查表。另外导出 tier / alt / RIG_PER_PX / FX_VARIANTS,见下 |
| HeroDollFighter.ts | 可选的战斗表现层:跳跃状态机 + 按武器档位选连招 + 命中顿帧/震屏/特效。不挂它,HeroDoll 照常工作——纯播放器 |
| HeroDollGhost.ts | 可选的残影层:池子 + 淡出 + 拓印。不挂就零开销 |
| fx/ | 命中特效贴图,按 档位/套号/fx-01.png… 组织。由 HeroDollFighter 加载,不挂它就用不到 |
| HeroDollEventRelay.ts | 帧事件转发器,HeroDoll 自己挂上去用。别删也别并进 HeroDoll.ts——Cocos 一个脚本只允许一个 Component 类,并进去会 errorID(3615) 然后 _sealed TypeError,整个场景起不来 |
.meta 是包的一部分:prefab 靠 UUID 引用贴图和剪辑,丢了 meta 引用全断。
拷贝时保留隐藏文件;同一工程里别导入两份(UUID 会撞)。
换一个角色 / 换一批装备图能用吗
能——组件和表全是生成的,美术全换也不用改代码,但要回骨架仓库重跑工具链:
新散图 → tools/cut-parts.py / cut-sheet.py # 切件
→ tools/tone-match.py # 调子拉齐
→ tools/gen-skins.py # 量挂点/缩放 → skins.py
→ tools/gen-anims.py # 出 35 个 .anim
→ 工程里跑 tools/gen-hero-prefab.py # 出 prefab(贴图 UUID 每个工程不同)
→ tools/pack-kit.py # 重新打包两个前提:新角色得是同构的(6 件浮空部件、朝左、比例别差太远——动作表里的角度
是按这套身材调的,身材大变要回调参页重调);量挂点的规则里有几条是按这套图定的
(武器找白手套、头盔补 17px 下巴插深),换画风可能要在 gen-skins.py 里改常数。
发一个新版本(骨架仓库这边)
# 1. 改 tools/kit/package.json 的 version(版本号真源在 tools/kit/,kit/ 是生成物)
python3 tools/pack-kit.py # 2. 重打包
cd kit/hero-doll && npm publish # 3. 发布(首次要 npm login;包名 hero-doll-kit)