mcpField
M
msw-avatar

msw-avatar

github.com/msw-git开发3810.0k 次安装3 周前更新
代码仓库

关于 msw-avatar

msw-avatar 是用于 MapleStory Worlds 的头像服装与动画配置指南,面向编辑项目模型、地图和脚本的开发者。它说明如何为 17 个装备槽设置头像物品、同步编辑器状态,以及使用 AvatarRendererComponent 调整颜色和透明度。

主要功能

  1. 01编辑模型与地图中的服装配置
  2. 02映射 17 个装备槽与类别
  3. 03说明长外套及武器槽占用规则
  4. 04通过 RUID 指定头像物品
  5. 05使用渲染器设置颜色与透明度

适用场景

  1. 01为 DefaultPlayer 配置默认服装
  2. 02为地图中的 NPC 设置装备
  3. 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 访问它们。

其他同步属性
属性类型描述
UseCustomEquipOnlyboolean(默认值为 false)当值为 true 时,忽略用户账号的默认服装,仅使用通过脚本/模型指定的服装。若要锁定世界内的外观,这一点很重要。
DefaultEquipUserIdstring克隆指定用户的装备,然后在此基础上应用自定义装备。也可以指定当前不在线的用户。如果该用户之后更换装备,反映出的外观也可能随之改变。
EquippedItems只读运行时的实际装备信息。无法通过脚本修改。

17 个槽位 ↔ 属性 ↔ MapleAvatarItemCategory

CostumeManagerComponent 的 17 个装备字符串字段与引擎枚举 MapleAvatarItemCategory 的对应关系如下。(枚举定义:参见 Environment/NativeScripts/Enum/MapleAvatarItemCategory.d.mlua。)

#组件属性(字符串 RUID)MapleAvatarItemCategory说明
1CustomBodyEquipBody (1)皮肤/身体
2CustomHairEquipHair (3)头发
3CustomFaceEquipFace (4)脸部/脸型
4CustomCapEquipCap (5)帽子
5CustomCapeEquipCape (6)披风
6CustomCoatEquipCoat (7)上衣
7CustomLongcoatEquipLongcoat (9)长外套——一种同时占用上衣和下装槽位的物品类别
8CustomPantsEquipPants (10)下装
9CustomGloveEquipGlove (8)手套
10CustomShoesEquipShoes (12)鞋子
11CustomOneHandedWeaponEquipOneHandedWeapon (13)单手武器
12CustomTwoHandedWeaponEquipTwoHandedWeapon (14)双手武器——同时占用单手武器槽和副武器槽
13CustomSubWeaponEquipSubWeapon (15)副武器
14CustomFaceAccessoryEquipFaceAccessory (16)脸部饰品
15CustomEyeAccessoryEquipEyeAccessory (17)眼部饰品
16CustomEarAccessoryEquipEarAccessory (18)耳部饰品
17CustomEarEquipEar (19)耳朵(身体部位)
没有对应 17 个字段的枚举值
MapleAvatarItemCategory描述
Head (2)接近“未用作装备”的类别——会自动处理,以匹配身体颜色。没有 CustomHeadEquip 字段。
Invalid (0)用于检测错误/未定义值。
Shield (11)根据枚举注释,它使用副武器槽。在存储中,最好将它视为与 CustomSubWeaponEquip 互斥。

互斥/槽位占用规则(必须了解)

  1. Longcoat ↔ Coat + Pants
    Longcoat 的设计用途是同时占用 Coat 和 Pants 槽位。装备长外套时,将长外套 RUID 放入 CustomLongcoatEquip,并从逻辑上处理与上衣/下装的组合——通常使用长外套时,应将上衣/下装留空,或避免出现相互冲突的外观。

  2. 双手武器 ↔ 单手武器 + 副武器
    TwoHandedWeapon 同时占用单手武器槽和副武器槽。使用双手武器时,应以 CustomTwoHandedWeaponEquip 为准,并确保单手武器/副武器字段也没有设置值——避免重复装备。

  3. 盾牌 ↔ 副武器
    Shield 使用副武器槽。不要指望它能与 CustomSubWeaponEquip 中的另一件副武器共存。

  4. 空字符串 = 脱下装备
    与脚本中的 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/ 下目标地图的实体记录。

  1. 在 ContentProto.Entities 数组中查找目标实体(按名称/路径/ID)。
  2. 在 jsonString["@components"] 中,找到带有 "@type": "MOD.Core.CostumeManagerComponent" 的对象。
  3. 直接在该对象上编辑 Custom*Equip、UseCustomEquipOnly、DefaultEquipUserId 等属性。
  4. 确认 componentNames 字符串列表中也列有 MOD.Core.CostumeManagerComponent,并且该列表与组件数组一致。

如果地图使用仅二进制格式,编辑工具可能会因工作区策略而异。如果文件以 JSON 文本形式打开,请遵循上述结构。


将 GET /v3/avatars 结果映射到槽位

将 GET /v3/avatars 返回项目的 category 字段映射到 Custom*Equip 属性。搜索方法请参阅 msw-search 技能 → references/resource/avatar.md。

API categoryCustom*Equip 属性MapleAvatarItemCategory
bodyCustomBodyEquipBody (1)
hairCustomHairEquipHair (3)
faceCustomFaceEquipFace (4)
faceaccessoryCustomFaceAccessoryEquipFaceAccessory (16)
eyeaccessoryCustomEyeAccessoryEquipEyeAccessory (17)
earaccessoryCustomEarAccessoryEquipEarAccessory (18)
capCustomCapEquipCap (5)
capeCustomCapeEquipCape (6)
longcoatCustomLongcoatEquipLongcoat (9)
coatCustomCoatEquipCoat (7)
pantsCustomPantsEquipPants (10)
gloveCustomGloveEquipGlove (8)
shoesCustomShoesEquipShoes (12)
weaponCustomOneHandedWeaponEquipOneHandedWeapon (13)
twohandweaponCustomTwoHandedWeaponEquipTwoHandedWeapon (14)
subweaponCustomSubWeaponEquipSubWeapon (15)
shieldCustomSubWeaponEquipShield (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(枚举)PascalCaseStand、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
ActionSheetIsLegacy = true(旧版)SyncDictionary<string, string>State→AnimationKey,例如 "ATTACK" → "attack"
StateToAvatarBodyActionSheetIsLegacy = false(新版,默认)SyncDictionary<string, AvatarBodyActionElement>例如 "ATTACK" → {AvatarBodyActionStateName="attack", PlayRate=1.33}
StateToAvatarBodyActionSheet 默认映射(IsLegacy=false 时的 11 个默认键)
键(State)AvatarBodyActionStateNamePlayRate触发条件(存在 PlayerControllerComponent 时)
IDLEstand1.0无输入
MOVEwalk1.68向左/向右移动
ATTACKattack1.33左 Ctrl(Attack 操作)
HIThit1.0HitComponent 处理命中
CROUCHcrouch1.0向下箭头
FALLfall1.0在空中下落
JUMPfall1.0空格(Jump 操作)
CLIMBrope1.0进入绳索
LADDERladder1.0进入梯子
DEADdead1.0死亡
SITsit1.0C(Sit 操作)

请注意,State 键为大写,而 AvatarBodyActionStateName 的值为小写。

默认解析表:MapleAvatarBodyActionState → 实际动作 ID

AvatarBodyActionStateName 字符串("attack"、"stand" 等)会被转换为枚举 MapleAvatarBodyActionState,随后引擎会将其解析为以下默认值,并合成一个 ActionStateChangedEvent。

MapleAvatarBodyActionStateCoreActionNamePartsActionNamePlayRatePlayType
Standstand1 / stand2相同1ZigzagLoop
Walkwalk1 / walk2相同1Loop
Attackalert(未装备武器时的默认值)alert1Loop
Crouchproneprone1Loop
Falljumpjump1Loop
Sitsitsit1Loop
Roperoperope1Loop
Ladderladderladder1Loop
Deaddeadstand11Loop
Blinkblinkblink1Loop
Flyflyfly1Loop
Hitalertalert1ZigzagLoop
Alertalertalert1ZigzagLoop
Healhealheal1Loop

装备武器时,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 映射到其中一个动作后,它就会自动播放。

身体动作名称枚举含义
standStand待机
walkWalk移动
attackAttack攻击(精灵 ID 会根据武器自动选择)
hitHit受击
crouchCrouch蹲伏
fallFall坠落
ropeRope握住绳索
ladderLadder梯子
deadDead死亡
sitSit坐下
healHeal治疗
alertAlert警戒
flyFly飞行
blinkBlink闪烁
其他情况 — 通过 ActionStateChangedEvent 直接播放

对于 14 个枚举成员之外的任意精灵动作 ID(例如 shoot1、swingO2、cast1、throw1、dance、cheer),请使用以下流程。

播放流程

  1. 从实体的 AvatarRendererComponent 调用 GetBodyEntity(),获取角色的身体实体。动画事件应发送到身体实体,而不是角色根实体。
  2. 创建一个 ActionStateChangedEvent 并填写其字段。
  3. 通过 body:SendEvent(event) 将事件发送到身体实体。
  4. 动画只需让各客户端看到,因此通常应将其限定在 @ExecSpace("Client") 中。游戏逻辑(伤害、生成投射物等)应放在 ServerOnly 一侧。

ActionStateChangedEvent 的主要字段(构造函数:ActionStateChangedEvent(coreActionName, partsActionName, playRate=1, playType=Loop, startFrameIndex=0, endFrameIndex=2147483647))

字段类型默认值描述
CoreActionNamestring""要在核心部件(身体)上播放的动画 ID。必填(例如 "shoot1"、"swingO1")
PartsActionNamestring""要在子部件上播放的动画 ID。必填,通常与 CoreActionName 的值相同
PlayRatefloat1播放速度倍率(1.0 = 正常速度,1.5 = 1.5 倍)
PlayTypeSpriteAnimClipPlayTypeLoopOnetime / Loop / ZigzagLoop。单次播放的动作请使用 Onetime
StartFrameIndexint320起始帧(负值会被限制为 0)
EndFrameIndexint322147483647结束帧(超出总帧数时会被限制)

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
字段描述
ActionStateMapleAvatarBodyActionState 枚举(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
判断流程
  1. 你想播放的动作是否属于14 种内置状态(stand、walk、attack、hit、crouch、fall、rope、ladder、dead、sit、heal、alert、fly、blink)之一?
    • 是 → 只需在 AvatarStateAnimationComponent 中对应的插槽里指定动画片段,无需脚本。
    • 否 → 继续下一步。
  2. 对于自定义动作(例如 shoot1、cast1、dance),创建一个 ActionStateChangedEvent,并将其 SendEvent 到 AvatarRendererComponent:GetBodyEntity() 返回的身体实体。
  3. 拆分执行空间:在服务器(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-searchRUID 查询,references/resource/avatar.md
msw-maker-mcprefresh,也可选用 get_component / set_property(与运行时调整结合时)

总结检查清单

服装
  1. 通过资源搜索 / Avatar 参考文档获取 RUID。
  2. DefaultPlayer / Player → ./Global/*.model 中的 Values(或基础模型定义)。
  3. 地图实体 → ./map/*.map 中目标实体的 @components。
  4. 遵守 Longcoat / 双手武器 / 盾牌 ↔ 副武器互斥规则。
  5. 通过 UseCustomEquipOnly 决定是否忽略用户账号的默认服装。
  6. 保存后,调用 msw-maker-mcp → refresh。
动画
  1. 区分状态键(大写)与身体动作名称(小写)。 格式:StateToAvatarBodyActionSheet["ATTACK"] = AvatarBodyActionElement("attack", 1.33)。
  2. 如果所需动作属于 14 种枚举身体动作(stand·walk·attack·hit·crouch·fall·rope·ladder·dead·sit·heal·alert·fly·blink),只需定义映射即可。
  3. 对于其他动作 ID(shoot1、swingT3、dance 等),创建一个 ActionStateChangedEvent,并将其 SendEvent 到 AvatarRendererComponent:GetBodyEntity() 的返回值。若要在枚举中重新启动某个状态,请使用 BodyActionStateChangeEvent + 根实体。
  4. 填写全部四个字段 — CoreActionName / PartsActionName / PlayRate / PlayType,一次性动作使用 SpriteAnimClipPlayType.Onetime。
  5. 检查是否与自动状态转换冲突。 在具有 PlayerControllerComponent 的实体上,输入会自动触发 MOVE/ATTACK/JUMP/... — 若要使用自定义攻击,请通过 RemoveActionSheet 或 SetActionSheet 清理冲突键(在服务器上调用)。
  6. 将游戏逻辑拆分到 @ExecSpace("ServerOnly"),将动画播放拆分到 @ExecSpace("Client")。
  7. 如果你的目标只是更改攻击动作的武器变体,最简单的做法是替换 CostumeManagerComponent 的武器槽 RUID(策略 D)。

评论

支持 Markdown