关于 msw-combat-system
msw-combat-system 汇总 MapleStory Worlds 常见 2D 战斗层的原生 API 流程,面向实现战斗系统的开发者。内容涵盖攻击判定、伤害、受击反应、战斗状态、事件、AI 与伤害显示,并链接到具体实现参考。
主要功能
- 01说明攻击形状与目标筛选
- 02整理伤害计算及命中事件接口
- 03涵盖击退、无敌帧与战斗反馈
- 04介绍状态、玩家生命值和复活流程
- 05梳理事件总线与原生 AI 能力
适用场景
- 01为近战或范围攻击配置命中判定
- 02实现怪物受击、击退与无敌帧
- 03构建带状态机或行为树的战斗 AI
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ msw-git/ msw-ai-coding-plugins-official --skill msw-combat-system安装后在 Claude Code 中输入/msw-combat-system确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ai-bt.mdhp-gauge.mdprojectile.md详细介绍
msw-combat-system
完整的 MSW 战斗流程。仅涵盖通用 2D 战斗层中具有 MSW 原生 API 支持的项目,与游戏类型无关。不包括公式和理论。API 签名基于 Environment/NativeScripts/**/*.d.mlua。
0. 覆盖范围矩阵
| # | 层 | 原生支持 | 需要自定义 |
|---|---|---|---|
| 1 | 攻击判定 | AttackComponent + HitComponent(Box/Circle/Polygon) | Capsule/Cone/Ray、穿透次数 |
| 2 | 伤害模型 | CalcDamage/CalcCritical/GetCriticalDamageRate/GetDisplayHitCount 钩子 + HitEvent.Extra:any | 元素相性、复合公式 |
| 3 | 受击反应 | 各 Body 击退 API、基于 IsHitTarget 的无敌帧 | 硬直等级、状态效果 |
| 4 | 游戏手感 | 全部 6 项均为原生支持(受击停顿、震动、缩放、闪光、VFX、SFX) | — |
| 5 | 战斗状态 | StateComponent + DeadEvent/ReviveEvent、PlayerComponent HP/复活 | MP/耐力/怒气、仇恨 |
| 6 | 事件总线 | HitEvent/AttackEvent/StateChangeEvent/PlayerActionEvent + 自定义 @Event | 击杀/格挡事件 |
| 7 | AI | StateComponent(FSM)+ AIComponent(BT、4 个原生组合节点 + 自定义 extends CompositeNode)+ AIChaseComponent/AIWanderComponent、_UserService.UserEntities | 装饰器/记忆(黑板)、威胁表 |
| + | 伤害外观 | 3 个 DamageSkin* 组件 + DamageSkinService | — |
| + | 受击特效 | HitEffectSpawnerComponent(自动) | — |
| + | 化身动作 | AvatarStateAnimationComponent(State→MapleAvatarBodyActionState) | — |
0.5 参考资料 — 查阅指南
本 SKILL.md 仅介绍系统流程和原生 API 范围。实际模型 JSON、完整脚本代码和变化模式,请参阅以下 references/* 文件——直接阅读即可。
| 文件 | 范围 | 查阅时机 |
|---|---|---|
../msw-general/references/monster.md | 怪物 .model 组件组装 + ActionSheet + AI 选择 + 规范 Pattern A 脚本(Soldier 风格)+ HP/重生 + 生成 + 验证 | 构建可战斗的怪物时 |
references/hp-gauge.md | 基于 PixelRendererComponent 的头顶 HP 条完整实现 | 添加头顶 HP 条时 |
references/projectile.md | 投射物(无 Body 实体 + OnUpdate Translate)+ 追踪/穿透/溅射变体 | 构建箭矢、子弹、魔法弹等远程攻击时 |
references/ai-bt.md | 行为树——AIComponent + 组合节点(4 个原生 + 自定义)+ @BTNode + 自定义装饰器/记忆/威胁 | 需要基于 BT 的怪物/Boss AI 和多层决策时 |
优先顺序:本 SKILL.md(概念 + API 表格)→ 相关的 references/*(完整实现)。
1. 攻击判定
1-1. 形状与攻击触发
HitComponent.ColliderType 仅支持 Box / Circle / Polygon。其他形状必须通过组合近似实现。
AttackComponent:
Attack(Vector2 size, Vector2 offset, string attackInfo, CollisionGroup? cg) → table<Component>
Attack(Shape shape, string attackInfo, CollisionGroup? cg) → table<Component>
AttackFast(Shape shape, string attackInfo, CollisionGroup? cg) → void (for mass resolution, bullet hell)
AttackFrom(Vector2 size, Vector2 position, string attackInfo, CollisionGroup? cg) → table<Component>
emitter EmitAttackEvent(AttackEvent)
- 形状:
CircleShape(position, radius)/BoxShape(position, size, angle)/PolygonShape(position, points, angle)。对于轴对齐矩形,使用BoxShape(center, size, 0)——没有RectangleShape类型。无需旋转时,向BoxShape/PolygonShape传入angle = 0。 - 多边形受击区域:
HitComponent.PolygonPoints: SyncList<Vector2> AttackFast不会构建受击表 → 子弹地狱 / 大规模判定时性能更好
1-2. 目标筛选
| 一侧 | 重写方法 | 用途 |
|---|---|---|
| 攻击者 | AttackComponent:IsAttackTarget(defender, attackInfo) → boolean | 阵营 / 距离 / 状态 |
| 防御者 | HitComponent:IsHitTarget(attackInfo) → boolean | 无敌 / 免疫 |
任一方法返回 false → 排除该次命中。父类调用方式为 __base:IsAttackTarget(...)(mlua 专用)。
⚠ 重写时不要添加
@ExecSpace— 父类中的IsAttackTarget和IsHitTarget均未指定 ExecSpace(=All)。在子类中添加@ExecSpace("ServerOnly")等注解,会在运行时触发 LEA-3014SignatureMismatch。即使没有注解,调用路径也会经过服务端命中流程,因此实际执行发生在服务端。详情:msw-scripting/SKILL.md§9 "Method override"。
Attack*(..., cg)中的cg参数是对防御者HitComponent.CollisionGroup的精确匹配筛选条件(默认为CollisionGroups.HitBox)。它不会查询碰撞矩阵,攻击者自身的组也无关紧要。nil(省略)= 每个HitComponent都是候选对象。若传入的组有效,但与防御者实际所属组不同,则会命中 0 个目标且不报错(静默失败);若引用的组不存在于当前世界的碰撞组集合中,则其行为等同于nil(所有组)。传入组(例如CollisionGroups.Monster)时,先将目标怪物的HitComponent.CollisionGroup设为同一组 — 示例工作区中的怪物会将其重写为Monster,因此示例攻击脚本可以正常工作。从其他世界导入的模型若其组 ID 不在当前世界的组集合中,则会静默注册到Default组 — 使用nil或CollisionGroups.Default进行检测。- 重复命中防止 / 穿透 / 最大命中数:原生不支持。通过
Attack返回的表格和table<Entity, boolean>缓存,在脚本中自行管理。
1-3. attackInfo 标记
用于扩展的字符串,会传递到 CalcDamage/IsHitTarget/GetDisplayHitCount。取值约定由项目自行决定。建议使用 "melee.light"、"dot.poison" 这类命名空间格式。
1-4. ⚠️ IsLegacy
只有当 HitComponent.IsLegacy = false 时,ColliderType/ColliderOffset/PolygonPoints 才有效。BoxOffset/ColliderName 已弃用。
1-5. 各攻击形式的形状映射
| 形式 | 形状构造 |
|---|---|
| 正面近战 Box | BoxShape(pos + LookDirectionX*offset, size, 0) — 参见 DefaultPlayer PlayerAttack |
| 圆形 AoE | CircleShape(self.WorldPos, radius) |
| 投射物 | 生成一个无 Body 的模型(仅 Sprite+Transform),并在 OnUpdate(delta) 中调用 TransformComponent:Translate(speed*delta, 0) + 基于距离的命中检测 + _EntityService:Destroy。移动规则见 §1-6,完整实现 → references/projectile.md |
MSW 没有专用的投射物系统 — 通过实体 + AttackComponent 组合实现。
1-6. 连续移动 — 投射物、怪物和 AI 的通用规则
连续移动(追逐、飞行、自动移动)基于每帧调用 OnUpdate(delta)。通过定时器(SetTimerRepeat(0.1~0.15s))移动会产生 6~10 Hz 的瞬移效果,看起来很卡顿。
每种目标建议使用的 API
| 目标 | 是否需要 Body? | 移动 API | 原因 |
|---|---|---|---|
| 怪物 / NPC / AI | 需要(地图类型对应的 Body + MovementComponent) | MovementComponent:MoveToDirection(dir, 0) + MovementComponent.InputSpeed | MovementComponent.d.mlua:1——可控制 Rigid/Kinematic/Sideview 这三种类型。InputSpeed 属于 MovementComponent(.d.mlua:7),因此并非仅限 Player。第二个参数 0——deltaTime 仅在梯子上生效(.d.mlua:32)。官方 BT 示例 ActionFollow/ActionMoveRandom 也使用 0。 |
| 投射物 / 宝石 / 掉落物品 / 特效 | 不需要(Sprite+Transform+Trigger) | 每帧执行 self.Entity.TransformComponent:Translate(speed*delta, 0) | 没有 Body 时,直接操作 Transform 是安全的。这种模式源自官方教程“Create a Long-Range Projectile”。 |
| 直接控制 Rigidbody(进阶) | 需要 | body:AddForce(...)——持续加速 / 冲量 | RigidbodyComponent.d.mlua:71——MoveVelocity“主要由 MovementComponent 控制”,因此应优先通过 MovementComponent 控制,而不是直接写入。 |
关于
MovementComponent.InputSpeed在不同地图类型下的实际速度换算,请参阅msw-general/references/platform.md§10(MapleTile=×1,RectTile=÷1.2,SideView=×1.5)。
禁止使用的模式
| ❌ | 原因 |
|---|---|
使用 _TimerService:SetTimerRepeat(move, 0.1~0.15) 移动 | 以 6~10Hz 传送,没有逐帧插值 → 移动生硬 |
在 OnUpdate 中调用 body:SetPosition(...) / MovementComponent:SetPosition(...) | 两者都是传送方法(MovementComponent.d.mlua:37,以及各 Body 的 .d.mlua 中的 SetPosition)。用它们连续移动会很卡顿。仅用于一次性生成 / 重生 / 瞬移。 |
对有活动 Body 的实体使用 self.Entity.TransformComponent.Position = newPos | 物理引擎会在下一帧覆盖该值,并阻止网络同步。 |
不带 delta 的固定步长移动,例如 Translate(0.009, 0) | 受帧率影响。60FPS 和 30FPS 下的速度不同。 |
⚠ 谨慎对待官方 msw-search 中的反模式
mlua_Document_Retriever 会返回一篇得分很高的“使用 MovementComponent 控制实体移动”文档,但其正文展示的是一种反模式:在 OnUpdate 中每帧调用 MovementComponent:SetPosition(...) 移动——请忽略该文档,改用上表中的 MoveToDirection / Translate。FlappyFish Remake、Stopping the Taxi 和 Making a Moving Foothold 也展示了缺少 delta 或直接赋值 Position 的做法,引用时请谨慎。
MovementComponent 附加方式——怪物 / NPC
怪物模型默认不包含此组件。.model 必须包含以下所有组件。
| 组件 | 说明 |
|---|---|
MOD.Core.TransformComponent | 默认 |
MOD.Core.SpriteRendererComponent | 渲染器 |
| Body(地图类型对应) | RigidbodyComponent(MapleTile)/ KinematicbodyComponent(RectTile)/ SideviewbodyComponent(SideViewRectTile) |
MOD.Core.MovementComponent | 例如 InputSpeed = 2.0——移动 API 必需 |
交叉引用
- **击退(单次冲量)**不属于连续移动,因此请直接使用 §3-1。
- 按地图类型选择 Body / InputSpeed 换算:
msw-general/references/platform.md§4·§10
1-7. 基于碰撞的命中——选择检测方式(不要手动计算距离)
当需求是“基于碰撞/接触的命中”(玩家↔怪物接触伤害、危险区域、陷阱、重叠)时,应根据意图选择。逐帧计算距离并非默认方式——设计要求基于碰撞体接触时却使用距离计算,是需求方与实现方最常见的不匹配。
| 意图 | 检测方式 | 原因 |
|---|---|---|
| 主动攻击挥击/命中框/弹幕 | AttackComponent:Attack/AttackFast/AttackFrom → 对防御方的 HitComponent 进行判定,并触发 HitEvent | 完整流程:伤害皮肤、命中效果、IsHitTarget 无敌帧、OnHit |
| 身体接触/区域/陷阱重叠,目标任意或多个 | TriggerComponent + OnEnterTriggerBody/OnStayTriggerBody/OnLeaveTriggerBody(或 TriggerEnter/Stay/LeaveEvent)+ CollisionGroup 筛选,然后通过 HitEvent 流程处理伤害 | 引擎端重叠检测:遵循实际碰撞体形状(Box/Circle/Polygon)、ColliderOffset 和分组筛选 |
| 追踪/单个已知目标 | 逐帧执行 OnUpdate 距离检测(references/projectile.md) | 仅当事先确切知道只有一个目标实体时才合理 |
- ❌ 对玩家↔怪物或多目标碰撞逐帧执行
math.sqrt(dx*dx+dy*dy) < r:这种方式会悄悄地近似为一个以变换为中心的圆(忽略每个碰撞体的形状/大小/ColliderOffset),跳过CollisionGroup筛选,并且每帧的开销为 O(攻击者 × 目标)。所谓“基于碰撞的命中”指的是原生碰撞体,而不是轮询距离。 CollisionGroup默认值为CollisionGroups.TriggerBox。重叠期间,OnStayTriggerBody会每帧触发——请通过冷却时间限制伤害,不要每帧都造成伤害。- 检测 ≠ 伤害应用:原生重叠事件触发后,仍须通过
HitEvent/AttackComponent造成伤害——绝不可直接扣除 HP。
2. 伤害模型
AttackComponent:
method integer CalcDamage(attacker, defender, attackInfo) -- default 1 (ExecSpace=All)
method boolean CalcCritical(attacker, defender, attackInfo) -- default false (ExecSpace=All)
method float GetCriticalDamageRate() -- default 2.0 (ExecSpace=All)
method int32 GetDisplayHitCount(attackInfo) -- default 1 (ExecSpace=All)
method void OnAttack(defender) -- (ExecSpace=All)
HitComponent:
method void OnHit(Entity attacker, integer damage, boolean isCritical, string attackInfo, int32 hitCount)
emitter EmitHitEvent(HitEvent)
⚠ 上述所有钩子在父类中都具有未指定的 ExecSpace(=All)。重写时添加
@ExecSpace("ServerOnly")等标注会触发 LEA-3014SignatureMismatch。删除标注,仅声明method ...。详情:msw-scripting/SKILL.md§9 "Method override"。
2-1. HitEvent 负载
AttackCenter: Vector2
AttackerEntity: Entity (nilable)
Damages: List<integer> -- multi-hit split
Extra: any -- ★ extension slot (knockback/stun/element/tags)
IsCritical: boolean
TotalDamage: integer
FeedbackAction: HitFeedbackAction -- ⚠ entire enum deprecated
将辅助信息(击退向量、硬直时间、元素)放在 Extra 表中。
2-2. AttackEvent 负载
仅有一个字段:DefenderEntity: Entity。攻击者是处理器的 self。
2-3. ⚠️ 反模式:直接扣除 HP——不要绕过 HitEvent
直接扣除防御方的 HP,例如 monster.Hp -= damage / target.MonsterAI.HP -= damage,不会触发 HitEvent——伤害皮肤、命中效果、IsHitTarget 免疫和 OnHit 重写都会被悄悄跳过。
对于玩家侧自定义伤害(持续/光环/持续伤害),绕过流程还会导致角色动画异常:AvatarStateAnimationComponent 只响应 StateChangeEvent,因此即使 HP 条下降,玩家角色仍会保持待机状态。如果必须在不触发 HitEvent 的情况下造成伤害,还要手动调用 StateComponent:ChangeState("HIT")(大写键——参见 §10),并在死亡/复活时调用 PlayerComponent:ProcessDead() / ProcessRevive()。否则,受击/死亡动作会悄悄失效,且不会报错。
3. 受击反应
3-1. 击退 — 各 Body 的 API
| Body(地图类型) | 实现方式 |
|---|---|
| Rigidbody(MapleTile) | body:AddForce(Vector2(dir*5, 3)) ★推荐 · SetForce · JustJump(Vector2(0, 4))(垂直方向) |
| Kinematicbody(RectTile / 俯视视角) | body.MoveVelocity = Vector2(dir*5, 0) — 不要使用 AddForce |
| Sideviewbody(SideViewRectTile) | body.MoveVelocity + body.JumpSpeed |
- Rigidbody 会由引擎自动阻尼。Kinematic/Sideview 必须在
OnUpdate中手动阻尼(MoveVelocity *= 0.9)。 - 墙壁反弹:订阅
FootholdCollisionEvent并反转速度。 - 击退是一次性冲量,不要将其与持续移动(追逐、飞行)混淆。持续移动请参见 §1-6。
- ⚠ 禁止:在具有活动 Body 的实体上直接设置
TransformComponent.Position→ 网络同步会被阻止。body:SetPosition(...)是传送方法,因此不要在OnUpdate循环中调用(§1-6)。
3-2. 无敌帧
标准模式:基于 _UtilLogic.ElapsedSeconds 检查截止时间 + 从 HitComponent:IsHitTarget 返回 false。DefaultPlayer 默认的 PlayerHit.mlua 已直接提供此模式(§9-4)。
另一种方式:无敌期间,将 HitComponent.CollisionGroup 切换到单独的组 → 直接排除碰撞判定。此方式更适合需要精确到帧的场景。
3-3. 状态效果(Buff/Debuff)
不原生支持。 直接实现 @Component BuffComponent + 使用 _TimerService:SetTimerRepeat 进行计时 + 广播自定义的 StatusAppliedEvent/StatusExpiredEvent。
对于单次简单的眩晕,使用 StateComponent:ChangeState("STUN") + 输入/AI 阻止标志即可。
4. 游戏手感 — 全部原生支持
| 元素 | API | ExecSpace |
|---|---|---|
| 命中停顿(全局) | _UtilLogic:SetClientTimeScale(float) — 0~100 | ClientOnly |
| 命中停顿(单个对象) | renderer.PlayRate = 0(Sprite/Skeleton/Avatar) | @Sync |
| 慢动作 | _UtilLogic:SetClientTimeScale(0.3) + 使用计时器恢复 | ClientOnly |
| 镜头震动 | cameraComp:ShakeCamera(intensity, duration, targetUserId?) | Client |
| 镜头缩放 | cameraComp:SetZoomTo(percent, duration, targetUserId?) · 需先设置 IsAllowZoomInOut=true | Client |
| 命中闪白 | spriteRenderer.Color = Color(r,g,b,a) → 使用计时器恢复 | @Sync |
| HDR 颜色过亮 | Color.HSVToRGB(h, s, v, hdr=true) — 允许使用大于 1.0 的值 | — |
| 固定位置 VFX | _EffectService:PlayEffect(clipRUID, instigator, pos, zRot, scale, isLoop?, options?) → 返回 serial | — |
| 附着式 VFX | _EffectService:PlayEffectAttached(clipRUID, parent, localPos, localZRot, localScale, isLoop?, options?) | — |
| 移除 VFX | _EffectService:RemoveEffect(serial) | — |
| 2D SFX | _SoundService:PlaySound(id, volume, targetUserId?) | Client |
| 3D SFX | _SoundService:PlaySoundAtPos(id, pos, listener, volume) | Client |
| 循环 SFX | PlayLoopSound / PlayLoopSoundAtPos | Client |
| 附着式 SFX | SoundComponent:Play() · 通过 Pitch 0~3 实现音高随机化 | Client |
| BGM | _SoundService:PlayBGM(id, volume) / StopBGM(immediately) | Client |
| 预加载 | _SoundService:LoadSound(id) | ClientOnly |
PlayEffect 的选项键:FlipX, FlipY, SortingLayer, OrderInLayer, Alpha, StartFrameIndex, EndFrameIndex, PlayRate, SyncFlip, Color, MaterialID, IgnoreMapLayerCheck, LitMode
获取当前镜头:_CameraService:GetCurrentCameraComponent()。
ParticleService — 内置粒子
仅由枚举值驱动的通用粒子效果,无需 RUID。分为 3 类:
-- BasicParticle: general-purpose presets (no RUID needed)
integer _ParticleService:PlayBasicParticle(BasicParticleType, Entity instigator, Vector3 pos, number zRot, Vector3 scale, boolean isLoop, Dictionary options)
integer _ParticleService:PlayBasicParticleAttached(BasicParticleType, Entity parent, Vector3 localPos, number localZRot, Vector3 localScale, boolean isLoop, Dictionary options)
-- SpriteParticle: custom sprite as a particle (spriteRUID required)
integer _ParticleService:PlaySpriteParticle(SpriteParticleType, string spriteRUID, Entity instigator, Vector3 pos, number zRot, Vector3 scale, boolean isLoop, Dictionary options)
integer _ParticleService:PlaySpriteParticleAttached(SpriteParticleType, string spriteRUID, Entity parent, Vector3 localPos, number localZRot, Vector3 localScale, boolean isLoop, Dictionary options)
-- AreaParticle: environmental particles over a wide area (areaSize added)
integer _ParticleService:PlayAreaParticle(AreaParticleType, Vector2 areaSize, Entity instigator, Vector3 pos, number zRot, Vector3 scale, boolean isLoop, Dictionary options)
void _ParticleService:RemoveParticle(integer serial)
选项键:Color, SortingLayer, OrderInLayer, ParticleSize, ParticleCount
循环粒子(isLoop=true)必须通过 RemoveParticle(serial) 清理。将 serial 存储在 self._T 中,以便之后移除。
完整的 BasicParticleType 列表
| 系列 | 名称 | 描述 |
|---|---|---|
| 爆炸/冲击 | SparkExplosion | 火花(一次性)——通用撞击效果 |
SparkLoop | 连续火花 | |
SparkRadialExplosion | 向四周散射的火花 | |
SmallExplosion | 小型爆炸 + 烟雾 | |
BigExplosion | 大型爆炸 + 烟雾 | |
TinyExplosion | 极小爆炸(忽略 Color 选项) | |
DustExplosion | 环形冲击波 + 烟雾(忽略 Color 选项) | |
EnergyExplosion | 环形冲击波,随后向中心汇聚 | |
CircleBurst | 环形光爆 | |
PillarBurst | 环形光爆 + 定向光 | |
| 火焰/烈焰 | FireField | 卡通火焰 |
FireFieldIntense | 强烈的卡通火焰 | |
FireBall | 单点火焰 | |
FlameThrower | 喷火流 | |
LargeFlames | 从地面升起的大型火焰 | |
MediumFlames | 从地面升起的中型火焰 | |
TinyFlames | 从地面升起的微小火焰 | |
WildFire | 巨大的火焰柱(忽略 Color 选项) | |
| 闪电/电流 | LightningOrbSharp | 球形电粒子 |
LightningStrikeSharp | 闪电 | |
LightningStrikeSharpTall | 高耸的闪电 | |
LightningOrbSoft | 电波发射 | |
LightningBlast | 周期性电波 | |
LightningStrike | 周期性闪电 | |
LightningStrikeTall | 周期性高耸闪电 | |
| 增益/魔法 | Aura | 从地面升起的极光 |
Buff | 从地面升起的强光 | |
Charge | 大型粒子向一点汇聚 | |
ChargeOrb | 粒子向一点汇聚 | |
Enchant | 强光,周围伴有微光/粒子 | |
SpinField | 环绕旋转圆圈的粒子 | |
StarVortex | 星光向中心汇聚 | |
Nova | 宽阔的环形波 | |
UpperCylinder | 从地面升起的光柱 | |
| 其他 | Firework | 烟花 |
FireworkCluster | 多个烟花同时绽放 | |
FireFlies | 萤火虫 | |
GoopSpray | 向侧面喷射液体 | |
GoopSprayEffect | 向下喷射液体 | |
DustStorm | 范围宽广的沙尘暴 | |
RisingSteam | 从地面升起的白色雾气 | |
BigSplash | 大型水花 | |
Shower | 向一处浇水 |
完整的 SpriteParticleType 列表(8 种)
| 名称 | 描述 |
|---|---|
BurstBig | 精灵以放射状出现 |
SpawnField | 粒子 + 精灵在圆形区域内出现 |
BurstNova | 粒子 + 精灵以环形爆发 |
SimpleSpawn | 简单的粒子 + 精灵显现效果 |
Burst | 粒子 + 精灵四散 |
Stream | 移动时沿特定方向生成 |
StreamSharp | 沿特定方向移动的细线 |
AdditiveColor | 应用于精灵的颜色效果 |
完整的 AreaParticleType 列表(12 种)
| 名称 | 描述 |
|---|---|
Rain | 雨 |
Snow | 雪 |
FogCalm | 雾 |
FogHeavy | 向下沉降的浓雾 |
FogLively | 上升的雾 |
CalmStarField | 升起的星群 |
StarFieldSimple | 闪烁的星群 |
StarFog | 星星 + 星云粒子(静止) |
StarFogFlow | 星星 + 星云粒子(上升) |
Windlines | 细线 |
WindlinesBig | 细线 + 粗线 |
WindlinesSpeedy | 快速直线 |
选择 EffectService 还是 ParticleService
| 情况 | 推荐方案 |
|---|---|
| MapleStory 技能 / 命中动画(特定画面) | EffectService(指定 RUID) |
| 常规命中 / 爆炸(快速实现) | ParticleService.BasicParticle |
| 将自定义图像散布为粒子 | ParticleService.SpriteParticle |
| 雨 / 雪 / 雾等环境氛围 | ParticleService.AreaParticle |
| 增益光环等持续效果 | 两者均可,设置 isLoop=true |
| 丰富的分层效果 | 组合使用 EffectService 和 ParticleService |
服务端事件 → 客户端效果的标准模式:更改
@Sync属性 → 在OnSyncProperty(ClientOnly)中检测 → 调用 EffectService/ParticleService。
5. 死亡 / 复活
| 事件 | 触发条件 | 载荷 |
|---|---|---|
DeadEvent | StateComponent:ChangeState("DEAD") 时自动触发 | 无 |
ReviveEvent | PlayerComponent:Respawn() 时自动触发(仅玩家) | 无 |
StateChangeEvent | 每次状态转换时自动触发 | CurrentStateName、PrevStateName |
追踪击杀者:DeadEvent 不含载荷 → 在 HandleHitEvent 中缓存 self.LastAttacker = event.AttackerEntity,并在 HandleDeadEvent 中使用。
对于玩家专属的死亡 / 复活,优先使用 §9-1 PlayerComponent.Respawn/ProcessDead/ProcessRevive。
6. 事件总线
| 逻辑事件 | MSW 实现 |
|---|---|
| OnAttackStart | OnAttack 钩子或自定义 AttackStartEvent |
| OnAttackHit / OnDamageTaken | 原生 HitEvent |
| OnAttackMiss | 自定义——当 IsAttackTarget 返回 false 时调用 SendEvent |
| OnCriticalHit | 由 HitEvent.IsCritical 标志覆盖 |
| OnDeath / OnRevive | 原生 DeadEvent/ReviveEvent |
| OnStateChange | 原生 StateChangeEvent |
| OnKill / OnBlocked / OnParry / OnStatusApplied | 自定义 @Event |
6-1. 自定义事件规则
- 定义:
@Event script XxxEvent extends EventType+property声明 - 接收:
handler关键字(非方法)、@EventSender("Self" | "Service","XxxService" | "Logic","XxxLogic") - 连接 / 断开连接:
entity:ConnectEvent(XxxEvent, self.Handler)/ 在OnEndPlay中调用DisconnectEvent(引擎不会自动断开连接) - 全局:
@Logic CombatEventBusLogic单例 +@EventSender("Logic","CombatEventBusLogic")
7. AI — FSM(StateComponent)+ BT(AIComponent)+ 自定义脚本(模式 A),均兼容原生功能
| 模式 | 适用场景 | 参考 |
|---|---|---|
FSM(StateComponent + @State) | 简单敌人(3~5 个状态)、玩家 IDLE/HIT/DEAD、Boss 阶段、动画同步(AvatarStateAnimationComponent 自动映射 §10)。如果需要让 StateAnimationComponent 自动切换动画片段,必须设置 StateComponent.IsLegacy=false。 | ../msw-general/references/animation-state.md(统一状态机与动画管线) |
BT(AIComponent + 组合节点(4 个原生 + 自定义)+ @BTNode) | 巡逻 + 追击 + 攻击连招、多样化的 Boss 模式、组合节点 / 装饰器复用、按概率加权的动作。必须设置 StateComponent.IsLegacy=false。 | references/ai-bt.md |
使用自身状态的自定义脚本(持有 CurrentAIState 的 @Component,并直接赋值 SpriteRUID——Soldier 风格模式) | 不适用于 AIChase/AIWander 的行为(游荡 ↔ 站立 ↔ 说话 ↔ 攻击、受距离限制的攻击、对话待机)。无需 AIChaseComponent/AIWanderComponent,也无需设置 IsLegacy=false——脚本绕过 ActionSheet 管线。StateComponent 仅用于 IDLE ↔ DEAD。 | ../msw-general/references/monster.md §7 "Canonical Pattern A Scripts (Soldier)" |
7-1. FSM — StateComponent(摘要)
StateComponent + @State script XxxStateType extends StateType(生命周期:OnEnter/OnUpdate/OnExit/OnConditionCheck)。仅以下状态会自动注册:IDLE/DEAD(如果存在 HitComponent,还会注册 HIT;如果存在 AIChase/AIWander,还会注册 MOVE)——ATTACK/PATROL/STUN/PHASE2 等状态都必须在 OnBeginPlay 中通过 AddState("name", XxxStateType) 预先注册。自动转换使用 AddCondition(from, to) + 每帧调用的 OnConditionCheck()。
⚠ 状态名称必须使用大写;未注册的名称会立即产生
[LEA-3005] InvalidArgument : 'stateName'。在AvatarStateAnimationComponent.StateToAvatarBodyActionSheet中注册键名不会自动在StateComponent中注册该状态——两者相互独立。
完整实现 → ../msw-general/references/animation-state.md(FSM 编写、ChangeState 失败情况对照表、标准的 PATROL/CHASE/ATTACK/HIT/DEAD 怪物模式,以及状态→动画管线都在此处一并介绍——这是同一底层系统的两个视角)
7-2. BT — AIComponent(摘要)
AIComponent + SequenceNode/SelectorNode/RandomSelectorNode/ParallelNode + @BTNode Action Nodes + 原生 AIChaseComponent/AIWanderComponent。全部 4 种 Composite 类型均为原生支持,也可通过 @BTNode ... extends CompositeNode 编写自定义子节点流转策略(ChildCount/ChildBehave);Decorator/Memory(Blackboard)/Threat Table 必须手动实现。
⚠ 使用自定义 BT 时,请从
.model中移除AIChaseComponent/AIWanderComponent。
完整实现 → references/ai-bt.md
完整的怪物实体构成 →
../msw-general/references/monster.md此 SKILL.md 仅介绍战斗相关内容(ATTACK/HIT/DEAD + DeadEvent/ReviveEvent + BT 入口)。有关通用 mlua 状态机/脚本编写模式,请参阅
msw-scripting。
8. UI 原生支持
| UI | API |
|---|---|
| HP 条(固定在屏幕上) | 使用 HP 量表切片精灵图(image_ruid = "f0911af597259044aa624a11332c0595")通过 SpriteGUIRendererComponent 填充,并通过左侧为轴心的 UITransformComponent 调整宽度。用户可拖动的控件使用 SliderComponent,只读 HP 量表则不适用。⚠ 仅限 UI 实体 |
| 伤害数字 | 3 个 DamageSkin* 组件 + DamageSkinService — §11 |
| 准星 | .ui 中的 SpriteGUIRendererComponent |
| 连击计数器 / 增益图标 | TextGUIRendererComponent + SpriteGUIRendererComponent |
世界空间 HP 条(头顶显示):原生不支持。有两种实现方案:
| 方案 | 方法 | 适用场景 |
|---|---|---|
| 轻量级 | 在子实体的 SpriteRendererComponent 上调整 LocalScale.x = hp/maxHp,或使用 TiledSize.x(搭配 SpriteDrawMode.Tiled) | 快速原型、简单量表 |
| 完整方案 | 基于 PixelRendererComponent — 完整实现 references/hp-gauge.md | 生产级,可同时显示大量怪物 |
9. DefaultPlayer 战斗原生功能
玩家实体原生具备 HP、复活和输入功能。不要创建自定义的 Hp/MaxHp 属性——请使用 PlayerComponent。
PlayerComponent/PlayerControllerComponent的完整属性/方法表请参阅msw-defaultplayer/SKILL.md。此处仅介绍战斗必需内容。
9-1. 核心战斗 API
| 项目 | 用法 |
|---|---|
| HP 扣减 | self.Entity.PlayerComponent.Hp -= event.TotalDamage |
| 死亡检查 | PlayerComponent:IsDead() |
| 复活 | PlayerComponent:Respawn() — RespawnPosition → SpawnLocation → map entry point。自动发出 DeadEvent/ReviveEvent |
| 仅客户端死亡处理 | @ExecSpace("Client") ProcessDead(targetUserId?) / ProcessRevive(targetUserId?) |
| 方向检查 ★ | PlayerControllerComponent.LookDirectionX(+1 表示向右,-1 表示向左)。不要使用 TransformComponent.Scale.x |
| 动作钩子覆写 | ActionAttack / ActionJump / ActionInteraction(key, isKeyDown) 等 |
| 接收动作事件 | EmitPlayerActionEvent(PlayerActionEvent) → §9-3 |
⚠ 自动攻击 / AI 触发的攻击必须先设置朝向。
LookDirectionX是可写的@Sync属性 — 赋值1/-1可使玩家头像朝向相应方向。默认输入流程只会根据移动输入更新该属性,因此在没有移动时触发的攻击(自动战斗循环、AI 计时、技能按钮)会沿用上一次的朝向(默认向左)— 头像会朝错方向,基于LookDirectionX的攻击框(§1-5)也会计算到错误的一侧。触发此类攻击前,应将朝向设为目标所在方向:controller.LookDirectionX = target.TransformComponent.WorldPosition.x >= self.Entity.TransformComponent.WorldPosition.x and 1 or -1。只赋值符号(±1),不要使用原始位置差值 — 攻击偏移会乘以LookDirectionX,因此非单位值会缩放 / 错位命中框。
9-3. PlayerActionEvent
property string ActionName -- "Attack" / "Jump" / "Crouch" / ...
property Entity PlayerEntity
默认模式是使用 PlayerAttack extends AttackComponent,接收 @EventSender("Self") handler HandlePlayerActionEvent(...),并根据 event.ActionName == "Attack" 进行分支处理。
9-4. 默认模板(RootDesk/MyDesk/)
可直接复制粘贴,无需修改。按需覆写:
| 文件 | 作用 | 要点 |
|---|---|---|
PlayerAttack.mlua | 正面盒状攻击 | 使用 LookDirectionX 确定方向,AttackFast + CollisionGroups.Monster,CalcDamage=50,30% 暴击 |
PlayerHit.mlua | 无敌帧 | ImmuneCooldown 属性,_UtilLogic.ElapsedSeconds 截止时间,覆写 IsHitTarget |
Monster.mlua | 怪物 HP | 自定义 @Sync Hp(不使用 PlayerComponent),HandleHitEvent → Dead/Respawn |
MonsterAttack.mlua | 基于精灵图尺寸的近战攻击 | 在 IsAttackTarget 中使用 isvalid(defender.PlayerComponent) + __base:IsAttackTarget(...) 父类调用 |
9-5. 时间参考
推荐使用 _UtilLogic.ElapsedSeconds(世界时钟,暂停 / 恢复时保持一致)。不要使用 os.clock()。
9-6. 标准 CollisionGroups
| 常量 | 用途 |
|---|---|
CollisionGroups.Player | 怪物 → 玩家攻击 |
CollisionGroups.Monster | 玩家 → 怪物攻击 |
CollisionGroups.HitBox | HitComponent.CollisionGroup 的默认值 |
10. 头像动作 — AvatarStateAnimationComponent
自动将 StateComponent 状态转换关联到头像动画。
@Sync property SyncDictionary<string, AvatarBodyActionElement> StateToAvatarBodyActionSheet -- IsLegacy=false
@Sync property SyncDictionary<string, string> ActionSheet -- IsLegacy=true (deprecated)
method void SetActionSheet(string key, string animationClipRuid)
method void RemoveActionSheet(string key)
method string StateStringToAnimationKey(string stateName)
emitter EmitBodyActionStateChangeEvent(BodyActionStateChangeEvent)
ChangeState("HIT")→ 自动播放映射的MapleAvatarBodyActionState.Hit- 与战斗相关的状态值:
Attack=3、Hit=14、Dead=10、Alert=4、Heal=13 - 固定为
IsLegacy=false;仅使用StateToAvatarBodyActionSheet
完整的头像组件说明(
AvatarRendererComponent等)请参阅msw-defaultplayer。本节仅介绍战斗动作映射。
11. 伤害皮肤(数字显示)
默认 RUID
| 用途 | RUID | 用于 |
|---|---|---|
| 命中 | 3271c3e79bf04ecba9a107d55495970d | 攻击者 DamageSkinSettingComponent.DamageSkinId 的默认值 |
| 受到命中 | 02c22d93421b4038b3c413b3e40b57ec | 防守方显示 — 手动调用 _DamageSkinService:Play |
| 治疗 | d58b67cf0f3a4eaf9fe1ad87c0ffac8a | 治疗 / 药水 — 手动调用 _DamageSkinService:Play |
11-1. 自动模式(基于组件)
在 Attack/AttackFast 上,如果存在以下全部 3 个组件,就会自动显示伤害数字:
| 一侧 | 组件 | 作用 |
|---|---|---|
| 攻击方 | DamageSkinSettingComponent | 要显示的皮肤/样式 |
| 防御方 | DamageSkinSpawnerComponent | 显示位置偏移 |
| 防御方 | DamageSkinComponent | 伤害数字主体(显示在实体上方) |
将这 3 个组件都添加到 .model 中,无需编写任何脚本代码即可显示伤害数字。
DamageSkinSettingComponent(攻击方)
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
DamageSkinId | DataRef | 命中 RUID(见上表) | 伤害数字皮肤 RUID |
DamageSkinScale | Vector2 | (1, 1) | 数字大小 |
Alpha | float | 1 | 不透明度 |
PlayRate | float | 1 | 播放速度 |
DelayPerAttack | float | 0.05 | 多段命中之间的间隔(秒) |
TweenType | DamageSkinTweenType | Default | 动画样式 |
LitMode | LitMode | Default | 光照影响 |
DamageSkinTweenType:Default(弹出)/Volcano(扇形散开)/Blade(重叠)/各类 *Mini(75% 缩放)
DamageSkinSpawnerComponent(防御方)
| 属性 | 类型 | 默认值 |
|---|---|---|
DamageSkinOffset | Vector2 | (0,0) |
11-2. 手动模式 — DamageSkinService
自动模式未覆盖的情况(治疗、Miss/Guard、非标准伤害来源)可直接调用 _DamageSkinService。
_DamageSkinService:Play(targetEntity, skinRuid, delay, damages:List<int>, tweenType, isCritical, offset, scale, playRate, alpha, litMode)
_DamageSkinService:PlayTextDamage(targetEntity, skinRuid, textType, tweenType)
_DamageSkinService:PreloadAsync(skinRuid, callback(success)) -- ClientOnly
DamageSkinTextType:Miss/Guard/Resist/Shot/Counter
⚠
_DamageSkinService:Play位于Client空间——要从服务器逻辑(扣除 HP 等)调用它,请将调用封装在@ExecSpace("Client")方法中,或修改@Sync属性并从OnSyncProperty触发调用。
⚠
Play()有 6 个必需参数。只传入 5 个可选参数中的部分参数会触发 LEA-3005InvalidArgument。
11-3. 用法示例
(a) 强调暴击 — 自动模式 + 动态缩放
当 IsCritical=true 时,自动模式会自动显示红色字体。要进一步强调,可暂时增大攻击方的缩放比例:
-- ⚠ AttackComponent hooks (CalcDamage/CalcCritical/GetCriticalDamageRate/GetDisplayHitCount/
-- IsAttackTarget/IsHitTarget/OnAttack) have an unspecified ExecSpace (=All) on the parent.
-- Adding @ExecSpace when overriding triggers LEA-3014 SignatureMismatch.
-- Details: msw-scripting/SKILL.md §9 "Method override → LEA-3014"
method integer CalcDamage(Entity attacker, Entity defender, string attackInfo)
return 100
end
method boolean CalcCritical(Entity attacker, Entity defender, string attackInfo)
return math.random() < 0.3
end
method float GetCriticalDamageRate()
return 2.5 -- 100 → 250
end
还可以通过设置 DamageSkinSettingComponent.TweenType = Volcano(扇形散开)或 Blade(重叠)来突出显示暴击。
(b) 治疗/恢复 — 手动调用
local HEAL_RUID = "d58b67cf0f3a4eaf9fe1ad87c0ffac8a"
@ExecSpace("Client")
method void ShowHeal(Entity target, integer amount)
_DamageSkinService:Play(
target, HEAL_RUID, 0,
{ amount }, -- damages
DamageSkinTweenType.Default,
false, -- isCritical
Vector2(0, 0.5), -- offset (above head)
Vector2(1, 1), 1.0, 1.0, LitMode.Default
)
end
(c) Miss/Guard/Resist 文本
local HIT_RUID = "02c22d93421b4038b3c413b3e40b57ec"
@ExecSpace("Client")
method void ShowMiss(Entity target)
_DamageSkinService:PlayTextDamage(
target, HIT_RUID, DamageSkinTextType.Miss, DamageSkinTweenType.Default
)
end
当 AttackComponent:IsAttackTarget 返回 false 时调用此方法 →“未命中动画 + 伤害 0”。
(d) 多段命中 — 单次调用拆分为 N 个数字
如果将 List 作为 _DamageSkinService:Play 的 damages 参数传入,数字会按 DelayPerAttack(攻击方组件的值)指定的间隔依次显示:
_DamageSkinService:Play(target, ATTACK_RUID, 0, { 12, 8, 14, 11, 9 },
DamageSkinTweenType.Default, false, Vector2(0,0), Vector2(1,1), 1, 1, LitMode.Default)
自动模式对 HitEvent.Damages(List)的处理方式相同——重写 GetDisplayHitCount(attackInfo) 以控制拆分数量。
(e) 预加载 — 避免首次显示卡顿
首次使用某个皮肤 RUID 时,纹理加载可能会有延迟。请在进入地图时预加载:
@ExecSpace("ClientOnly")
method void OnBeginPlay()
_DamageSkinService:PreloadAsync("3271c3e79bf04ecba9a107d55495970d", function(ok) end)
_DamageSkinService:PreloadAsync("02c22d93421b4038b3c413b3e40b57ec", function(ok) end)
_DamageSkinService:PreloadAsync("d58b67cf0f3a4eaf9fe1ad87c0ffac8a", function(ok) end)
end
(f) TweenType 的使用场景
| TweenType | 推荐场景 |
|---|---|
Default | 普通命中 |
Volcano | 暴击/范围命中(向上散开) |
Blade | 连续斩击/连击(数字重叠) |
*Mini | 中毒/燃烧等持续伤害造成的小额伤害——减少画面杂乱 |
(g) 阵营专属皮肤
若要为不同阵营(玩家与敌人、PvP 阵营等)使用不同的皮肤 RUID,可在运行时替换 DamageSkinSettingComponent.DamageSkinId:
self.Entity.DamageSkinSettingComponent.DamageSkinId = MY_TEAM_SKIN_RUID
12. 命中效果 — HitEffectSpawnerComponent
将其附加到防御方后,命中时会在 HitEvent 上自动播放命中效果。没有任何属性——只需将组件添加到 .model 中。
13. 完整战斗检查清单
- 攻击者模型:派生自
AttackComponent的脚本(+ 可选:DamageSkinSettingComponent) - 防御者模型:
HitComponent+HitEffectSpawnerComponent+(可选:DamageSkinSpawnerComponent+DamageSkinComponent) - HitComponent:
IsLegacy=false,设置ColliderType/BoxSize/CircleRadius,设置CollisionGroup - 状态动作:在
StateComponent中注册ATTACK/HIT/DEAD+AvatarStateAnimationComponent.StateToAvatarBodyActionSheet - HP 处理:玩家使用
PlayerComponent.Hp;怪物使用自定义@Sync Hp - 方向检查:使用
LookDirectionX(不要使用 Scale.x);对于自动触发或 AI 触发的攻击,在攻击前将其设置为朝向目标的方向(±1)——§9-1 - 时间参考:
_UtilLogic.ElapsedSeconds(不要使用 os.clock) - 事件清理:在
OnEndPlay中显式调用DisconnectEvent - 实体规则:不要直接为带有活动 Body 的实体赋值
TransformComponent.Position
14. 需要自定义实现的功能
Buff/Debuff · BT 装饰器/记忆(Blackboard)· 仇恨/威胁表 · 投射物池化 · 穿透/最大命中次数 · 硬直等级系统 · 资源(MP/体力/怒气)· 连击/取消窗口 · 格挡/招架 · 世界坐标→屏幕坐标转换 · 世界空间 HP 条
不在范围内
- 一般玩家相关主题(HP/移动/摄像机/服装除外):
msw-defaultplayer - 一般 mlua 语法/生命周期:
msw-scripting .model编写规则/模板:msw-general