Loading...
Loading...
MSW 아바타 관리 — 코스튬(CostumeManagerComponent 17슬롯) + 애니메이션 3계층 파이프라인(StateComponent → AvatarStateAnimationComponent → AvatarRendererComponent). State 키(대문자)/AvatarBodyActionStateName(소문자)/MapleAvatarBodyActionState enum/스프라이트 액션 ID(swingO1, shoot1) 4단계 구분, IsLegacy/ActionSheet/StateToAvatarBodyActionSheet 두 시스템, PlayerControllerComponent 자동 상태 전이와 ActionStateChangedEvent 충돌 시 RemoveActionSheet/SetActionSheet/BodyActionStateChangeEvent로 해결. DefaultPlayer뿐 아니라 모든 엔티티(NPC, 몬스터 등)에 적용 가능. Use for avatar costume get/set, 17 equip slots, animation state mapping, action override, weapon-specific attack motion, custom shoot/cast/dance action. Keywords: avatar, costume, animation, state, action, shoot, swing, weapon, ActionStateChangedEvent, BodyActionStateChangeEvent, RemoveActionSheet, SetActionSheet, StateToAvatarBodyActionSheet, AvatarStateAnimationComponent, AvatarRendererComponent, MapleAvatarBodyActionState, PlayerControllerComponent, 아바타, 코스튬, 애니메이션, 상태, 동작, 장비, 무기, 공격, 활, 칼, 사격, 휘두르기, 커스텀 액션, 자동 재생 차단, 매핑 변경.
npx skill4agent add msw-git/msw-ai-coding-plugins-official msw-avatarMOD.Core.CostumeManagerComponentAvatarStateAnimationComponentAvatarRendererComponentget_costumeset_costumemsw-maker-mcprefresh워크스페이스 경로 규칙: 맵, UI./map/, 스크립트·기타 에셋./ui/, DefaultPlayer·Player 등 글로벌 모델./RootDesk/MyDesk/./Global/
| 대상 | 편집 파일 | 비고 |
|---|---|---|
| DefaultPlayer | | |
| Player (베이스) | | 보통 코스튬 기본값은 여기보다 DefaultPlayer.model에서 오버라이드 |
| 맵에 배치된 엔티티 (NPC, 몬스터 등) | | 해당 엔티티의 |
| 커스텀 모델만 참조하는 엔티티 | 해당 | 맵에 인라인 컴포넌트가 없고 |
CostumeManagerComponentValuesget_componentmsw-maker-mcprefreshrefreshmsw-maker-mcprefreshmsw-maker-mcpmsw-searchreferences/avatar.mdreferences/search.mdreferences/detail.mdSetEquip(MapleAvatarItemCategory, itemRUID)Custom*EquipGetEquipSetEquipMapleAvatarItemCategory| 프로퍼티 | 타입 | 설명 |
|---|---|---|
| UseCustomEquipOnly | | |
| DefaultEquipUserId | | 지정한 유저의 장비를 복제한 뒤, 그 위에 커스텀 장비를 얹는 방식. 접속하지 않은 유저도 지정 가능. 대상 유저가 이후 장비를 바꾸면 반영이 달라질 수 있다. |
| EquippedItems | 읽기 전용 | 런타임에서 실제 장착 정보. 스크립트에서 수정 불가. |
CostumeManagerComponentMapleAvatarItemCategoryEnvironment/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) | 귀(신체 파츠) |
| MapleAvatarItemCategory | 설명 |
|---|---|
| Head (2) | “장비로 쓰지 않음”에 가깝고 바디 색에 맞춰 자동 처리되는 분류. |
| Invalid (0) | 오류/미정의 검출용. |
| Shield (11) | enum 주석상 보조 무기 슬롯(SubWeapon) 을 사용. 실질 저장은 CustomSubWeaponEquip 쪽과 배타적으로 정리하는 것이 안전하다. |
CustomLongcoatEquipSetEquip(category, "")""Values./Global/DefaultPlayer.modelContentProto.Json.Values"MOD.Core.CostumeManagerComponent"CustomCapEquipUseCustomEquipOnlyDefaultPlayer.modelValuesSystem.String, mscorlib, ...System.Boolean, mscorlib, ...truefalse(TargetType, Name){
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "CustomCapEquip",
"ValueType": {
"$type": "MODNativeType",
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": "여기에_32자hex_RUID"
}{
"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.EntitiesjsonString["@components"]"@type": "MOD.Core.CostumeManagerComponent"Custom*EquipUseCustomEquipOnlyDefaultEquipUserIdcomponentNamesMOD.Core.CostumeManagerComponent맵이 바이너리 포맷만 쓰는 경우 등은 워크스페이스 정책에 따라 편집 도구가 다를 수 있다. JSON 텍스트로 열리는 경우 위 구조를 따른다.
GET /v3/avatarscategoryCustom*Equipmsw-searchreferences/resource/avatar.mdAPI | | |
|---|---|---|
| | Body (1) |
| | Hair (3) |
| | Face (4) |
| | FaceAccessory (16) |
| | EyeAccessory (17) |
| | EarAccessory (18) |
| | Cap (5) |
| | Cape (6) |
| | Longcoat (9) |
| | Coat (7) |
| | Pants (10) |
| | Glove (8) |
| | Shoes (12) |
| | OneHandedWeapon (13) |
| | TwoHandedWeapon (14) |
| | SubWeapon (15) |
| | Shield (11) — SubWeapon 슬롯 공유 |
msw-searchreferences/resource/avatar.mdGET /v3/avatarsGET /v3/avatars/{ruid}[1] 입력 / 게임 로직
│ PlayerControllerComponent · 스크립트
▼
[2] StateComponent ──── StateChangeEvent ────▶ AvatarStateAnimationComponent
(ex. "ATTACK") (CurrentStateName) (StateToAvatarBodyActionSheet
또는 ActionSheet 룩업)
│
▼
[3] AvatarRendererComponent ◀── BodyActionStateChange / ActionStateChanged ── 바디 엔티티
(실제 스프라이트 재생)| 용어 | 형식 | 예시 |
|---|---|---|
| State 키 | 대문자 영문 | |
| AvatarBodyActionStateName (Value 쪽) | 소문자 영문 | |
| MapleAvatarBodyActionState (enum) | Pascal case | |
| CoreActionName/PartsActionName (실제 스프라이트 액션 ID) | 소문자+숫자 | |
흔한 혼동: "attack"은 State 가 아니다. State는 대문자, 매핑 Value는 소문자ATTACK(= MapleAvatarBodyActionState.Attack), 그리고 그 Value가 다시 무기에 따라attack/swingO1같은 스프라이트 액션 ID로 풀린다.shoot1
MOD.Core.AvatarStateAnimationComponent| 프로퍼티 | 사용 조건 | 형식 | 비고 |
|---|---|---|---|
| 두 시스템 선택 스위치 | | |
| | | |
| | | |
StateToAvatarBodyActionSheet| Key (State) | AvatarBodyActionStateName | PlayRate | 트리거 조건(PlayerControllerComponent 동반 시) |
|---|---|---|---|
| | 1.0 | 입력 없음 |
| | 1.68 | 좌/우 이동 |
| | 1.33 | Left Ctrl (Attack 액션) |
| | 1.0 | HitComponent 피격 처리 |
| | 1.0 | 아래 방향키 |
| | 1.0 | 공중에서 낙하 |
| | 1.0 | Space (Jump 액션) |
| | 1.0 | 로프 진입 |
| | 1.0 | 사다리 진입 |
| | 1.0 | 사망 |
| | 1.0 | C (Sit 액션) |
State 키는 대문자, AvatarBodyActionStateName 값은 소문자라는 점에 주의.
MapleAvatarBodyActionStateAvatarBodyActionStateName"attack""stand"MapleAvatarBodyActionStateActionStateChangedEvent| MapleAvatarBodyActionState | CoreActionName | PartsActionName | PlayRate | PlayType |
|---|---|---|---|---|
| Stand | | 동일 | 1 | ZigzagLoop |
| Walk | | 동일 | 1 | Loop |
| Attack | | | 1 | Loop |
| Crouch | | | 1 | Loop |
| Fall | | | 1 | Loop |
| Sit | | | 1 | Loop |
| Rope | | | 1 | Loop |
| Ladder | | | 1 | Loop |
| Dead | | | 1 | Loop |
| Blink | | | 1 | Loop |
| Fly | | | 1 | Loop |
| Hit | | | 1 | ZigzagLoop |
| Alert | | | 1 | ZigzagLoop |
| Heal | | | 1 | Loop |
무기를 장착하면은 무기 종류에 맞는 스프라이트 액션 ID로 자동 치환된다 (다음 절 표 참조). 즉 한손검을 쥐면 칼 휘두르기, 활을 쥐면 활 쏘기 모션이 나온다.Attack
attackATTACK| 무기 분류 | 사용되는 CoreActionName/PartsActionName 후보 |
|---|---|
한손검/단검 ( | |
두손검/해머 ( | |
활 ( | |
| 스태프/완드 | |
| 무기 없음 (기본 body) | 별도 attack 클립 없음 → |
같은 분류라도 아이템 메타에 따라 사용되는 액션 ID 집합이 다를 수 있다. 위 표는 SDK 가이드()가 사용하는 대표 후보군._ActionNameLogic
MOD.Core.PlayerControllerComponentStateComponentMOVECLIMBLADDERCROUCHJUMPFALLATTACKATTACK_WAITSITActionStateChangedEventshoot1ATTACKAvatarStateAnimationComponentattack| 전략 | 방법 | 언제 쓰나 |
|---|---|---|
| A. 매핑 제거 | | 공격 모션을 완전히 커스텀으로 대체할 때 (활 쏘기, 마법 시전 등) |
| B. 매핑 변경 | | 다른 내장 상태 애니메이션으로 바꾸고 싶을 때 (예: ATTACK→heal) |
| C. 강제 리셋 | | 같은 상태를 재시작하고 싶을 때 |
| D. 무기 교체 | | 단순히 attack 모션의 무기 종류만 바꾸고 싶을 때 (가장 직관적) |
@Component
script PlayerAttack extends AttackComponent
@HideFromInspector
property any Shape = nil
@ExecSpace("ServerOnly")
method void OnBeginPlay()
self.Shape = BoxShape(Vector2.zero, Vector2.one, 0)
-- 엔진이 ATTACK 상태에서 자동 재생하던 attack(=칼 휘두르기) 매핑을 제거
local asac = self.Entity.AvatarStateAnimationComponent
if isvalid(asac) then
asac:RemoveActionSheet("ATTACK")
end
end
@ExecSpace("ServerOnly")
method void AttackNormal()
-- ... 데미지 판정 ...
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
./Global/DefaultPlayer.modelValuesshoot1{
"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 — 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
}AvatarStateAnimationComponentMapleAvatarBodyActionStateswingO2shoot1dancecast1StateToAvatarBodyActionSheetActionSheet| 바디 동작 이름 | enum | 의미 |
|---|---|---|
| Stand | 대기 |
| Walk | 이동 |
| Attack | 공격(무기에 따라 sprite ID 자동 결정) |
| Hit | 피격 |
| Crouch | 웅크리기 |
| Fall | 낙하 |
| Rope | 로프 잡기 |
| Ladder | 사다리 |
| Dead | 사망 |
| Sit | 앉기 |
| Heal | 회복 |
| Alert | 경계 |
| Fly | 비행 |
| Blink | 눈 깜박임 |
ActionStateChangedEventshoot1swingO2cast1throw1dancecheerAvatarRendererComponentGetBodyEntity()ActionStateChangedEventbody:SendEvent(event)@ExecSpace("Client")ServerOnlyActionStateChangedEventActionStateChangedEvent(coreActionName, partsActionName, playRate=1, playType=Loop, startFrameIndex=0, endFrameIndex=2147483647)| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| string | | 코어 파츠(바디)에서 재생할 애니메이션 ID. 필수 (예: |
| string | | 부위 파츠에서 재생할 애니메이션 ID. 필수 — 보통 |
| float | | 재생 속도 배율 ( |
| | | |
| int32 | | 시작 프레임 (음수면 0으로 보정) |
| int32 | | 끝 프레임 (총 프레임 초과 시 자동 보정) |
SpriteAnimClipPlayType| 값 | 의미 |
|---|---|
| 1회 재생 후 정지 |
| 0→끝 반복 |
| 0→끝→0 왕복 반복 |
BodyActionStateChangeEventMapleAvatarBodyActionStateneedResetAction=trueSendEventActionStateChangedEventself.Entitylocal event = BodyActionStateChangeEvent()
event.ActionState = MapleAvatarBodyActionState.Fly
event.needResetAction = true
event.startFrameIndex = 1
event.endFrameIndex = 2
self.Entity:SendEvent(event)
-- 내부적으로 ActionStateChangedEvent("fly", "fly", 1, Loop, 1, 2)로 변환되어 전달| 필드 | 설명 |
|---|---|
| |
| |
| |
shoot1swingO2danceActionStateChangedEventBodyActionStateChangeEventshootshoot1@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
endstandwalkattackhitcrouchfallropeladderdeadsithealalertflyblinkAvatarStateAnimationComponentshoot1cast1danceActionStateChangedEventAvatarRendererComponent:GetBodyEntity()SendEventServerOnlyClientATTACKattackStateToAvatarBodyActionSheetATTACKRemoveActionSheet("ATTACK")SetActionSheet("ATTACK", "<원하는 동작>")ActionStateChangedEventAvatarRendererComponent:GetBodyEntity()self.EntityBodyActionStateChangeEventAvatarStateAnimationComponentMapleAvatarBodyActionStateshootcastdanceActionStateChangedEventPartsActionNameCoreActionNameCoreActionNamePlayTypeLoopSpriteAnimClipPlayType.OnetimeRemoveActionSheetSetActionSheetStateToAvatarBodyActionSheet@SyncServerOnlyClientshoot1CustomTwoHandedWeaponEquip| 스킬 | 용도 |
|---|---|
| msw-defaultplayer | |
| msw-search | RUID 검색, |
| msw-maker-mcp | |
./Global/*.modelValues./map/*.map@componentsUseCustomEquipOnlymsw-maker-mcprefreshStateToAvatarBodyActionSheet["ATTACK"] = AvatarBodyActionElement("attack", 1.33)standwalkattackhitcrouchfallropeladderdeadsithealalertflyblinkshoot1swingT3danceActionStateChangedEventAvatarRendererComponent:GetBodyEntity()SendEventBodyActionStateChangeEventCoreActionNamePartsActionNamePlayRatePlayTypeSpriteAnimClipPlayType.OnetimeMOVE/ATTACK/JUMP/...RemoveActionSheetSetActionSheet@ExecSpace("ServerOnly")@ExecSpace("Client")CostumeManagerComponent