关于 msw-avatar
msw-avatar 是用于 MapleStory Worlds 的头像服装与动画配置指南,面向编辑项目模型、地图和脚本的开发者。它说明如何为 17 个装备槽设置头像物品、同步编辑器状态,以及使用 AvatarRendererComponent 调整颜色和透明度。
主要功能
- 01编辑模型与地图中的服装配置
- 02映射 17 个装备槽与类别
- 03说明长外套及武器槽占用规则
- 04通过 RUID 指定头像物品
- 05使用渲染器设置颜色与透明度
适用场景
- 01为 DefaultPlayer 配置默认服装
- 02为地图中的 NPC 设置装备
- 03在游戏中实现头像闪色或淡出
MCP 相关
修改工作区文件后,需调用 msw-maker-mcp MCP 服务的 refresh 工具,让编辑器同步变更。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ msw-git/ msw-ai-coding-plugins-official --skill msw-avatar安装后在 Claude Code 中输入/msw-avatar确认已加载。
详细介绍
MSW Avatar(服装 · 动画)
Avatar 通过两个维度进行管理。
- 服装(外观):
MOD.Core.CostumeManagerComponent—— 装备了哪些物品(17 个槽位)。 - 动画(动作):
AvatarStateAnimationComponent+AvatarRendererComponent—— 播放哪个状态动画片段(14 个默认状态 + 自定义动作)。
直接编辑工作区文件,然后调用 msw-maker-mcp 的 refresh 工具,让编辑器加载更改。
本文档先介绍服装(基于文件编辑),最后介绍动画(基于脚本)。
工作区路径规则:地图
./map/、UI./ui/、脚本和其他资源./RootDesk/MyDesk/、DefaultPlayer/Player 等全局模型./Global/。
按目标查找编辑位置
| 目标 | 要编辑的文件 | 备注 |
|---|---|---|
| DefaultPlayer | ./Global/DefaultPlayer.model | 通过 Values 数组覆盖 CostumeManagerComponent 属性 |
| Player(基础) | ./Global/Player.model | 服装默认值通常在 DefaultPlayer.model 中覆盖,而不是在此处 |
| 放置在地图中的实体(NPC、怪物等) | ./map/{mapName}.map | 该实体 jsonString.@components 内的 CostumeManagerComponent 块 |
| 仅引用自定义模型的实体 | 对应的 .model(例如位于 ./RootDesk/MyDesk/ 下) | 如果地图中没有内嵌组件,且实体仅通过 modelId 绑定,则编辑模型一侧 |
读取(相当于 get):读取上述文件,并检查与 CostumeManagerComponent 相关的字段 / Values 条目。如果已连接 Maker MCP,可以使用 get_component 作为运行时 / 编辑器快照辅助工具(参见 msw-maker-mcp 技能)。
应用(相当于 set):将值写入文件,然后调用 refresh。
应用更改:MCP refresh
保存文件后,必须调用 msw-maker-mcp 服务的 refresh 工具,以同步 Maker 及其视觉状态。(参见 msw-maker-mcp 技能中的工具列表。)
RUID(资源唯一 ID)
写入服装的字符串是 avatar 物品 RUID(通常是 32 个字符的十六进制字符串)。
- 切勿猜测或伪造 RUID。请使用
msw-search技能查找——avatar RUID 工作流(默认身体 / 头部、物品详情、渲染组合)请参见../msw-search/references/resource/avatar.md;通用搜索请参见../msw-search/references/resource/search.md;单个物品详情请参见../msw-search/references/resource/detail.md。 - 脚本 API
SetEquip(MapleAvatarItemCategory, itemRUID)与编辑器 / 模型中存储的值使用同一个 RUID 字符串。 Custom*Equip槽位只接受纯 Guid。任何带前缀的形式——包括thumbnail://<ruid>——都会被静默拒绝,且槽位会保持未装备状态(不会报错或警告)。msw-search返回的 RUID 已经是纯 Guid;不要添加 scheme 前缀。更广泛的缩略图 / 图标规则请参见msw-sprite-ruid技能。
CostumeManagerComponent 概述
此组件附加到使用 avatar 的实体(玩家、NPC 等)。装备槽以 17 个字符串属性的形式公开,名称为 Custom*Equip;在脚本中,可通过 MapleAvatarItemCategory 枚举使用 GetEquip / SetEquip 访问它们。
其他同步属性
| 属性 | 类型 | 描述 |
|---|---|---|
| UseCustomEquipOnly | boolean(默认值为 false) | 当值为 true 时,忽略用户账号的默认服装,仅使用通过脚本/模型指定的服装。若要锁定世界内的外观,这一点很重要。 |
| DefaultEquipUserId | string | 克隆指定用户的装备,然后在此基础上应用自定义装备。也可以指定当前不在线的用户。如果该用户之后更换装备,反映出的外观也可能随之改变。 |
| EquippedItems | 只读 | 运行时的实际装备信息。无法通过脚本修改。 |
17 个槽位 ↔ 属性 ↔ MapleAvatarItemCategory
CostumeManagerComponent 的 17 个装备字符串字段与引擎枚举 MapleAvatarItemCategory 的对应关系如下。(枚举定义:参见 Environment/NativeScripts/Enum/MapleAvatarItemCategory.d.mlua。)
| # | 组件属性(字符串 RUID) | MapleAvatarItemCategory | 说明 |
|---|---|---|---|
| 1 | CustomBodyEquip | Body (1) | 皮肤/身体 |
| 2 | CustomHairEquip | Hair (3) | 头发 |
| 3 | CustomFaceEquip | Face (4) | 脸部/脸型 |
| 4 | CustomCapEquip | Cap (5) | 帽子 |
| 5 | CustomCapeEquip | Cape (6) | 披风 |
| 6 | CustomCoatEquip | Coat (7) | 上衣 |
| 7 | CustomLongcoatEquip | Longcoat (9) | 长外套——一种同时占用上衣和下装槽位的物品类别 |
| 8 | CustomPantsEquip | Pants (10) | 下装 |
| 9 | CustomGloveEquip | Glove (8) | 手套 |
| 10 | CustomShoesEquip | Shoes (12) | 鞋子 |
| 11 | CustomOneHandedWeaponEquip | OneHandedWeapon (13) | 单手武器 |
| 12 | CustomTwoHandedWeaponEquip | TwoHandedWeapon (14) | 双手武器——同时占用单手武器槽和副武器槽 |
| 13 | CustomSubWeaponEquip | SubWeapon (15) | 副武器 |
| 14 | CustomFaceAccessoryEquip | FaceAccessory (16) | 脸部饰品 |
| 15 | CustomEyeAccessoryEquip | EyeAccessory (17) | 眼部饰品 |
| 16 | CustomEarAccessoryEquip | EarAccessory (18) | 耳部饰品 |
| 17 | CustomEarEquip | Ear (19) | 耳朵(身体部位) |
没有对应 17 个字段的枚举值
| MapleAvatarItemCategory | 描述 |
|---|---|
| Head (2) | 接近“未用作装备”的类别——会自动处理,以匹配身体颜色。没有 CustomHeadEquip 字段。 |
| Invalid (0) | 用于检测错误/未定义值。 |
| Shield (11) | 根据枚举注释,它使用副武器槽。在存储中,最好将它视为与 CustomSubWeaponEquip 互斥。 |
互斥/槽位占用规则(必须了解)
-
Longcoat ↔ Coat + Pants
Longcoat 的设计用途是同时占用 Coat 和 Pants 槽位。装备长外套时,将长外套 RUID 放入CustomLongcoatEquip,并从逻辑上处理与上衣/下装的组合——通常使用长外套时,应将上衣/下装留空,或避免出现相互冲突的外观。 -
双手武器 ↔ 单手武器 + 副武器
TwoHandedWeapon 同时占用单手武器槽和副武器槽。使用双手武器时,应以CustomTwoHandedWeaponEquip为准,并确保单手武器/副武器字段也没有设置值——避免重复装备。 -
盾牌 ↔ 副武器
Shield 使用副武器槽。不要指望它能与CustomSubWeaponEquip中的另一件副武器共存。 -
空字符串 = 脱下装备
与脚本中的SetEquip(category, "")一样,在文件中将字段留为""表示该槽位未装备。
DefaultPlayer.model — 将服装放入 Values
在 ./Global/DefaultPlayer.model 的 ContentProto.Json.Values 数组中添加或修改条目。
- TargetType:
"MOD.Core.CostumeManagerComponent" - Name:上表中的一个属性名(例如
CustomCapEquip、UseCustomEquipOnly) - ValueType:遵循
DefaultPlayer.model中其他Values条目的相同模式。字符串使用System.String, mscorlib, ...,布尔值使用System.Boolean, mscorlib, ... - Value:RUID 字符串或
true/false
如果相同的 (TargetType, Name) 已存在,仅更新该条目;否则向数组追加一个新对象。
字符串槽位示例(仅展示结构;通过搜索替换 RUID)
{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "CustomCapEquip",
"ValueType": {
"$type": "MODNativeType",
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": "PUT_32_HEX_RUID_HERE"
}
UseCustomEquipOnly 示例
{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "UseCustomEquipOnly",
"ValueType": {
"$type": "MODNativeType",
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": true
}
地图实体 — 在 .map 文件中编辑
打开 ./map/ 下目标地图的实体记录。
- 在
ContentProto.Entities数组中查找目标实体(按名称/路径/ID)。 - 在
jsonString["@components"]中,找到带有"@type": "MOD.Core.CostumeManagerComponent"的对象。 - 直接在该对象上编辑
Custom*Equip、UseCustomEquipOnly、DefaultEquipUserId等属性。 - 确认
componentNames字符串列表中也列有MOD.Core.CostumeManagerComponent,并且该列表与组件数组一致。
如果地图使用仅二进制格式,编辑工具可能会因工作区策略而异。如果文件以 JSON 文本形式打开,请遵循上述结构。
将 GET /v3/avatars 结果映射到槽位
将 GET /v3/avatars 返回项目的 category 字段映射到 Custom*Equip 属性。搜索方法请参阅 msw-search 技能 → references/resource/avatar.md。
API category | Custom*Equip 属性 | MapleAvatarItemCategory |
|---|---|---|
body | CustomBodyEquip | Body (1) |
hair | CustomHairEquip | Hair (3) |
face | CustomFaceEquip | Face (4) |
faceaccessory | CustomFaceAccessoryEquip | FaceAccessory (16) |
eyeaccessory | CustomEyeAccessoryEquip | EyeAccessory (17) |
earaccessory | CustomEarAccessoryEquip | EarAccessory (18) |
cap | CustomCapEquip | Cap (5) |
cape | CustomCapeEquip | Cape (6) |
longcoat | CustomLongcoatEquip | Longcoat (9) |
coat | CustomCoatEquip | Coat (7) |
pants | CustomPantsEquip | Pants (10) |
glove | CustomGloveEquip | Glove (8) |
shoes | CustomShoesEquip | Shoes (12) |
weapon | CustomOneHandedWeaponEquip | OneHandedWeapon (13) |
twohandweapon | CustomTwoHandedWeaponEquip | TwoHandedWeapon (14) |
subweapon | CustomSubWeaponEquip | SubWeapon (15) |
shield | CustomSubWeaponEquip | Shield (11) — 与 SubWeapon 槽位共用 |
头像资源搜索参考
msw-search技能 →references/resource/avatar.md:关于GET /v3/avatars(服装搜索)、默认身体/头部、GET /v3/avatars/{ruid}、渲染合成等的详细信息。- 结合类别搜索和详情 API,收集装备 RUID。
Avatar 染色 / 透明度(视觉重染色)
对于任何附加了 AvatarRendererComponent 的实体(DefaultPlayer、带 Avatar 的 NPC、怪物),如需实现颜色和透明度效果(受击闪光、幽灵淡出、调色板替换等),请使用渲染器自身的方法。对 Avatar 实体使用 SpriteRendererComponent.Color 和 FlipX 不会产生任何效果(即使 isvalid(spriteRenderer) 返回 true,Avatar 渲染器仍会覆盖 Sprite 渲染器的输出)。
| 方法 | 签名 | 备注 |
|---|---|---|
SetColor | (r, g, b, a [, targetUserId]) | r/g/b/a 是 0~1 范围内的浮点数。为整个 Avatar 着色。客户端 ExecSpace。 |
SetAlpha | (a [, targetUserId]) | 0~1 范围内的浮点数。独立设置透明度。客户端 ExecSpace。 |
SetAvatarPartColor | (category, r, g, b, a [, targetUserId]) | 仅为一个 MapleAvatarItemCategory 槽位着色。 |
@ExecSpace("Client")
method void FlashRed()
local renderer = self.Entity.AvatarRendererComponent
if isvalid(renderer) == false then return end
renderer:SetColor(1.0, 0.25, 0.25, 1.0) -- red flash
wait(0.1)
renderer:SetColor(1.0, 1.0, 1.0, 1.0) -- restore
end
如需设置 Avatar 的朝向 / 翻转,请使用 MovementComponent 上的朝向 API(例如 MoveDirection),而不要设置 Sprite 级别的翻转,原因同样是设置后不会产生任何效果。
Avatar 动画——整体结构
Avatar 动画通过一个 3 层流水线运行。只修改其中一层,会导致其他层覆盖你的改动并产生意外动作。
[1] Input / game logic
│ PlayerControllerComponent · scripts
▼
[2] StateComponent ──── StateChangeEvent ────▶ AvatarStateAnimationComponent
(e.g. "ATTACK") (CurrentStateName) (StateToAvatarBodyActionSheet
or ActionSheet lookup)
│
▼
[3] AvatarRendererComponent ◀── BodyActionStateChange / ActionStateChanged ── body entity
(actual sprite playback)
关键区别:
| 术语 | 格式 | 示例 |
|---|---|---|
| 状态键 | 大写 | IDLE、MOVE、ATTACK、HIT、CROUCH、FALL、JUMP、CLIMB、LADDER、DEAD、SIT、ATTACK_WAIT |
| AvatarBodyActionStateName(Value 侧) | 小写 | stand、walk、attack、hit、crouch、fall、rope、ladder、dead、sit、alert、fly、blink、heal |
| MapleAvatarBodyActionState(枚举) | PascalCase | Stand、Walk、Attack、Hit、Crouch、Fall、Sit、Rope、Ladder、Dead、Blink、Fly、Heal、Alert、Invalid |
| CoreActionName / PartsActionName(实际 Sprite 动作 ID) | 小写 + 数字 | stand1、walk1、swingO1、shoot1、prone、jump、alert 等 |
常见混淆:
"attack"不是 State。State 是大写的ATTACK,映射 Value 是小写的attack(即MapleAvatarBodyActionState.Attack),随后会根据武器解析为swingO1/shoot1等 Sprite 动作 ID。从脚本触发该状态时,应调用StateComponent:ChangeState("ATTACK")(大写字符串)——"Attack"或"attack"会静默失效(没有错误,只是状态不会改变)。
AvatarStateAnimationComponent——状态 ↔ 动作映射
MOD.Core.AvatarStateAnimationComponent 包含这两套系统。
| 属性 | 使用时机 | 类型 | 备注 |
|---|---|---|---|
IsLegacy | 在两套系统之间切换 | boolean(默认 false) | true = 使用 ActionSheet,false = 使用 StateToAvatarBodyActionSheet |
ActionSheet | IsLegacy = true(旧版) | SyncDictionary<string, string> | State→AnimationKey,例如 "ATTACK" → "attack" |
StateToAvatarBodyActionSheet | IsLegacy = false(新版,默认) | SyncDictionary<string, AvatarBodyActionElement> | 例如 "ATTACK" → {AvatarBodyActionStateName="attack", PlayRate=1.33} |
StateToAvatarBodyActionSheet 默认映射(IsLegacy=false 时的 11 个默认键)
| 键(State) | AvatarBodyActionStateName | PlayRate | 触发条件(存在 PlayerControllerComponent 时) |
|---|---|---|---|
IDLE | stand | 1.0 | 无输入 |
MOVE | walk | 1.68 | 向左/向右移动 |
ATTACK | attack | 1.33 | 左 Ctrl(Attack 操作) |
HIT | hit | 1.0 | HitComponent 处理命中 |
CROUCH | crouch | 1.0 | 向下箭头 |
FALL | fall | 1.0 | 在空中下落 |
JUMP | fall | 1.0 | 空格(Jump 操作) |
CLIMB | rope | 1.0 | 进入绳索 |
LADDER | ladder | 1.0 | 进入梯子 |
DEAD | dead | 1.0 | 死亡 |
SIT | sit | 1.0 | C(Sit 操作) |
请注意,State 键为大写,而
AvatarBodyActionStateName的值为小写。
默认解析表:MapleAvatarBodyActionState → 实际动作 ID
AvatarBodyActionStateName 字符串("attack"、"stand" 等)会被转换为枚举 MapleAvatarBodyActionState,随后引擎会将其解析为以下默认值,并合成一个 ActionStateChangedEvent。
| MapleAvatarBodyActionState | CoreActionName | PartsActionName | PlayRate | PlayType |
|---|---|---|---|---|
| Stand | stand1 / stand2 | 相同 | 1 | ZigzagLoop |
| Walk | walk1 / walk2 | 相同 | 1 | Loop |
| Attack | alert(未装备武器时的默认值) | alert | 1 | Loop |
| Crouch | prone | prone | 1 | Loop |
| Fall | jump | jump | 1 | Loop |
| Sit | sit | sit | 1 | Loop |
| Rope | rope | rope | 1 | Loop |
| Ladder | ladder | ladder | 1 | Loop |
| Dead | dead | stand1 | 1 | Loop |
| Blink | blink | blink | 1 | Loop |
| Fly | fly | fly | 1 | Loop |
| Hit | alert | alert | 1 | ZigzagLoop |
| Alert | alert | alert | 1 | ZigzagLoop |
| Heal | heal | heal | 1 | Loop |
装备武器时,
Attack会自动替换为与武器类型匹配的精灵动作 ID(见下表)。手持单手剑时会挥剑,手持弓时会射箭。
各武器的 attack 解析——候选精灵动作 ID
触发 ATTACK 时,引擎会检查已装备的武器(MapleAvatarItemCategory),并播放以下动作 ID 之一。
| 武器类别 | 候选 CoreActionName / PartsActionName |
|---|---|
单手剑/匕首(OneHandedWeapon) | swingO1、swingO2、swingO3、stabO1、stabO2 |
双手剑/锤(TwoHandedWeapon) | swingT1、swingT2、swingT3、stabT1、stabT2 |
弓(TwoHandedWeapon,弓类) | swingT1、swingT3、shoot1 |
| 法杖/魔杖 | swingO1、swingO2、swingO3 |
| 未装备武器(默认身体) | 没有专用攻击动画片段 → 通过 alert 等动作显示 |
即使属于同一类别,使用的动作 ID 集合也可能因物品元数据而异。上表列出了 SDK 指南(
_ActionNameLogic)中使用的代表性候选项。
PlayerControllerComponent 和自动添加状态
将 MOD.Core.PlayerControllerComponent 附加到玩家实体后,以下 State 会自动添加到 StateComponent,并在按键输入时自动切换:
MOVE、CLIMB、LADDER、CROUCH、JUMP、FALL、ATTACK、ATTACK_WAIT、SIT
因此,当 DefaultPlayer 按下 Ctrl 时,ATTACK 状态会自动激活,映射的攻击身体动作(即对应武器的挥剑/射箭/挥杖动作)也会自动播放——即使没有额外脚本,剑仍会挥动。
自动播放 ↔ 手动 ActionStateChangedEvent 冲突(★ 常见陷阱)
症状:即使通过脚本发送了自定义动作(例如 shoot1)的 ActionStateChangedEvent,**剑挥动作(或武器的默认攻击动作)**仍会播放;或者自定义动作只显示一帧就立刻被覆盖。
原因:当 ATTACK 处于激活状态时,AvatarStateAnimationComponent 会持续重新发送映射的 attack 身体动作。你发送的一次性事件会立即被覆盖。
解决策略
| 策略 | 方法 | 适用场景 |
|---|---|---|
| A. 移除映射 | 调用 asac:RemoveActionSheet("ATTACK") 移除该键。然后通过 ActionStateChangedEvent 直接播放动作。 | 当你想用自定义动作(射箭、施法等)完全替换攻击动作时 |
| B. 更改映射 | 调用 asac:SetActionSheet("ATTACK", "<Body Action name>"),或将 StateToAvatarBodyActionSheet["ATTACK"] 更改为另一个 MapleAvatarBodyActionState。 | 当你想切换到另一种内置状态动画时(例如 ATTACK→heal) |
| C. 强制重置 | 发送 BodyActionStateChangeEvent,并设置 needResetAction=true。 | 当你想重新开始相同状态时 |
| D. 更换武器 | 将 CostumeManagerComponent 的武器槽替换为一个弓的 RUID。 | 当你只想更改攻击动作的武器变体时(最直观的选项) |
策略 A 示例 — 关闭挥剑动作,替换为射箭动作
@Component
script PlayerAttack extends AttackComponent
@HideFromInspector
property any Shape = nil
@ExecSpace("ServerOnly")
method void OnBeginPlay()
self.Shape = BoxShape(Vector2.zero, Vector2.one, 0)
-- Remove the attack(=sword swing) mapping that the engine auto-plays during ATTACK
local asac = self.Entity.AvatarStateAnimationComponent
if isvalid(asac) then
asac:RemoveActionSheet("ATTACK")
end
end
@ExecSpace("ServerOnly")
method void AttackNormal()
-- ... damage resolution ...
self:PlayShootAnimation()
end
@ExecSpace("Client")
method void PlayShootAnimation()
local body = self.Entity.AvatarRendererComponent:GetBodyEntity()
if isvalid(body) == false then return end
local event = ActionStateChangedEvent()
event.CoreActionName = "shoot1"
event.PartsActionName = "shoot1"
event.PlayType = SpriteAnimClipPlayType.Onetime
body:SendEvent(event)
end
@ExecSpace("ServerOnly")
@EventSender("Self")
handler HandlePlayerActionEvent(PlayerActionEvent event)
if event.ActionName == "Attack" then
self:AttackNormal()
end
end
end
RemoveActionSheet/SetActionSheet必须在服务端调用才能同步更改,因为StateToAvatarBodyActionSheet是一个@Sync属性。
策略 D 示例 — 通过 CostumeManagerComponent 装备弓
如果你在 ./Global/DefaultPlayer.model 的 Values 中添加弓的 RUID,引擎会在 ATTACK 期间自动选择 shoot1 动作,无需更改映射。
{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "CustomTwoHandedWeaponEquip",
"ValueType": {
"$type": "MODNativeType",
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": "<bow RUID — obtain via msw-search>"
},
{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "UseCustomEquipOnly",
"ValueType": {
"$type": "MODNativeType",
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": true
}
14 种状态与自定义动作——哪些需要你自行触发?
AvatarStateAnimationComponent / MapleAvatarBodyActionState 已知的 14 种身体动作与该集合之外的任意精灵动作 ID(例如 swingO2、shoot1、dance、cast1)会通过不同的路径处理。
引擎自动处理的 14 种身体动作(= MapleAvatarBodyActionState 的成员)
这些名称可用作 StateToAvatarBodyActionSheet/ActionSheet 的值。将某个 State 映射到其中一个动作后,它就会自动播放。
| 身体动作名称 | 枚举 | 含义 |
|---|---|---|
stand | Stand | 待机 |
walk | Walk | 移动 |
attack | Attack | 攻击(精灵 ID 会根据武器自动选择) |
hit | Hit | 受击 |
crouch | Crouch | 蹲伏 |
fall | Fall | 坠落 |
rope | Rope | 握住绳索 |
ladder | Ladder | 梯子 |
dead | Dead | 死亡 |
sit | Sit | 坐下 |
heal | Heal | 治疗 |
alert | Alert | 警戒 |
fly | Fly | 飞行 |
blink | Blink | 闪烁 |
其他情况 — 通过 ActionStateChangedEvent 直接播放
对于 14 个枚举成员之外的任意精灵动作 ID(例如 shoot1、swingO2、cast1、throw1、dance、cheer),请使用以下流程。
播放流程
- 从实体的
AvatarRendererComponent调用GetBodyEntity(),获取角色的身体实体。动画事件应发送到身体实体,而不是角色根实体。 - 创建一个
ActionStateChangedEvent并填写其字段。 - 通过
body:SendEvent(event)将事件发送到身体实体。 - 动画只需让各客户端看到,因此通常应将其限定在
@ExecSpace("Client")中。游戏逻辑(伤害、生成投射物等)应放在ServerOnly一侧。
ActionStateChangedEvent 的主要字段(构造函数:ActionStateChangedEvent(coreActionName, partsActionName, playRate=1, playType=Loop, startFrameIndex=0, endFrameIndex=2147483647))
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
CoreActionName | string | "" | 要在核心部件(身体)上播放的动画 ID。必填(例如 "shoot1"、"swingO1") |
PartsActionName | string | "" | 要在子部件上播放的动画 ID。必填,通常与 CoreActionName 的值相同 |
PlayRate | float | 1 | 播放速度倍率(1.0 = 正常速度,1.5 = 1.5 倍) |
PlayType | SpriteAnimClipPlayType | Loop | Onetime / Loop / ZigzagLoop。单次播放的动作请使用 Onetime |
StartFrameIndex | int32 | 0 | 起始帧(负值会被限制为 0) |
EndFrameIndex | int32 | 2147483647 | 结束帧(超出总帧数时会被限制) |
SpriteAnimClipPlayType:
| 值 | 含义 |
|---|---|
Onetime | 播放一次后停止 |
Loop | 从 0 播放到末尾,然后重复 |
ZigzagLoop | 从 0 播放到末尾,再回到 0,然后重复 |
BodyActionStateChangeEvent — 用于 14 种内置状态的高级事件
直接指定一个 MapleAvatarBodyActionState 枚举值。无需记忆每种武器的动作 ID,还可以通过 needResetAction=true 强制重新播放相同状态。
与 ActionStateChangedEvent 不同,SendEvent 的目标是角色根实体(self.Entity),而不是身体实体。
local event = BodyActionStateChangeEvent()
event.ActionState = MapleAvatarBodyActionState.Fly
event.needResetAction = true
event.startFrameIndex = 1
event.endFrameIndex = 2
self.Entity:SendEvent(event)
-- Internally converted to ActionStateChangedEvent("fly", "fly", 1, Loop, 1, 2) and dispatched
| 字段 | 描述 |
|---|---|
ActionState | MapleAvatarBodyActionState 枚举(Stand/Walk/Attack/Hit/...) |
needResetAction | 为 true 时,即使该状态正在播放,也会强制从头重新播放 |
playRate / startFrameIndex / endFrameIndex | 与 ActionStateChangedEvent 相同 |
如何选择
- 任意精灵动作 ID(
shoot1、swingO2、dance等)→ActionStateChangedEvent(发送到身体实体) - 14 种枚举状态之一(Stand/Walk/Attack/...)→
BodyActionStateChangeEvent(发送到根实体)
示例 — 播放射箭(shoot)动画
一种常见模式:服务器处理攻击输入并生成投射物;客户端播放 shoot1 动作。
@Component
script PlayerAttack extends Component
property string ArrowModelId = "model://bc9f9d0e-2b5d-4b3b-a115-d857f85e9145"
@HideFromInspector
property integer ArrowCount = 0
@ExecSpace("ServerOnly")
method void FireArrow()
if self.ArrowModelId == nil or self.ArrowModelId == "" then
log_warning("PlayerAttack: ArrowModelId is not set")
return
end
local playerController = self.Entity.PlayerControllerComponent
local transform = self.Entity.TransformComponent
if isvalid(playerController) == false or isvalid(transform) == false then
return
end
local dirX = playerController.LookDirectionX
if dirX == 0 then dirX = 1 end
local worldPos = transform.WorldPosition
local spawnPos = Vector3(worldPos.x + 0.35 * dirX, worldPos.y + 0.35, worldPos.z)
self.ArrowCount += 1
local arrowName = "PlayerArrow_" .. tostring(self.ArrowCount)
local parent = self.Entity.CurrentMap
if isvalid(parent) == false then
parent = self.Entity.Parent
end
local arrow = _SpawnService:SpawnByModelId(self.ArrowModelId, arrowName, spawnPos, parent)
if isvalid(arrow) == false then
log_warning("PlayerAttack: failed to spawn arrow")
return
end
local arrowProj = arrow.ArrowProjectile
if isvalid(arrowProj) then
arrowProj:Fire(Vector2(dirX, 0))
end
self:PlayShootAnimation()
end
@ExecSpace("Client")
method void PlayShootAnimation()
local avatarRenderer = self.Entity.AvatarRendererComponent
if isvalid(avatarRenderer) == false then
return
end
local body = avatarRenderer:GetBodyEntity()
if isvalid(body) == false then
return
end
local event = ActionStateChangedEvent()
event.CoreActionName = "shoot1"
event.PartsActionName = "shoot1"
event.PlayRate = 1.5
event.PlayType = SpriteAnimClipPlayType.Onetime
body:SendEvent(event)
end
@ExecSpace("ServerOnly")
@EventSender("Self")
handler HandlePlayerActionEvent(PlayerActionEvent event)
local ActionName = event.ActionName
if ActionName == "Attack" then
self:FireArrow()
end
end
end
判断流程
- 你想播放的动作是否属于14 种内置状态(
stand、walk、attack、hit、crouch、fall、rope、ladder、dead、sit、heal、alert、fly、blink)之一?- 是 → 只需在
AvatarStateAnimationComponent中对应的插槽里指定动画片段,无需脚本。 - 否 → 继续下一步。
- 是 → 只需在
- 对于自定义动作(例如
shoot1、cast1、dance),创建一个ActionStateChangedEvent,并将其SendEvent到AvatarRendererComponent:GetBodyEntity()返回的身体实体。 - 拆分执行空间:在服务器(
ServerOnly)处理输入和伤害判定,在客户端(Client)播放动画。
常见错误
- 混淆 State 键与 AvatarBodyActionStateName(即枚举)。 State 键使用大写字母(
ATTACK);映射值使用小写字母(attack)。如果在StateToAvatarBodyActionSheet中交换 Key/Value,映射会静默失效。 - 仅发送 ActionStateChangedEvent,却未禁用自动播放。 按下 Ctrl 时,
ATTACK状态会自动激活,映射的攻击身体动作会立即覆盖你的事件。若要使用自定义攻击动作,必须通过RemoveActionSheet("ATTACK")或SetActionSheet("ATTACK", "<desired motion>")清除映射。 - 为
ActionStateChangedEvent设置了错误的 SendEvent 目标:目标必须是AvatarRendererComponent:GetBodyEntity()返回的身体实体。将其发送到self.Entity(Avatar 根实体)或组件都不会播放。(相反,BodyActionStateChangeEvent要发送到根实体。) - 在 DefaultPlayer 形状的实体上直接写入
AvatarBodyActionSelectorComponent.ActionState(运行着PlayerControllerComponent、StateComponent和AvatarStateAnimationComponent)。控制器每个 tick 都会重新评估地面、移动和输入状态,并在状态转换时调用ChangeState;产生的StateChangeEvent → BodyActionStateChangeEvent会重绘选择器,静默丢弃你的写入。请改用StateComponent:ChangeState("UPPERCASE_KEY")。只有在没有这套控制器组件的 NPC/怪物上,直接写入选择器才会生效。 - 尝试将任意状态名称写入
AvatarStateAnimationComponent。 不属于 14 个枚举成员(MapleAvatarBodyActionState)的值(例如shoot、cast、dance)都会被忽略。自定义 ID 必须通过ActionStateChangedEvent设置。 - 忘记设置
PartsActionName。 如果只设置CoreActionName,子部件(武器、帽子、披风等)就不会解析,因此可能出现上半身在动,而武器保持静止的情况。请使用与CoreActionName相同的值。 - 未设置
PlayType。 默认值为Loop,会导致单次动作无限重复。对于单次动作,请显式设置SpriteAnimClipPlayType.Onetime。 - 在客户端调用
RemoveActionSheet/SetActionSheet。StateToAvatarBodyActionSheet是@Sync属性——必须在服务端调用这些方法,才能让所有客户端都收到更改。 - 忘记区分服务端/客户端执行空间。 游戏逻辑(伤害、投射物)=
ServerOnly;动画播放 =Client。将两者混在一起会导致每个客户端重复播放,或缺少视觉效果。 - 期望未装备弓时也能播放弓的动作。 播放
shoot1会让身体摆出弓箭姿势,但如果CustomTwoHandedWeaponEquip中没有设置弓的 RUID,手中就不会出现弓。想要自然的视觉效果,请同时设置动作和武器。
相关技能
| 技能 | 用途 |
|---|---|
| msw-defaultplayer | ./Global/DefaultPlayer.model / Player.model 的结构与 Values 规则 |
| msw-search | RUID 查询,references/resource/avatar.md |
| msw-maker-mcp | refresh,也可选用 get_component / set_property(与运行时调整结合时) |
总结检查清单
服装
- 通过资源搜索 / Avatar 参考文档获取 RUID。
- DefaultPlayer / Player →
./Global/*.model中的Values(或基础模型定义)。 - 地图实体 →
./map/*.map中目标实体的@components。 - 遵守 Longcoat / 双手武器 / 盾牌 ↔ 副武器互斥规则。
- 通过
UseCustomEquipOnly决定是否忽略用户账号的默认服装。 - 保存后,调用
msw-maker-mcp→refresh。
动画
- 区分状态键(大写)与身体动作名称(小写)。 格式:
StateToAvatarBodyActionSheet["ATTACK"] = AvatarBodyActionElement("attack", 1.33)。 - 如果所需动作属于 14 种枚举身体动作(
stand·walk·attack·hit·crouch·fall·rope·ladder·dead·sit·heal·alert·fly·blink),只需定义映射即可。 - 对于其他动作 ID(
shoot1、swingT3、dance等),创建一个ActionStateChangedEvent,并将其SendEvent到AvatarRendererComponent:GetBodyEntity()的返回值。若要在枚举中重新启动某个状态,请使用BodyActionStateChangeEvent+ 根实体。 - 填写全部四个字段 —
CoreActionName/PartsActionName/PlayRate/PlayType,一次性动作使用SpriteAnimClipPlayType.Onetime。 - 检查是否与自动状态转换冲突。 在具有 PlayerControllerComponent 的实体上,输入会自动触发
MOVE/ATTACK/JUMP/...— 若要使用自定义攻击,请通过RemoveActionSheet或SetActionSheet清理冲突键(在服务器上调用)。 - 将游戏逻辑拆分到
@ExecSpace("ServerOnly"),将动画播放拆分到@ExecSpace("Client")。 - 如果你的目标只是更改攻击动作的武器变体,最简单的做法是替换
CostumeManagerComponent的武器槽 RUID(策略 D)。