mcpField
M
msw-avatar

msw-avatar

github.com/msw-git開発3810.0k 回インストール3 週間前に更新
リポジトリ

msw-avatar について

msw-avatarは、MapleStory Worldsのコスチュームとアニメーション設定を扱うガイドです。モデル、マップ、スクリプトを編集する開発者向けに、装備スロットの対応、エディターの更新、AvatarRendererComponentによる色や透明度の調整を説明します。

主な機能

  1. 01モデルやマップ内のコスチューム設定編集
  2. 0217個の装備スロットとカテゴリの対応
  3. 03ロングコートや武器のスロット占有規則
  4. 04RUIDによるアバターアイテム指定
  5. 05レンダラーによる色と透明度の設定

活用シーン

  1. 01DefaultPlayerの標準コスチューム設定
  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 アバター(コスチューム・アニメーション)

アバターは、次の 2 つの軸で管理されます。

  • コスチューム(外見):MOD.Core.CostumeManagerComponent — 装備するアイテム(17 スロット)を指定します。
  • アニメーション(動き):AvatarStateAnimationComponent + AvatarRendererComponent — 再生する状態クリップ(デフォルトの 14 状態 + カスタムアクション)を指定します。

ワークスペース内のファイルを直接編集し、続いて msw-maker-mcp の refresh ツールを呼び出して、エディターに変更を反映させてください。

このドキュメントでは、まずコスチューム(ファイル編集ベース)を説明し、最後にアニメーション(スクリプトベース)を説明します。

ワークスペースのパスのルール:マップは ./map/、UI は ./ui/、スクリプトなどのアセットは ./RootDesk/MyDesk/、DefaultPlayer/Player などのグローバルモデルは ./Global/ です。


対象ごとの編集場所

対象編集するファイル備考
DefaultPlayer./Global/DefaultPlayer.modelValues 配列を通じて 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

ファイルを保存した後、Maker とその表示状態を同期するため、必ず msw-maker-mcp サーバーの refresh ツールを呼び出してください(msw-maker-mcp スキルのツール一覧を参照)。


RUID(リソース固有 ID)

コスチュームに書き込む文字列は、アバターアイテムの RUID(通常は 32 文字の 16 進文字列)です。

  • RUID を推測または捏造しないでください。msw-search スキルで検索してください。アバター 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 です。スキームを付加しないでください。サムネイル/アイコンに関する一般的なルールについては、msw-sprite-ruid スキルを参照してください。

CostumeManagerComponent の概要

アバターを使用するエンティティ(プレイヤー、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)列挙型のコメントによると、SubWeapon スロットを使用します。保存時には、CustomSubWeaponEquip と同時に装備できないものとして扱うのが安全です。

排他/スロット占有ルール(必ず理解してください)

  1. Longcoat ↔ Coat + Pants
    Longcoat は、Coat スロットと Pants スロットの両方を占有するよう設計されています。ロングコートを装備する場合は、ロングコートの RUID を CustomLongcoatEquip に設定し、coat/pants との組み合わせを論理的に解決してください。通常、ロングコートを使用する場合は coat/pants を空にするか、見た目が競合しないようにします。

  2. 両手武器 ↔ 片手武器 + サブ武器
    TwoHandedWeapon は、片手武器スロットとサブ武器スロットの両方を使用します。両手武器を使用する場合は、CustomTwoHandedWeaponEquip を中心に設定し、片手武器/サブ武器にも値が設定されていないことを確認してください。二重装備を避けてください。

  3. Shield ↔ Sub-weapon
    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. MOD.Core.CostumeManagerComponent が componentNames 文字列リストにも記載されていること、およびこのリストがコンポーネント配列と一致していることを確認します。

マップがバイナリのみの形式を使用している場合、編集ツールはワークスペースのポリシーによって異なることがあります。ファイルを 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 を収集します。

アバターの色合い/アルファ値(視覚的な色変更)

AvatarRendererComponent がアタッチされているすべてのエンティティ(DefaultPlayer、アバターを持つ NPC、モンスター)に色や透明度の効果(被弾時のフラッシュ、ゴーストのフェード、パレットの切り替えなど)を適用する場合は、レンダラー独自のメソッドを使用してください。アバターエンティティでは、SpriteRendererComponent.Color と FlipX は何もせずに終了します(isvalid(spriteRenderer) が true を返す場合でも、アバターレンダラーがスプライトレンダラーの出力を上書きします)。

メソッドシグネチャ備考
SetColor(r, g, b, a [, targetUserId])r/g/b/a は 0~1 の浮動小数点数です。アバター全体に色を付けます。クライアント ExecSpace。
SetAlpha(a [, targetUserId])0~1 の浮動小数点数です。透明度を個別に設定します。クライアント ExecSpace。
SetAvatarPartColor(category, r, g, b, a [, targetUserId])MapleAvatarItemCategory のスロットを1つだけ着色します。
@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

アバターの向き/反転には、スプライトレベルの反転を書き換えるのではなく、MovementComponent の向き API(例:MoveDirection)を使用してください。理由は同じく、書き換えても何も起こらないためです。


アニメーション — 全体構造

アバターのアニメーションは3層のパイプラインを通じて処理されます。1つの層だけを変更すると、ほかの層が変更を上書きし、意図しない動きが発生します。

[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)

主な違いは次のとおりです。

用語形式例
State key大文字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(enum)パスカルケースStand、Walk、Attack、Hit、Crouch、Fall、Sit、Rope、Ladder、Dead、Blink、Fly、Heal、Alert、Invalid
CoreActionName / PartsActionName(実際のスプライトアクション ID)小文字+数字stand1、walk1、swingO1、shoot1、prone、jump、alert など

よくある混同:「attack」はStateではありません。Stateは大文字の ATTACK、マッピング Value は小文字の attack(=MapleAvatarBodyActionState.Attack)で、その Value は武器に応じて swingO1/shoot1 などのスプライトアクション ID に解決されます。スクリプトから State を切り替える呼び出しは StateComponent:ChangeState("ATTACK")(大文字の文字列)です。「Attack」や「attack」では何も起こらず(エラーも発生せず、State が変わらないだけです)、一致しません。


AvatarStateAnimationComponent — State とモーションのマッピング

MOD.Core.AvatarStateAnimationComponent は両方のシステムを保持します。

プロパティ使用条件型備考
IsLegacy2つのシステムを切り替えるとき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個)
Key(State)AvatarBodyActionStateNamePlayRateトリガー条件(PlayerControllerComponent が存在する場合)
IDLEstand1.0入力なし
MOVEwalk1.68左右への移動
ATTACKattack1.33Left Ctrl(攻撃アクション)
HIThit1.0HitComponent による被弾処理
CROUCHcrouch1.0下矢印キー
FALLfall1.0空中で落下中
JUMPfall1.0Space(ジャンプアクション)
CLIMBrope1.0ロープに入る
LADDERladder1.0はしごに入る
DEADdead1.0死亡
SITsit1.0C(座るアクション)

State のキーは大文字で、AvatarBodyActionStateName の値は小文字です。

デフォルト解決テーブル:MapleAvatarBodyActionState → 実際のアクションID

AvatarBodyActionStateName 文字列("attack"、"stand" など)は enum の 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 と State の自動追加

MOD.Core.PlayerControllerComponent がプレイヤーエンティティにアタッチされると、以下の State が StateComponent に自動的に追加され、キー入力に応じて自動的に遷移します。

MOVE、CLIMB、LADDER、CROUCH、JUMP、FALL、ATTACK、ATTACK_WAIT、SIT

そのため、DefaultPlayer が Ctrl を押すと ATTACK state が自動的に有効になり、マッピングされた攻撃時のボディモーション(武器ごとの剣/弓/杖の振り)が自動的に再生されます。追加のスクリプトがなくても、剣を振ります。


自動再生と手動 ActionStateChangedEvent の競合(★ よくある落とし穴)

症状:スクリプトから ActionStateChangedEvent を使って shoot1 などのカスタムアクションを送信しても、**剣の振り(または武器のデフォルト攻撃)**が再生され続ける、あるいはカスタムアクションが1フレームだけ表示され、すぐに上書きされます。

原因:ATTACK が有効な間、AvatarStateAnimationComponent はマッピングされた attack のボディモーションを継続的に再送信します。1回限りのイベントはすぐに上書きされます。

解決方法
方法手順使用する状況
A. マッピングを削除するasac:RemoveActionSheet("ATTACK") を呼び出して、キーを削除します。その後、ActionStateChangedEvent でアクションを直接再生します。カスタムアクション(弓の射撃、呪文の詠唱など)で攻撃モーションを完全に置き換えたい場合
B. マッピングを変更するasac:SetActionSheet("ATTACK", "<Body Action name>") を呼び出すか、StateToAvatarBodyActionSheet["ATTACK"] を別の MapleAvatarBodyActionState に変更します。別の組み込み状態アニメーションに切り替えたい場合(例:ATTACK→回復)
C. 強制的にリセットするneedResetAction=true を指定して BodyActionStateChangeEvent を送信します。同じ状態を再開したい場合
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 の値として使用できる名前です。状態をこれらのいずれかにマッピングすると、自動的に再生されます。

ボディモーション名Enum意味
standStand待機
walkWalk移動
attackAttack攻撃(スプライト ID は武器によって自動的に選択されます)
hitHit被弾
crouchCrouchしゃがみ
fallFall落下
ropeRopeロープにつかまる
ladderLadderはしご
deadDead死亡
sitSit座る
healHeal回復
alertAlert警戒
flyFly飛行
blinkBlinkまばたき
その他 — ActionStateChangedEvent で直接再生する

14 個の enum メンバー以外の任意のスプライトアクション 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。1 回限りのアクションには Onetime を使用します
StartFrameIndexint320開始フレーム(負の値は 0 に補正されます)
EndFrameIndexint322147483647終了フレーム(全フレーム数を超える場合は補正されます)

SpriteAnimClipPlayType:

値意味
Onetime1 回再生して停止します
Loop0 から終端までを繰り返します
ZigzagLoop0 から終端まで再生した後、0 に戻る動作を繰り返します
BodyActionStateChangeEvent — 組み込みの 14 種類の状態に対応する高レベルイベント

MapleAvatarBodyActionState の enum 値を直接指定します。武器ごとのアクション 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 の enum(Stand/Walk/Attack/Hit/...)
needResetActiontrue の場合、すでに再生中の状態であっても最初から強制的に再スタートします
playRate / startFrameIndex / endFrameIndexActionStateChangedEvent と同じです

使い分け

  • 任意のスプライトアクション ID(shoot1、swingO2、dance など)→ ActionStateChangedEvent(ボディエンティティに送信)
  • 14 種類の enum 状態のいずれか(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 を作成し、AvatarRendererComponent:GetBodyEntity() が返すボディエンティティに SendEvent で送信します。
  3. 実行空間を分けます。入力処理とダメージ判定はサーバー側(ServerOnly)、**アニメーション再生はクライアント側(Client)**で行います。
よくある間違い
  • State キーと AvatarBodyActionStateName(=列挙型)を混同する。 State キーは大文字(ATTACK)、マッピングの値は小文字(attack)です。StateToAvatarBodyActionSheet のキーと値を入れ替えると、マッピングはエラーなく機能しなくなります。
  • 自動再生を無効にせず、ActionStateChangedEvent だけを送信する。 Ctrl キーを押すと ATTACK 状態が自動的に有効になり、マッピングされた攻撃モーションがイベントのモーションを即座に上書きします。カスタムの攻撃モーションを使うには、RemoveActionSheet("ATTACK") または SetActionSheet("ATTACK", "<desired motion>") でマッピングを必ず解除してください。
  • ActionStateChangedEvent の SendEvent 先を間違える。 AvatarRendererComponent:GetBodyEntity() が返すボディエンティティに送信する必要があります。self.Entity(アバターのルート)やコンポーネントに送信しても再生されません。(一方、BodyActionStateChangeEvent の送信先はルートエンティティです。)
  • DefaultPlayer 型のエンティティ(PlayerControllerComponent + StateComponent + AvatarStateAnimationComponent が実行中)の AvatarBodyActionSelectorComponent.ActionState を直接書き換える。 コントローラーは毎ティック、接地状態・移動・入力を再評価し、遷移時に 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. リソース検索/アバターリファレンスのドキュメントで RUID を取得します。
  2. DefaultPlayer / Player → ./Global/*.model(またはベースモデルの定義)の Values。
  3. マップエンティティ → ./map/*.map 内の対象エンティティの @components。
  4. ロングコート/両手武器/盾 ↔ サブ武器の併用制限を守ります。
  5. UseCustomEquipOnly でユーザーのアカウント既定コスチュームを無視するかどうかを決めます。
  6. 保存後、msw-maker-mcp → refresh を呼び出します。
アニメーション
  1. State キー(大文字)とボディモーション名(小文字)を区別します。 形式: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 を作成し、AvatarRendererComponent:GetBodyEntity() の戻り値に対して SendEvent します。列挙型内の State を再生し直すには、BodyActionStateChangeEvent とルートエンティティを使用します。
  4. 4 つすべてのフィールド(CoreActionName / PartsActionName / PlayRate / PlayType)に値を設定し、ワンショットアクションには SpriteAnimClipPlayType.Onetime を使用します。
  5. 自動 State 遷移との競合を確認します。 PlayerControllerComponent を持つエンティティでは、入力に応じて MOVE/ATTACK/JUMP/... が自動的に発火します。カスタム攻撃を使用するには、競合するキーを RemoveActionSheet または SetActionSheet で削除します(サーバーで呼び出します)。
  6. ゲームロジックは @ExecSpace("ServerOnly") に、アニメーション再生は @ExecSpace("Client") に分けます。
  7. 攻撃モーションの武器バリエーションだけを変更する場合は、CostumeManagerComponent の武器スロット RUID を入れ替える方法が最も簡単です(Strategy D)。

コメント